Top C# Interview Questions & Answers

C# interviews cover the .NET type system, memory management, and async patterns that show up across enterprise backend and desktop development roles.

Take Practice Interview Now
Improve your chances of getting selected — rehearse these answers out loud with an AI interviewer and get a scored report in minutes.
Start Free →

1. What's the difference between value types and reference types in C#?

Value types—things like `int`, `double`, `bool`, and `struct`—store their actual data directly on the stack (or inline within whatever object contains them), and when you assign one to another variable you get a full copy of the data. Reference types—classes, arrays, delegates, strings—store a reference on the stack that points to the actual object data on the heap, so assigning one variable to another just copies the reference, meaning both variables point to the same underlying object. This means mutating a reference type through one variable is visible through the other, while mutating a copied value type isn't. `string` is a bit of a special case: it's technically a reference type but behaves immutably, so it often feels value-like in practice. Structs are generally used for small, immutable, short-lived data because copying them is cheap, while classes are used when you need identity, inheritance, or larger object graphs.

2. What's the difference between `==` and `.Equals()`?

`==` is an operator that, by default, does reference equality for reference types and value equality for value types, but it can be overloaded—`string` is a good example where `==` is overloaded to compare content rather than reference. `.Equals()` is a virtual method defined on `object` that's meant to be overridden to define value-based equality semantics for a type, and it's what collections like `Dictionary` and `HashSet` rely on internally alongside `GetHashCode`. For custom reference types, if you override `Equals` you should also override `GetHashCode` and typically the `==`/`!=` operators to keep behavior consistent. A common gotcha is comparing two objects through a base type or interface reference where `==` isn't overridden—it silently falls back to reference equality even if `Equals` was overridden on the concrete type. `object.ReferenceEquals` is the way to force a true reference comparison regardless of any overrides.

3. What is boxing and unboxing, and why does it matter for performance?

Boxing is the process of wrapping a value type in an object so it can be treated as a reference type, which happens implicitly whenever you assign a value type to an `object` or an interface it implements, such as putting an `int` into a non-generic collection like `ArrayList`. Unboxing is the reverse—extracting the value type back out of the object wrapper—and it requires an explicit cast plus a runtime type check. Both operations involve a heap allocation on boxing and a copy on unboxing, so doing this repeatedly in a hot loop creates significant GC pressure and slows things down. Generic collections like `List<int>` avoid this entirely because they're specialized for the value type at compile time and never need to box it. This is one of the main reasons generics were introduced in C# 2.0—to eliminate the boxing overhead that plagued the non-generic collections in `System.Collections`.

4. How does garbage collection work in .NET, and when do you need IDisposable and using?

