Skip to content

Csharp Programming

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.

Type System and Language Fundamentals

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 for allocation-free equality, IComparable for natural ordering, and IComparer for alternative orderings. The ICloneable interface is widely considered a design mistake because its Clone method does not specify whether the copy is shallow or deep, and modern APIs prefer explicit copy methods or record with-expressions. The Predicate delegate is largely historical; Func is the modern preference for predicates, especially with LINQ.

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.

Functions, Delegates, and Events

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 delegate has the same shape as Func and was historically used by collection filtering methods like List.Find and Array.FindAll, but Func is the preferred form in modern code, especially with LINQ.

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, where TEventArgs derives from System.EventArgs, and the raising class typically exposes a protected virtual method that calls Invoke on the event. This pattern delivers the sender and the event payload to subscribers, which is why EventHandler is preferred over a plain Action for events. Custom callback delegates like Action remain appropriate for non-event scenarios, such as Timer constructors.

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 interface defines OnNext, OnError, and OnCompleted, while IObservable is a source that subscribes observers. Rx.NET (System.Reactive) is the canonical implementation, and exceptions are delivered through OnError as part of the data flow. For one-off async sequences, IAsyncEnumerable consumed with await foreach is usually a more natural fit. The IAsyncDisposable interface is the async counterpart of IDisposable and is used with await using so that async cleanup (flushing buffers, closing network connections) runs to completion.

