170 companion flashcards · AI-assisted study content · Open the deck →
This deck focuses on some of the most important intermediate and advanced features of C# programming. The questions cover four major areas: LINQ for querying and transforming collections, async/await for writing responsive asynchronous code, delegates as a foundation for callback-style programming, and events for building observable patterns. Together, these topics form the backbone of modern, real-world C# development.
The cards are best suited for learners who already feel comfortable with basic C# syntax, such as classes, methods, and loops, and are ready to move into more powerful language features. If you are preparing for a coding interview, transitioning from another object-oriented language, or trying to level up on a C# project at work, this deck can help you review the concepts and terminology you will encounter again and again.
Because the topics are related, try to notice the connections as you study. Delegates are the mechanism that makes LINQ possible and that underlies the event system, while async/await is built on top of the Task types. Understanding these relationships will make the individual facts easier to remember than treating each card in isolation.
For the best results, work through the deck in short, spaced sessions rather than long cramming blocks. A few cards a day, with occasional review of older material, will help these concepts move from short-term recall into confident, lasting knowledge you can apply when reading or writing real C# code.
The C# type system distinguishes between value types and reference types with real consequences for performance and behavior. A struct is a value type typically allocated on the stack, copied on assignment, and zero-initialized by default; a class is a reference type allocated on the heap, assigned by reference, and null by default. When a struct is assigned to an object or interface variable, it is boxed, copying the value to the heap, and the reverse requires an explicit cast. Records (C# 9+) and record structs (C# 10+) provide value-based equality and a with expression for non-destructive mutation, blurring the line between class and struct idioms. Structs cannot inherit from other structs, while classes support single inheritance, and large mutable structs suffer from copy semantics that hurt performance. Two struct instances with the same field values are value-equal, while two class instances with the same fields are reference-equal by default unless Equals and GetHashCode are overridden or the type is a record.
Generics let you parameterize classes, methods, and interfaces over a type, providing type safety at compile time without the cost of boxing. Constraints expressed with where clauses, such as class, struct, new(), a base class, or an implemented interface, narrow the allowed type arguments. Interfaces and delegates also support variance: covariance (out T) lets a generic type be used as a more derived type, while contravariance (in T) lets it be used as a less derived type. Types often implement IEquatable
C# provides rich syntax for declaring members. Properties, including auto-implemented and expression-bodied forms, encapsulate fields behind get and set accessors, and init-only setters enable immutable initialization. Indexers allow array-like access with any parameter type. Extension methods add methods to existing types without modifying them, provided they live in a static class and use the this modifier on the first parameter, with instance methods taking precedence over matching extensions. Access modifiers (public, private, protected, internal, protected internal, private protected, and file in C# 11) control visibility, with internal types scoped to the same assembly. An assembly is the compiled output of a project, a DLL or EXE that contains IL, metadata, and a manifest. The partial keyword lets a type be split across multiple files, which is common in code generation. Constants are inlined at compile time, while readonly fields are runtime constants settable in a constructor; a static class can hold only static members and is implicitly sealed and abstract, while a sealed class can be instantiated but cannot be inherited, and a static constructor runs once per type before the first instance is created or any static member is accessed.
Polymorphism in C# takes several forms. Subtype polymorphism, enabled by virtual and override, lets the runtime type determine which method executes; abstract methods have no implementation and must be overridden, while virtual methods provide a default. Operator overloading lets you redefine operators like +, -, and == on custom types, with paired overloads and matching Equals and GetHashCode expected. Interfaces define contracts that implementing classes must provide, and C# 8+ allows default interface methods so an interface can ship a default implementation; explicit interface implementation makes a member accessible only through an interface reference, useful for resolving naming conflicts between multiple interfaces. The var keyword is statically typed with the type inferred at compile time, while dynamic bypasses compile-time checks via the Dynamic Language Runtime. The is operator tests type compatibility and can extract a value via pattern matching, while as performs a safe cast that returns null on failure; is null and == null differ when == is overloaded. The Convert class uses banker's rounding on doubles and tolerates null, while an explicit cast on a double truncates toward zero. Method parameters support ref (must be initialized, may be read and written), out (need not be initialized, must be assigned by the method), and in (read-only reference that avoids copies of large structs); ref locals and ref returns let you bind directly to a variable for in-place modification of struct fields. MemberwiseClone makes a shallow copy, while deep copies require recursive duplication. The checked and unchecked keywords control whether integer overflow throws OverflowException or silently wraps.
Delegates are type-safe function pointers that form the foundation for events, callbacks, and most functional patterns in C#. You declare a delegate type with a specific signature, then assign methods or lambdas to instances. The standard library provides built-in delegate types: Action and its generic variants return void, while Func and its generic variants return a value specified by the last type parameter. A multicast delegate holds references to multiple methods and invokes them in order when called, with += and -= adding and removing handlers. The Predicate
Events are a higher-level mechanism built on delegates that implements the publisher-subscriber pattern with proper encapsulation. While a delegate field can be invoked by any code that has access to it, an event restricts external code to subscribing and unsubscribing only, since only the declaring class can raise it. The standard event pattern uses EventHandler or EventHandler
Lambda expressions are anonymous functions written with the => operator, used heavily with LINQ and delegate-typed parameters. They come in two forms: an expression-bodied form (x => x * x) and a statement-bodied form with curly braces and an explicit return. C# 9+ allows static lambdas that cannot capture variables from the enclosing scope, which avoids the closure allocation that captured variables would otherwise create. Local functions are named methods declared inside another method; unlike lambdas, they can be marked static, generic, and ref/in/out, and they emit as real methods rather than delegate instances when not converted. They are ideal for pure, named subroutines inside methods.
For push-based streams, the IObserver
Modern C# has steadily added features that improve expressiveness and reduce boilerplate. Pattern matching tests a value against a pattern and extracts information, with common forms including type patterns (obj is string s), constant patterns (x is 42), and property patterns (p is { Age: > 18 }). C# 8 introduced switch expressions that use => to map patterns to values, with _ as the discard pattern serving as a default. C# 9 added relational and logical patterns so you can write score switch { >= 90 => "A", >= 80 and < 90 => "B", _ => "C" } using and, or, and not combinators. List patterns (C# 11) match sequences: [1, 2, ..] matches a sequence starting with 1 and 2, [_, _, _] matches exactly three elements, and [] matches the empty sequence, with the .. slice pattern matching zero or more elements. These work especially well with Span
Records (C# 9) are reference types designed for immutable data with value-based equality, and record structs (C# 10) are the value-type equivalent, allocated on the stack with the same equality and with expression semantics. A positional record like public record Person(string Name, int Age) auto-generates Equals, GetHashCode, ToString, and a Deconstruct method. The with expression creates a copy with selected properties changed, leaving the original intact. Init-only setters (C# 9) allow properties to be set only during object initialization, making them effectively immutable thereafter. The required modifier (C# 11) forces callers to set those properties at construction time, eliminating a class of bugs around unset state. C# 12 introduced primary constructors, which let you declare constructor parameters directly on the type, automatically making them available throughout the class body and removing the boilerplate of explicit field declaration. Tuples (ValueTuple) are lightweight value-type groupings, and deconstruction assigns tuple elements to individual variables, also available on custom types that implement a Deconstruct method.
C# 12 introduced collection expressions that unify array, list, and span literals: int[] arr = [1, 2, 3] works for any compatible target, and the spread operator .. combines collections, so int[] combined = [..arr, ..other] concatenates them. Index from end (^n) and range (a..b) operators enable concise slicing: arr[^1] is the last element, arr[1..3] is a slice, and arr[^2..] is the last two; types support these by exposing an int indexer and a Range indexer. Top-level statements let you write Program.cs without the class Program { static void Main() } boilerplate, with the compiler synthesizing a Main behind the scenes; only one file in a project may use them. Global using directives apply a using statement to every file in the project, and .NET 6+ projects auto-emit common ones like System, System.Collections.Generic, System.IO, and System.Linq.
Nullable value types let value types represent null, so int? age = null is shorthand for Nullable
LINQ (Language Integrated Query) lets you write query expressions directly in C# to filter, sort, and transform data from collections, databases, and XML. There are two equivalent syntaxes: query syntax resembles SQL with from, where, select, and orderby clauses, while method syntax uses extension methods like Where, Select, and OrderBy chained with dot notation. Both compile to the same IL code, so the choice is mostly stylistic. Most LINQ operators return IEnumerable
The core operators form a small, composable vocabulary. Select projects each element into a new form, essentially a map operation. Where filters elements based on a predicate. OrderBy and ThenBy produce a sorted sequence. GroupBy groups elements by a key, returning IEnumerable
LINQ also supports more advanced shapes. Join pairs elements by a key, like SQL's INNER JOIN, while Zip pairs elements positionally and stops at the shorter sequence. SelectMany flattens nested sequences: orders.SelectMany(o => o.Items) returns a single IEnumerable
A critical distinction is between IEnumerable
The async and await keywords let you write asynchronous code that reads like synchronous code. Marking a method async does not make it run on a separate thread by itself; rather, it allows the compiler to generate a state machine that handles continuations. When the runtime hits an await on a Task or Task
Choosing between Task and Thread matters. A Thread is a low-level OS thread, expensive to create (each carries roughly a 1 MB stack), and you manage its lifecycle directly. A Task is a higher-level abstraction over work that runs on the thread pool, cheap to create, with the runtime reusing pool threads. Prefer Task for almost everything; reserve Thread for long-lived, identity-bearing execution such as dedicated background loops with IsBackground. For coordinating multiple operations, Task.WhenAll awaits a collection of tasks concurrently and completes when all are done, while Task.WhenAny completes as soon as any one task finishes, making it useful for implementing timeouts or racing multiple sources. CancellationToken flows cancellation through async pipelines: a CancellationTokenSource provides the token, async methods periodically check IsCancellationRequested or call ThrowIfCancellationRequested, and OperationCanceledException surfaces to the caller. IAsyncDisposable, used with await using, is the asynchronous counterpart of IDisposable for resources that require async cleanup. Many types implement both IDisposable and IAsyncDisposable, and the runtime calls the right one based on the using form.
Several pitfalls are worth knowing. Exceptions in async methods are captured in the returned Task, so a try/catch with await is sufficient; without await, the exception goes unobserved. ExceptionDispatchInfo.Capture(ex).Throw() preserves the original stack trace when rethrowing across thread or async boundaries, so prefer it over the older throw ex pattern, which resets the stack trace to the current line. async void is dangerous outside top-level event handlers because exceptions go directly to the synchronization context (or crash the process) and cannot be awaited. Thread.Sleep blocks the current thread for a duration, while await Task.Delay yields the thread back to the pool, keeping the application responsive. TaskCompletionSource
Parallelism in .NET comes in several flavors. Parallel.ForEach executes foreach iterations across multiple threads, ideal for CPU-bound work, while PLINQ (Parallel LINQ) is the parallel version of LINQ, enabled with AsParallel(); both partition the work across cores but suffer overhead on small collections, so gains diminish quickly. For I/O-bound work, async/await with Task.WhenAll is the right tool, since it does not tie up threads while waiting. Be cautious with shared state in parallel code and prefer Concurrent collections or synchronization.
Modern C# provides several types for working with memory directly while avoiding garbage collector pressure. Span
Strings are immutable in C#, so every modification creates a new allocation. This makes StringBuilder the right tool when building strings in loops or concatenating many pieces, with O(1) amortized appends. String interpolation (C# 6) embeds expressions in a string with $ and {} placeholders, with the compiler emitting a string.Format call or, in .NET 6+, a DefaultInterpolatedStringHandler for better performance. Verbatim strings (prefix @) disable escape processing and allow multi-line text, with "" for embedded quotes, and they combine with $ for interpolated verbatim strings. Raw string literals (C# 11) delimit multi-line text with three or more double quotes, allowing embedded quotes freely and using indentation rules based on the closing quote position. When the final length is known, string.Create(length, state, action) is the fastest way to format a string, since it writes directly into the backing Span
String interning stores one copy of each distinct literal in a global pool, with string.Intern and string.IsInterned for runtime values; interned strings are reference-equal, which can be useful for memory optimization but is risky for unbounded or untrusted data. StringComparison.Ordinal compares raw code units, is culture-independent and fast, and is appropriate for internal identifiers, paths, and security checks; StringComparison.CurrentCulture uses the current culture's collation rules and suits user-visible text. Similarly, StringComparer.Ordinal is the right choice for hash-based collections of identifiers, while StringComparer.InvariantCulture applies the invariant culture's rules for cross-machine consistency. The System.Text.Encoding class converts between chars and bytes: Encoding.UTF8 is the safe default, while Encoding.Default uses the system's ANSI code page and should be avoided in cross-platform code. For null checks, string.IsNullOrEmpty covers null and empty strings, while string.IsNullOrWhiteSpace also treats whitespace-only strings as null. The Convert class uses banker's rounding on doubles and tolerates null input, while an explicit cast on a double truncates toward zero. Boxing and unboxing remain expensive in hot paths, which is why generics like List
The choice of collection profoundly affects performance. Dictionary
Concurrency control in C# has several layers. The lock statement is syntactic sugar over Monitor.Enter and Monitor.Exit inside a try/finally, with the locked object ideally a private, static, reference-typed field; locking on this, typeof(...), or strings is a known anti-pattern that can cause deadlocks. Use Monitor directly only when you need TryEnter, Wait, Pulse, or finer control. volatile tells the compiler that a field may be modified by multiple threads, preventing reordering and caching of reads and writes, but it is limited to specific primitive types and reference types. Concurrent collections in System.Collections.Concurrent, including ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBag, and BlockingCollection, provide thread-safe access without external locking, using lock-free and fine-grained locking internally. For cross-process coordination, Mutex is a single-owner lock that can be named system-wide (often used to ensure only one instance of an app runs), while SemaphoreSlim allows N concurrent holders in-process. AsyncLocal
Resource management follows the IDisposable pattern. A class that owns unmanaged resources should implement IDisposable and follow the dispose pattern: a public Dispose() method that calls Dispose(true) and GC.SuppressFinalize, a protected virtual Dispose(bool disposing) method that frees both managed and unmanaged resources when disposing is true and unmanaged resources only when false, and a finalizer that calls Dispose(false) as a last-resort safety net. Dispose is called deterministically by user code (typically via using), while the finalizer is called by the GC at an indeterminate time and should not allocate or reference other managed objects. Modern code often avoids finalizers by using SafeHandle. The using statement comes in two forms: the classic using (var r = ...) { ... } disposes at the end of the block, while the C# 8 using var r = ... declaration disposes at the end of the enclosing scope, reducing indentation. Throw helpers in .NET 6+ (ArgumentNullException.ThrowIfNull, ArgumentException.ThrowIfNullOrEmpty, ArgumentException.ThrowIfNullOrWhiteSpace) reduce validation boilerplate. Exception.Data is an IDictionary for arbitrary context like request IDs, while InnerException is the conventional wrapped cause; use Data for telemetry and throw new MyException("msg", innerEx) to wrap a cause. Even in managed code, memory leaks arise from references the GC cannot see as unreachable: static fields, unsubscribed event handlers, unbounded caches, and undisposed IDisposable instances. WeakReference
Reflection, logging, and configuration support the runtime diagnostic surface. Reflection in System.Reflection lets you inspect types and invoke members dynamically, which is powerful for DI containers, ORMs, and serializers but is slow and lacks compile-time checks; source generators and expression trees are modern alternatives for performance-critical paths. The System.Text.Json source generator emits serialization code at compile time, eliminating reflection and enabling AOT and trimmed apps. nameof returns a symbol's name as a string with compile-time checking, ideal for argument validation and INotifyPropertyChanged. The CallerMemberName, CallerFilePath, and CallerLineNumber attributes auto-fill parameters with the calling context. Microsoft.Extensions.Logging.ILogger
var results = list.Where(x => x > 5).OrderBy(x => x);public T Max<T>(T a, T b) where T : IComparable<T> {
return a.CompareTo(b) >= 0 ? a : b;
}T at the call site: Max(3, 7);var point = (X: 3, Y: 4);
Console.WriteLine(point.X);(string Name, int Age) GetPerson() => ("Alice", 30);ValueTuple (value type) under the hood.$ prefix and { } placeholders:var s = $"Hello, {name}! You are {age} years old.";string.Format or DefaultInterpolatedStringHandler call (C# 10+). Use {{ and }} to escape literal braces.public readonly int MaxItems;const, which is baked in at compile time. readonly works with both instance and static fields.List<T> is an ordered, indexable sequence. Contains is O(n).HashSet<T> is an unordered collection of unique items with O(1) Contains, Add, Remove.HashSet for membership tests and deduplication; use List when order and indexing matter.IDisposable not disposed (especially with unmanaged resources)GC.GetTotalMemory(true).internal — accessible anywhere in the same assembly.protected internal — accessible in the same assembly OR in any derived class (even in other assemblies). It is a union of protected and internal.private protected — accessible only in the same assembly AND in derived types. It is an intersection of protected and internal.where T : new() — the type argument must expose a public parameterless constructor. The compiler can then new T() inside the generic.where T : class — the type argument must be a reference type; no constructor is required.where T : class, new().File.ReadAllText(path) returns the entire file as a single string.File.ReadAllLines(path) returns a string[], one element per line.File.OpenText + StreamReader or File.ReadLines(path) (lazy IEnumerable<string>).Drill this topic
170 flashcards on Csharp Programming — free, no signup needed to start.
Study Csharp Programming flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.