The .NET garbage collector automatically manages memory for managed objects on the heap using a generational, mark-and-sweep approach—short-lived objects live in generation 0 and get collected frequently and cheaply, while objects that survive get promoted to generation 1 and 2, which are collected less often. This handles managed memory just fine, but it doesn't know how to clean up unmanaged resources like file handles, database connections, or sockets wrapped by classes such as `FileStream` or `SqlConnection`. That's what `IDisposable` is for: it defines a `Dispose()` method where a class releases those unmanaged resources deterministically instead of waiting for the GC to eventually finalize the object. The `using` statement (or `using` declaration in C# 8+) is syntactic sugar that guarantees `Dispose()` gets called even if an exception is thrown, by wrapping the code in a `try/finally` block under the hood. As a rule of thumb, if a class holds onto anything unmanaged or expensive, it should implement `IDisposable`, and callers should always consume it with `using`.

5. How do async and await work, and what is a Task?

`Task` and `Task<T>` represent an asynchronous operation that may not have completed yet—`Task` for one with no return value, `Task<T>` for one that eventually produces a result of type `T`. The `async` keyword marks a method as containing asynchronous code, letting you use `await` inside it, and `await` suspends execution of that method at that point, returning control to the caller until the awaited task completes, without blocking the calling thread. When the task finishes, execution resumes on a captured context (like the UI thread in a desktop app) or on a thread pool thread, depending on `ConfigureAwait`. This is fundamentally different from `Task.Run`, which offloads CPU-bound work to a thread pool thread—`async`/`await` on its own is about not blocking a thread while waiting on I/O, like a database call or HTTP request, rather than parallelism. A common mistake is blocking on async code with `.Result` or `.Wait()`, which can cause deadlocks in contexts with a synchronization context, like older ASP.NET or WPF apps.

6. What are delegates and events, and how do they relate?

A delegate is a type-safe function pointer—it defines a signature, and you can assign it any method (or lambda) that matches that signature, then invoke it like a method call. Delegates can be multicast, meaning a single delegate instance can hold references to multiple methods, all of which get invoked in order when you call it. An event is built on top of a delegate (commonly `EventHandler` or `EventHandler<T>`) but restricts how it can be used from outside the declaring class: external code can only subscribe with `+=` or unsubscribe with `-=`, it can't invoke the event directly or overwrite the whole invocation list, which the raw delegate would otherwise allow. This encapsulation is the whole point of events—they implement the publisher/subscriber pattern safely, so a `Button` class, for example, can expose a `Click` event without letting arbitrary code fire fake clicks or clear out other subscribers' handlers. Under the hood, `event` is really just a delegate field with a restricted public interface generated by the compiler.

7. What is LINQ and how does deferred execution work?

LINQ (Language Integrated Query) lets you write query-like operations—filtering, projecting, grouping, joining—directly in C# against any `IEnumerable<T>` or `IQueryable<T>` source, using either method syntax like `.Where()` and `.Select()` or query syntax like `from x in y select x`. Most LINQ operators use deferred execution, meaning the query isn't actually run when you write it; it's only evaluated when you enumerate the result, for example with a `foreach` loop or a call to `.ToList()`. This matters because the underlying data can change between when you define the query and when you execute it, and each enumeration re-runs the query unless you materialize it with something like `.ToList()` or `.ToArray()`. Operators like `Count()`, `First()`, `Sum()`, and `ToList()` are exceptions—they force immediate execution because they need to actually iterate the source to produce their result. Understanding this distinction helps avoid subtle bugs and performance issues, like accidentally querying a database multiple times because you kept re-enumerating a query instead of caching the materialized results.

8. When would you use an interface versus an abstract class?

An interface defines a contract—a set of members that implementing types must provide—without any implementation of its own (though default interface methods in C# 8+ allow some implementation now), and a class can implement any number of interfaces. An abstract class can provide shared implementation, fields, and constructors alongside abstract members that derived classes must implement, but a class can only inherit from one abstract class because C# doesn't support multiple inheritance of classes. You'd reach for an interface when you want to define a capability that unrelated types can share, like `IComparable` or `IDisposable`, regardless of where they sit in a class hierarchy. You'd reach for an abstract class when you have a family of closely related types that share common state or behavior and you want to avoid duplicating that implementation across each derived class. In modern C#, the line has blurred a little with default interface methods, but the general guidance still holds: interfaces model 'can do,' abstract classes model 'is a' with shared code.

9. What's the difference between a property and a field?

A field is a variable declared directly inside a class or struct that stores data directly, while a property is a member that exposes data through `get` and `set` accessors, which are really just methods under the hood even when you use the shorthand auto-property syntax like `public string Name { get; set; }`. Because properties compile down to accessor methods, they let you add logic—validation, lazy initialization, raising a `PropertyChanged` event—without changing the public API, whereas changing a public field to a property later is a breaking change for compiled consumers. Properties also let you expose different access levels for reading and writing, such as a public getter with a private setter, which fields can't do as cleanly. The general convention in C# is to keep fields private and expose data to the outside world through properties, reserving public fields for rare cases like `readonly` struct fields or constants. Auto-properties give you most of this safety for free without the ceremony of writing a backing field explicitly.

10. What are generics and why are they useful?

Generics let you define classes, interfaces, and methods with a placeholder type parameter, like `List<T>` or `Dictionary<TKey, TValue>`, that gets specified when the type is actually used, so `List<int>` and `List<string>` share the same implementation but work with different concrete types. This gives you compile-time type safety—you can't accidentally add a `string` to a `List<int>`—without the boxing and casting overhead that non-generic collections like `ArrayList` required for value types. Generics also let you write reusable algorithms once, like a generic `Swap<T>` method or a repository pattern like `IRepository<TEntity>`, instead of duplicating code for every type. Constraints, using the `where` clause, let you restrict what a type parameter can be—for example `where T : class`, `where T : struct`, `where T : new()`, or `where T : IComparable<T>`—so you can call specific members on the generic type inside the method body. Under the hood, generics are handled differently for value types and reference types: the JIT generates specialized native code per value type instantiation, while reference type instantiations share a single compiled implementation.

11. What are nullable reference types in C# 8+?

Nullable reference types are an opt-in compiler feature (enabled via `<Nullable>enable</Nullable>` in the project file or `#nullable enable` per file) that adds static analysis on top of the existing type system to help catch null reference exceptions at compile time rather than runtime. Once enabled, a plain reference type like `string` is treated as non-nullable by default, and you have to explicitly mark it as `string?` if you intend for it to possibly be null; the compiler then warns you if you dereference a `string?` without checking it first, or if you assign null to a plain `string`. It's important to understand this is purely a compile-time, opt-in warning system—it doesn't change runtime behavior at all, and you can still get a `NullReferenceException` if you ignore the warnings or misuse the null-forgiving operator `!`. The feature works well with flow analysis, so the compiler can often tell that a variable has been null-checked in an `if` block and won't warn about a dereference inside it. It's especially valuable in large codebases and APIs, since method signatures like `string? FindUser(int id)` now communicate nullability intent directly instead of relying on documentation or convention.

12. What are extension methods and how do they work?

Extension methods let you add new methods to an existing type without modifying its source code or creating a derived type, by writing a `static` method in a `static` class where the first parameter is prefixed with `this`, like `public static bool IsNullOrEmpty(this string s)`. The compiler then lets you call that method as if it were an instance method on the target type, so `myString.IsNullOrEmpty()` actually compiles down to a regular static call, `MyExtensions.IsNullOrEmpty(myString)`. This is how LINQ itself is implemented—methods like `.Where()`, `.Select()`, and `.OrderBy()` are extension methods defined on `IEnumerable<T>` in the `System.Linq` namespace, which is why you need a `using System.Linq;` to see them. Extension methods can't access private members of the type they extend since they're not really part of that type, and if an instance method with the same signature already exists on the type, the instance method always wins over the extension method. They're most useful for adding utility behavior to sealed types, interfaces, or types you don't own, like extending `IEnumerable<T>` or `DateTime`.

13. What's the difference between `const` and `readonly`?

A `const` field is a compile-time constant—its value must be known at compile time, it's implicitly static, and the compiler actually substitutes the literal value directly into the calling code wherever it's used, which means if you change a `const` value in a referenced assembly, consumers need to be recompiled to pick up the new value. A `readonly` field, by contrast, can be assigned either at declaration or inside a constructor, its value can be computed at runtime, and it's resolved at runtime by the consuming code rather than baked in at compile time. Because of that difference, `readonly` is generally the safer choice for values that might change between versions of a library, or that depend on runtime information passed into a constructor. `const` is best reserved for values that truly never change, like mathematical constants or fixed configuration keys, since it's slightly more efficient but far less flexible. Also worth noting: `readonly` can be applied to instance fields, giving each object its own immutable-after-construction value, while `const` is always implicitly shared across all instances.

14. What are some best practices for exception handling in C#?

Catch exceptions only where you can actually do something meaningful with them—log them, retry, show a user-friendly message, or clean up a resource—rather than catching broadly just to swallow the error, since an empty `catch` block hides bugs and makes debugging painful. Prefer catching specific exception types like `SqlException` or `FileNotFoundException` over a bare `catch (Exception)`, so you don't accidentally mask unrelated failures like a `NullReferenceException` that indicates an actual bug. Use `finally` blocks or, better, `using` statements to guarantee cleanup of resources regardless of whether an exception occurs. When you do need to rethrow, use a bare `throw;` inside the catch block rather than `throw ex;`, because `throw ex;` resets the stack trace and hides where the exception actually originated. Avoid using exceptions for normal control flow—they're relatively expensive and meant for exceptional, unexpected conditions—and consider patterns like `TryParse` or a result/error-object pattern for cases where failure is an expected, common outcome, like validating user input.

15. What's the difference between records and classes in C#?

A `record`, introduced in C# 9, is a reference type (or `record struct` for value semantics in C# 10+) designed primarily for representing immutable data, and the compiler automatically generates value-based equality, a `GetHashCode` override, and a readable `ToString()` for you, whereas a regular `class` gets reference equality and a default `ToString()` unless you write those overrides yourself. Records also support 'with expressions,' letting you create a modified copy of an immutable record concisely, like `var updated = person with { Age = 31 };`, which produces a new instance rather than mutating the original. By default, record properties declared with the positional syntax, like `record Person(string Name, int Age)`, are `init`-only, reinforcing the immutable-by-default design, though you can still add regular mutable properties if you need to. Under the hood a `record` really is just a class (or struct) with a bunch of compiler-generated boilerplate for equality and cloning, so you're not giving up inheritance or interfaces—records can still inherit from other records and implement interfaces. You'd reach for a record when modeling data-centric types like DTOs or value objects where equality should be based on content, and a regular class when you're modeling behavior-centric types with mutable state and identity.

16. What's the difference between IEnumerable and IQueryable?

`IEnumerable<T>` represents a sequence that can be iterated in memory—when you apply LINQ operators to it, they're compiled into delegates and executed directly against objects already loaded into memory, like a `List<T>`. `IQueryable<T>` represents a query that can be translated into another form, typically SQL, and executed against an external data source like a database—when you apply LINQ operators to an `IQueryable`, they're built up as an expression tree rather than executed immediately, and the underlying provider, like Entity Framework, translates that expression tree into a query at the point of enumeration. The practical consequence is that filtering with `.Where()` on an `IQueryable` from Entity Framework happens in the database, only pulling back the rows you need, whereas if you accidentally call `.ToList()` first and then `.Where()`, you've pulled the entire table into memory and are now filtering in C#. This is a very common performance bug: switching a query from `IQueryable` to `IEnumerable` too early, often unintentionally, by calling a method that isn't supported for translation, which silently forces the whole query to execute and continue in memory. As a rule, keep queries as `IQueryable` for as long as possible when working against a database, and only materialize with `.ToList()` once all your filtering and projection is defined.

17. What does `yield return` do?

`yield return` is used inside a method that returns `IEnumerable<T>` or `IEnumerator<T>` to create an iterator without you having to manually implement the `IEnumerator` interface and manage state yourself. When the compiler sees `yield return`, it rewrites the method into a state machine behind the scenes that remembers exactly where execution left off, so each time the consumer asks for the next item, via `foreach` or a direct call to `MoveNext()`, the method resumes right after the last `yield return` rather than starting over. This enables lazy, deferred evaluation—the code inside the method doesn't run at all until you start enumerating, and it only produces one item at a time, which is memory-efficient for large or even infinite sequences, like generating Fibonacci numbers on demand. It's commonly used for streaming data from a file line by line, or for writing custom filtering and transformation logic that you want to compose lazily like the built-in LINQ operators. One gotcha is that exceptions thrown inside an iterator method aren't raised until the caller actually enumerates far enough to hit that code, not when the method is first called.

18. What's the difference between `out`, `ref`, and `in` parameters?

`ref` passes an argument by reference rather than by value, meaning changes the method makes to the parameter are reflected back in the caller's variable, and the variable must already be initialized before it's passed in. `out` also passes by reference, but it's meant for parameters the method is expected to assign before returning—the caller doesn't need to initialize the variable beforehand, and the compiler enforces that the method assigns it on every code path before it returns, which is exactly the pattern used by `int.TryParse(string s, out int result)`. `in` (introduced in C# 7.2) passes by reference too, but read-only—the method receives a reference for efficiency, avoiding a copy for large structs, but it can't modify the value, and the compiler flags an error if you try. All three differ from normal parameters, which are passed by value, meaning the method gets its own independent copy and any changes inside the method don't affect the caller's original variable, unless it's a reference type, in which case the reference itself is copied but still points to the same object. A practical rule: use `out` for methods that need to return multiple values or signal success plus a result, use `ref` when a method needs to both read and mutate the caller's variable, and use `in` mainly for performance-sensitive code passing large read-only structs.

Take Practice Interview Now
Improve your chances of getting selected — rehearse these answers out loud with an AI interviewer and get a scored report in minutes.
Start Free →
Related topics
JavaPythonJavaScriptTypeScript