Modern C# Language Features

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 and arrays for parsing.

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 with HasValue and Value members. The null-coalescing operator (??) returns the left operand if it is not null, otherwise the right; the null-conditional operator (?.) short-circuits the entire chain to null if any part is null, so person?.Address?.City is safe; and the null-coalescing assignment operator (??=) assigns only when the left side is null. Nullable reference types (C# 8) add compile-time null-safety analysis, with string meaning non-nullable and string? meaning nullable; the compiler warns on potential null dereferences. The null-forgiving operator (!) suppresses those warnings without runtime checks, so use it sparingly at trust boundaries. The throw expression (C# 7) lets throw appear inside expressions, including ternary operators, switch expressions, and null-coalescing, as in name ?? throw new ArgumentNullException(nameof(name)). The nameof operator returns the name of a symbol as a string with compile-time checking, and [CallerMemberName] auto-fills a parameter with the calling member's name, both used heavily in property change notifications. Verbatim strings (prefix @) disable escape processing and allow multi-line text, with "" for embedded quotes, and they can be combined 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. Preprocessor directives like #if, #nullable, #pragma, #region, and #error are processed by the compiler rather than a separate preprocessor.

LINQ and Data Querying

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, and a key characteristic is deferred execution: a query is not executed when defined, but only when its results are enumerated, such as inside a foreach loop, a ToList() call, or a Count() call. The yield return statement enables iterator methods that produce values one at a time, lazily, with the compiler generating a state machine and yield break terminating the iterator early. IEnumerable is the base interface for all generic collections that can be enumerated and is what enables both foreach and LINQ.

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> where each group exposes its Key and the contained elements. Aggregate applies an accumulator function over a sequence, similar to reduce or fold in functional languages, and is useful for custom accumulation beyond what Sum and Count provide. Cast attempts to cast every element to T and throws on the first mismatch, while OfType keeps only elements of the target type, skipping the rest, which is the right tool when filtering a non-generic legacy collection.

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, while orders.Select(o => o.Items) returns IEnumerable>, which is rarely what you want. SelectMany is essential for one-to-many projections and chaining inner queries.

A critical distinction is between IEnumerable and IQueryable. IEnumerable represents an in-memory sequence where filtering happens client-side, while IQueryable builds an expression tree that can be translated to another query language (typically SQL) and executed server-side, often with significant performance benefits when working with databases; the same method syntax is used in both cases, but the execution location is fundamentally different. For materializing results, ToList returns a mutable List, while ToArray returns a fixed-size T[]; both force enumeration and are O(n). For single-element queries, First throws on an empty sequence, FirstOrDefault returns default(T), Single throws on zero or more than one element, and SingleOrDefault returns default(T) only if the sequence is empty; use Single when you expect exactly one match.

Asynchronous and Parallel Programming

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, it suspends the method, returns control to the caller, and resumes the method on a thread-pool thread when the task completes. Task represents an asynchronous operation with no return value, while Task represents an operation that returns a value of type T. The Task Parallel Library (TPL) is the foundation: it abstracts work onto the thread pool using work-stealing queues, and it underpins async/await, Parallel.For, and PLINQ.

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 lets you manually create a Task from non-async code, useful for wrapping callback-based or event-based APIs into awaitable form. Async streams (C# 8) expose time-varying sequences with IAsyncEnumerable and await foreach, often paired with WithCancellation to honor cancellation tokens.

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.

Memory, Performance, and Strings

Modern C# provides several types for working with memory directly while avoiding garbage collector pressure. Span is a ref struct representing a contiguous region of memory, such as an array, a string slice, or stack-allocated memory, and it enables zero-allocation slicing and parsing. Because it is a ref struct, Span cannot be boxed, stored on the heap, used as a generic type argument, or passed to an async method. Memory is the heap-friendly counterpart: a regular struct that can live in fields, be passed across await boundaries, and convert to a Span via its Span property. stackalloc allocates a buffer on the stack and, in C# 7.2+, implicitly converts to Span; the buffer is automatically freed when the method returns, making it ideal for small, short-lived buffers. ArrayPool.Shared rents pre-allocated arrays to avoid repeated allocations in I/O paths, and a Span can wrap the rented array for zero-copy slicing. JIT (Just-In-Time) compilation converts IL to native code at runtime with knowledge of the actual machine, while AOT (Ahead-Of-Time) compilation produces a single-file native binary at build time, ideal for serverless workloads, CLI tools, and platforms like iOS where JIT is not allowed.

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 without intermediate buffers.

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 are preferred over ArrayList. File.ReadAllText returns the entire file as a single string, File.ReadAllLines returns a string[] of lines, and File.ReadLines returns a lazy IEnumerable for streaming large files.

Collections, Concurrency, Resources, and Diagnostics

The choice of collection profoundly affects performance. Dictionary provides O(1) average-case lookup, insertion, and deletion via a hash table, with TryGetValue preferred over ContainsKey followed by the indexer to avoid double-hashing. SortedDictionary uses a red-black tree for O(log n) operations across the board, while SortedList uses a sorted array with O(log n) lookup but O(n) insert and remove due to shifts. HashSet holds unique items with O(1) Contains, Add, and Remove, while List is indexable and ordered but has O(n) Contains. Array.Length is a property on arrays with O(1) cost, while the LINQ Count() extension walks the sequence unless it is an ICollection. Array.Copy may use a slower path when source and destination overlap, while Array.ConstrainedCopy requires non-overlapping regions with identical element types and is suitable for constrained execution regions. For read-heavy scenarios, frozen collections (System.Collections.Frozen, .NET 8+) and FrozenSet are slow to build but very fast to read, with FrozenSet optimized for lookups over a build-once set; immutable collections in System.Collections.Immutable provide truly immutable trees with structural sharing, while ReadOnlyCollection is a thin wrapper over a mutable collection that can still change underneath.

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 flows values with the async control flow and is appropriate for ambient context like HttpContext, unlike ThreadLocal which is per OS thread and is broken by async continuations.

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 lets you hold an object without preventing collection, which is useful in caches and listener patterns, though you must always check TryGetTarget before use.

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 is the standard logging abstraction, supporting structured logging, levels (Trace through Critical), and providers (Console, Debug, Serilog, Application Insights); inject it via DI rather than calling Console.WriteLine in production code. The built-in DI container in ASP.NET Core supports three lifetimes: Singleton (one per app), Scoped (one per request), and Transient (new each time). IOptions is a singleton that reads configuration once at startup, while IOptionsSnapshot is scoped and re-reads per request, and IOptionsMonitor is a singleton that watches for changes and raises a callback. BackgroundService is the base class for IHostedService that simplifies long-running background work by handling start and stop, with ExecuteAsync receiving a CancellationToken. Lazy defers expensive initialization until first access with thread safety, and LazyInitializer.EnsureInitialized is a memory-efficient alternative for single-field lazy singletons that avoids the wrapper object. Stopwatch provides a high-resolution monotonic counter suitable for benchmarking, while DateTime.UtcNow has 10-15 ms resolution and can jump on system clock changes. DateTimeOffset stores a date plus an explicit offset from UTC, unambiguously identifying a moment, while DateTime with its Kind enum can be ambiguous and is less safe for server-side time-zone math. Environment.NewLine gives the platform-appropriate line separator, but for cross-platform files you should pick one explicitly and document it. The dynamic keyword bypasses compile-time checks via the DLR and is appropriate for COM interop, dynamic JSON, and interop with dynamic languages, while object is statically typed; is null and == null differ when == is overloaded, so prefer is null for reference-type null checks.

Frequently asked questions

What is LINQ in C#?

LINQ (Language Integrated Query) is a set of features that allows you to write query expressions directly in C# to filter, sort, and transform data from collections, databases, and XML.
var results = list.Where(x => x > 5).OrderBy(x => x);

How do you create a generic method in C#?

Define the type parameter after the method name:
public T Max<T>(T a, T b) where T : IComparable<T> {
  return a.CompareTo(b) >= 0 ? a : b;
}

The compiler infers T at the call site: Max(3, 7);

What are tuples in C#?

Tuples are lightweight data structures for grouping multiple values:
var point = (X: 3, Y: 4);
Console.WriteLine(point.X);

Method return example:
(string Name, int Age) GetPerson() => ("Alice", 30);
Tuples use ValueTuple (value type) under the hood.

What is string interpolation in C#?

String interpolation embeds expressions directly into a string using the $ prefix and { } placeholders:
var s = $"Hello, {name}! You are {age} years old.";
Introduced in C# 6. The compiler converts this into a string.Format or DefaultInterpolatedStringHandler call (C# 10+). Use {{ and }} to escape literal braces.

What is the readonly keyword for fields?

A readonly field can be assigned only at declaration or in a constructor:
public readonly int MaxItems;
After construction, the value cannot change. This is different from const, which is baked in at compile time. readonly works with both instance and static fields.

What is the difference between HashSet&lt;T&gt; and List&lt;T&gt;?

  • 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.
Use HashSet for membership tests and deduplication; use List when order and indexing matter.

What is a memory leak in managed C# code?

In .NET, memory leaks are not about forgetting to free memory (the GC does that) — they are about references that the GC cannot see as unreachable:
  • Static fields holding object graphs
  • Event handlers not unsubscribed
  • Unbounded caches
  • Captured closures in long-lived objects
  • IDisposable not disposed (especially with unmanaged resources)
Diagnose with dotMemory, PerfView, or GC.GetTotalMemory(true).

What is the difference between internal and protected internal?

  • 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.

What is the difference between new() constraint and class constraint?

  • 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.
You can combine them: where T : class, new().

What is the difference between File.ReadAllText and File.ReadAllLines?

  • File.ReadAllText(path) returns the entire file as a single string.
  • File.ReadAllLines(path) returns a string[], one element per line.
For large files, use streaming: 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 flashcards

LearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.