C# Interview Questions
Language Fundamentals
Q1: What is the difference between readonly and const?
Both create immutable values, but they differ in when the value is set and what types they accept:
| Feature | const | readonly |
|---|---|---|
| When assigned | Compile time | Runtime (constructor or declaration) |
| Implicitly static | Yes | No (can be instance or static) |
| Allowed types | Primitives, strings, enums, null | Any type |
Can use new | No | Yes |
| Changing value | Binary-breaking change (inlined) | Safe across assemblies |
public class Settings
{
// const — value baked in at compile time, implicitly static
public const int MaxRetries = 3;
public const string AppName = "MyApp";
// readonly — value set at runtime
public readonly DateTime CreatedAt;
public readonly string ConnectionString;
public Settings(string connectionString)
{
ConnectionString = connectionString;
CreatedAt = DateTime.UtcNow;
}
}
readonly for public valuesconst values are inlined at compile time — every referencing assembly gets the literal value baked in. If you change a const, all referencing assemblies must be recompiled. Use public static readonly for values that might change between versions.
Q2: What is the sealed keyword used for?
sealed prevents further inheritance or overriding:
// Sealed class — cannot be inherited
public sealed class ConnectionString
{
public string Value { get; }
public ConnectionString(string value) => Value = value;
}
// public class CustomConnStr : ConnectionString { } // Compile error
// Sealed method — cannot be overridden further down the chain
public class BaseRenderer
{
protected virtual void RenderHeader() { }
}
public class HtmlRenderer : BaseRenderer
{
protected sealed override void RenderHeader()
{
// This override is final — no further override allowed
}
}
Mark classes sealed unless you intentionally design for inheritance. The JIT compiler can optimize virtual calls on sealed types (devirtualization). Common sealed types: string, Exception.
Q3: Name all the access modifiers for types
| Modifier | Same Class | Derived Class | Same Assembly | External Assembly |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
private | Yes | No | No | No |
protected | Yes | Yes | No | No |
internal | Yes | No | Yes | No |
protected internal | Yes | Yes | Yes | No |
private protected | Yes | Yes (same assembly) | No | No |
Top-level types (not nested) can only be public or internal. The default is internal.
Q4: What is the difference between an interface and an abstract class?
| Feature | Abstract Class | Interface |
|---|---|---|
| Constructors | Yes | No |
| Fields / state | Yes | No (static fields only, C# 8+) |
| Default implementation | Yes | Yes (C# 8+, via DIM) |
| Multiple inheritance | No (single class inheritance) | Yes (multiple interfaces) |
| Access modifiers | Any | public (implicitly) |
| When to use | "Is-a" with shared logic | "Can-do" capability contract |
// Abstract class — shared state and logic
public abstract class Vehicle
{
public string Model { get; set; } // state
public void Start() => Console.WriteLine("Engine started"); // concrete method
public abstract double CalculateFuelEfficiency(); // must override
}
// Interface — pure contract
public interface IWriter
{
void WriteFile();
// C# 8+ default implementation
void Log() => Console.WriteLine("Writing...");
}
Since C# 8, interfaces can have default implementations, but they still cannot hold instance state (no instance fields).
Q5: When is a static constructor called?
A static constructor runs once per AppDomain, lazily — before the first instance is created or any static member is accessed. It is never called directly.
public class Configuration
{
public static int InstanceCount { get; private set; }
static Configuration()
{
InstanceCount = 0;
Console.WriteLine("Static ctor ran");
}
public Configuration() => InstanceCount++;
}
var c1 = new Configuration(); // "Static ctor ran" printed here
var c2 = new Configuration(); // No output — static ctor already ran
Rules:
- No access modifiers, no parameters
- Runs only once per AppDomain
- Called lazily (before first use)
- Thread-safe — the CLR guarantees only one thread executes it
Q6: How to create an extension method?
Extension methods add new methods to existing types without modifying them. They must be defined in a static class and the first parameter uses the this keyword:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string str)
=> string.IsNullOrEmpty(str);
public static string Truncate(this string str, int maxLength)
=> str.Length <= maxLength ? str : str[..maxLength];
}
// Usage — appears as instance method
string name = "CommitToMemory";
var shortName = name.Truncate(6); // "Commit"
bool empty = "".IsNullOrEmpty(); // true
Rules:
- Must be in a
staticclass at namespace level - First parameter must have
thismodifier - Cannot access private members of the extended type
- Instance methods always win over extension methods with the same signature
Q7: Does C# support multiple class inheritance?
No. C# supports only single class inheritance — a class can have exactly one direct base class. However, a class can implement multiple interfaces:
public class Base { }
public interface IReader { void Read(); }
public interface IWriter { void Write(); }
// Single class + multiple interfaces
public class Document : Base, IReader, IWriter
{
public void Read() { }
public void Write() { }
}
The reason: multiple class inheritance leads to the diamond problem — ambiguity when two base classes define the same member. Interfaces avoid this because they carry no implementation state (prior to C# 8 default methods, which are resolved differently).
Q8: Explain boxing and unboxing
Boxing converts a value type to a reference type (stored on the heap). Unboxing extracts the value type from the object.
int number = 42;
// Boxing — value type → reference type (heap allocation)
object boxed = number;
// Unboxing — reference type → value type (explicit cast)
int unboxed = (int)boxed;
// Hidden boxing in everyday code
ArrayList list = new(); // stores object — boxes value types
list.Add(42); // boxing!
int val = (int)list[0]; // unboxing
// Generic collections avoid boxing entirely
List<int> genericList = new();
genericList.Add(42); // no boxing — stored as int
Boxing allocates on the heap and causes GC pressure. Avoid it by using generic collections (List<T> over ArrayList) and generic interfaces (IEnumerable<T> over IEnumerable).
Q9: What is the heap and the stack?
| Aspect | Stack | Heap |
|---|---|---|
| Stores | Value types, method frames, references | Objects (reference type instances) |
| Speed | Very fast (CPU stack pointer) | Slower (indirected access) |
| Lifetime | Automatic — freed when method returns | Managed by GC |
| Size | Small (~1 MB per thread) | Large (GBs available) |
| Thread safety | Per-thread | Shared across threads |
void Example()
{
int x = 10; // x lives on the stack
string s = "hello"; // reference 's' on stack, "hello" object on heap
var p = new Person(); // reference 'p' on stack, Person object on heap
} // stack frame popped — x, s, p references gone; Person object lives until GC collects
Value types (int, double, bool, struct, enum) are typically stored on the stack (unless captured by a closure, part of a class, or boxed). Reference types (class, string, array, delegate) are always stored on the heap — the variable holds a reference (pointer) to the heap object.
Q10: What is the difference between string and StringBuilder?
string is immutable — every modification creates a new string object. StringBuilder is mutable — it modifies a single buffer, making it efficient for repeated concatenations.
// string — each + creates a new allocation
string result = "";
for (int i = 0; i < 1000; i++)
result += i.ToString(); // 1000 new string allocations!
// StringBuilder — modifies one buffer
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i.ToString()); // no intermediate allocations
string final = sb.ToString();
| Feature | string | StringBuilder |
|---|---|---|
| Mutability | Immutable | Mutable |
| Concatenation | Creates new object each time | Appends to same buffer |
| Performance | Slow for many operations | Fast for many operations |
| Thread safety | Naturally thread-safe | Not thread-safe |
| Use case | Few operations, comparisons | Loops, many concatenations |
StringBuilder when you have more than ~3-4 concatenations in a loop. For simple one-off concatenations, string is fine and the compiler optimizes + into string.Concat.Q11: What is the difference between is and as operators?
Both deal with runtime type checking, but they serve different purposes:
| Feature | is | as |
|---|---|---|
| Returns | bool (type check result) | Reference (casted object or null) |
| Throws | Never | Never |
| Value types | Supported (C# 7+ pattern matching) | Reference/boxing/unboxing only |
| Pattern matching | Yes (is int x, is string s) | No |
object obj = "Hello";
// is — checks type, returns bool
if (obj is string) { /* true */ }
// is with pattern matching (C# 7+) — check AND extract
if (obj is string str)
Console.WriteLine(str.Length); // 5
// as — attempts cast, returns null on failure
string? s = obj as string; // "Hello"
int? n = obj as int?; // null — int is a value type, must use nullable
// typeof — gets the Type object at compile time
Type t = typeof(string);
is with pattern matchingUse if (obj is string s) over var s = obj as string; if (s != null) — it's more concise and works with value types.
Q12: What is the difference between early binding and late binding?
Early binding resolves method calls at compile time. The compiler knows the exact type and validates method existence. Late binding resolves at runtime — the actual method is determined when the code executes.
// Early binding — compiler knows the type and method
string name = "Alice";
int len = name.Length; // resolved at compile time
// Late binding via dynamic — resolved at runtime
dynamic value = "Hello";
int length = value.Length; // resolved at runtime, no compile-time check
// Late binding via reflection — resolved at runtime
var type = obj.GetType();
var method = type.GetMethod("DoWork");
method?.Invoke(obj, null);
// Late binding via virtual methods — runtime dispatch
Animal animal = new Dog();
animal.Speak(); // Dog.Speak() resolved at runtime (but type-checked at compile time)
| Feature | Early Binding | Late Binding |
|---|---|---|
| Resolution | Compile time | Runtime |
| Performance | Faster (direct call) | Slower (lookup overhead) |
| Type safety | Caught at compile time | Runtime errors possible |
| IDE support | Full IntelliSense | None |
| Examples | Normal method calls, overloaded methods | dynamic, reflection, virtual dispatch |
Use early binding whenever possible for type safety, performance, and IDE support. Only use late binding (dynamic, reflection) when the type is genuinely unknown at compile time (e.g., COM interop, plugin systems).
Collections & LINQ
Q13: How to create a date with a specific timezone?
Use TimeZoneInfo to convert a DateTime to a specific timezone, or use DateTimeOffset for unambiguous point-in-time values:
// Using TimeZoneInfo
var tz = TimeZoneInfo.FindSystemTimeZoneById("SE Asia Standard Time"); // Windows ID
var utcNow = DateTime.UtcNow;
var localDate = TimeZoneInfo.ConvertTimeFromUtc(utcNow, tz);
// Using DateTimeOffset — stores date + time + offset
var dto = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.FromHours(7)); // UTC+7
Console.WriteLine(dto); // 6/15/2025 10:30:00 AM +07:00
Console.WriteLine(dto.UtcDateTime); // 6/15/2025 3:30:00 AM
// Cross-platform timezone IDs (IANA) — use TimeZoneInfo on .NET 6+
var ianaTz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Ho_Chi_Minh");
DateTimeOffset over DateTimeDateTime alone is ambiguous — you don't know if it's local, UTC, or unspecified. DateTimeOffset always carries the offset, making it safe for serialization, APIs, and cross-timezone scenarios.
Q14: How to change the current culture?
Use CultureInfo.CurrentCulture for formatting/parsing and CultureInfo.CurrentUICulture for resource lookup:
using System.Globalization;
// Change culture for the current thread
CultureInfo.CurrentCulture = new CultureInfo("vi-VN");
CultureInfo.CurrentUICulture = new CultureInfo("vi-VN");
Console.WriteLine(1234.56.ToString("N2")); // "1 234,56" (Vietnamese formatting)
// Culture-scoped change (restores after use)
using var _ = new CultureScope(new CultureInfo("en-US"));
Console.WriteLine(1234.56.ToString("N2")); // "1,234.56"
// Simple helper
public class CultureScope : IDisposable
{
private readonly CultureInfo _original = CultureInfo.CurrentCulture;
public CultureScope(CultureInfo culture) => CultureInfo.CurrentCulture = culture;
public void Dispose() => CultureInfo.CurrentCulture = _original;
}
Q15: What is the difference between HashSet<T> and Dictionary<TKey, TValue>?
| Feature | HashSet<T> | Dictionary<TKey, TValue> |
|---|---|---|
| Stores | Unique values only | Key-value pairs |
| Lookup | By value (O(1) average) | By key (O(1) average) |
| Duplicates | Silently ignored | Throws on duplicate key |
| Use case | Membership testing, set operations | Mapping keys to values |
// HashSet — unique items, set operations
var tags = new HashSet<string> { "csharp", "dotnet", "linq" };
tags.Add("csharp"); // ignored — already exists
tags.Add("async"); // added
bool has = tags.Contains("dotnet"); // true
// Dictionary — key → value mapping
var ages = new Dictionary<string, int>
{
["Alice"] = 30,
["Bob"] = 25
};
ages["Alice"] = 31; // update existing
ages["Charlie"] = 28; // add new
Q16: What is the purpose of the ToLookup method?
ToLookup creates a one-to-many dictionary (ILookup<TKey, TElement>) where each key can map to multiple values. Unlike GroupBy (deferred), ToLookup executes immediately:
var students = new[]
{
new { Name = "Alice", Grade = "A" },
new { Name = "Bob", Grade = "B" },
new { Name = "Charlie", Grade = "A" },
new { Name = "Diana", Grade = "B" },
};
// ToLookup — immediate execution, one-to-many
ILookup<string, string> byGrade = students.ToLookup(s => s.Grade, s => s.Name);
foreach (var group in byGrade)
{
Console.WriteLine($"Grade {group.Key}: {string.Join(", ", group)}");
}
// Grade A: Alice, Charlie
// Grade B: Bob, Diana
// Lookup by key — returns empty sequence if key not found (no KeyNotFoundException)
var gradeC = byGrade["C"]; // empty sequence, not an error
| Feature | GroupBy | ToLookup |
|---|---|---|
| Execution | Deferred | Immediate |
| Return type | IEnumerable<IGrouping> | ILookup<TKey, TElement> |
| Re-enumeration | Re-executes query | No (cached) |
| Missing key | N/A | Returns empty sequence |
Q17: Does LINQ Cast<T> method create a new object?
No. Cast<T> performs reference conversion only — it casts each element to T without creating new objects. If the cast fails at runtime, it throws InvalidCastException.
var list = new ArrayList { "a", "b", "c" };
// Cast<string> — no new objects, just reference casts
IEnumerable<string> strings = list.Cast<string>();
// This throws — can't cast int to string
var mixed = new ArrayList { "a", 1, "b" };
var result = mixed.Cast<string>().ToList(); // InvalidCastException at 1
OfType<T> to filter instead of throwOfType<T> filters out elements that can't be cast, instead of throwing:
var mixed = new ArrayList { "a", 1, "b", 2 };
var strings = mixed.OfType<string>().ToList(); // ["a", "b"] — no exception
Q18: Explain deferred execution in LINQ
Deferred execution means the query is not evaluated until you actually enumerate over it. Most LINQ operators (Where, Select, OrderBy, GroupBy) are deferred. Operators like ToList(), ToArray(), Count(), First() force immediate execution.
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Deferred — query defined but NOT executed
var query = numbers.Where(n =>
{
Console.WriteLine($"Filtering {n}");
return n > 2;
});
// Nothing printed yet — no execution
// Immediate — ToList() forces enumeration
var result = query.ToList();
// Filtering 1, Filtering 2, Filtering 3, Filtering 4, Filtering 5
// Each enumeration re-executes the query!
numbers.Add(6);
var result2 = query.ToList(); // Filters all 6 elements again
Deferred queries re-evaluate on every enumeration. If the source changes between enumerations, you get different results. Cache with ToList() or ToArray() if you need stable results.
Q19: How does ImmutableList work?
ImmutableList<T> is a collection that never modifies itself — every "mutation" returns a new instance while sharing unchanged data internally (structural sharing):
var list = ImmutableList.Create(1, 2, 3);
// Each operation returns a NEW list — original is unchanged
var list2 = list.Add(4); // list2 = [1, 2, 3, 4], list = [1, 2, 3]
var list3 = list2.Remove(2); // list3 = [1, 3, 4], list2 = [1, 2, 3, 4]
var list4 = list3.SetItem(0, 99); // list4 = [99, 3, 4]
// Builder — efficient for bulk mutations
var builder = list.ToBuilder();
builder.Add(4);
builder.Add(5);
var result = builder.ToImmutable(); // [1, 2, 3, 4, 5]
| Feature | List<T> | ImmutableList<T> |
|---|---|---|
| Mutability | Mutable | Immutable (returns new) |
| Add/Remove | O(1) amortized | O(log n) — tree structure |
| Thread safety | No | Naturally thread-safe |
| Use case | General purpose | Functional pipelines, shared state |
ImmutableArray<T> for read-heavy, small collections — it has O(1) read access (contiguous memory). Use ImmutableList<T> when you need efficient "mutation" via structural sharing.Q20: What is the difference between Array.CopyTo() and Array.Clone()?
Both copy array elements, but differ in destination and depth:
| Feature | CopyTo() | Clone() |
|---|---|---|
| Destination | Copies into an existing array | Creates a new array |
| Starting index | Can specify start index | Always starts at 0 |
| Return type | void | object (must cast) |
| Array must exist | Yes (must be pre-allocated) | No (creates new) |
int[] source = [1, 2, 3, 4, 5];
// Clone() — creates a new array, returns object (shallow copy)
int[] cloned = (int[])source.Clone();
// cloned = [1, 2, 3, 4, 5] — new array
// CopyTo() — copies into an existing array
int[] target = new int[5];
source.CopyTo(target, 0);
// target = [1, 2, 3, 4, 5]
// CopyTo with offset
int[] target2 = new int[7];
source.CopyTo(target2, 2);
// target2 = [0, 0, 1, 2, 3, 4, 5] — starts at index 2
Select(x => ...) or manual deep-copy logic when you need a true deep copy.Q21: What is the difference between Array and ArrayList?
| Feature | Array | ArrayList |
|---|---|---|
| Type safety | Strongly typed (T[]) | Stores object (no type safety) |
| Boxing | No | Yes (value types boxed to object) |
| Size | Fixed at creation | Dynamic (auto-resizes) |
| Namespace | System | System.Collections |
| Performance | Faster (no casting/boxing) | Slower (casting + boxing) |
| Modern alternative | T[] | List<T> |
// Array — type-safe, fixed size
int[] arr = new int[3];
arr[0] = 1;
// arr[0] = "text"; // Compile error — type safe
// ArrayList — stores object, dynamic size
ArrayList list = new ArrayList();
list.Add(1); // boxing: int → object
list.Add("text"); // mixed types allowed (no type safety)
int val = (int)list[0]; // unboxing: object → int
// Modern replacement — List<T>
List<int> generic = new List<int>();
generic.Add(1);
// generic.Add("text"); // Compile error — type safe
List<T> over ArrayList. ArrayList is a legacy type from before generics (C# 1.x). List<T> provides type safety, avoids boxing, and has better performance.Q22: What is the difference between SortedList and SortedDictionary?
Both maintain key-value pairs sorted by key, but with different internal structures and performance characteristics:
| Feature | SortedList<TKey, TValue> | SortedDictionary<TKey, TValue> |
|---|---|---|
| Internal structure | Sorted array (contiguous) | Red-black tree |
| Memory | Less memory (compact array) | More memory (tree nodes) |
| Lookup by key | O(log n) — binary search | O(log n) — tree traversal |
| Insert (unsorted data) | O(n) — array shift | O(log n) — tree rebalance |
| Insert (already sorted) | O(1) amortized — append | O(log n) |
| Retrieve min/max | O(1) — first/last element | O(log n) |
| Enumeration | Fast (contiguous memory) | Slower (tree traversal) |
// SortedList — compact, fast for lookups on static data
var sortedList = new SortedList<string, int>
{
["Charlie"] = 3,
["Alice"] = 1,
["Bob"] = 2
};
// Keys are always sorted: Alice, Bob, Charlie
// SortedDictionary — efficient inserts for dynamic data
var sortedDict = new SortedDictionary<string, int>
{
["Charlie"] = 3,
["Alice"] = 1,
["Bob"] = 2
};
// Same result, but faster for frequent insertions
SortedList when you populate once and query often (e.g., lookup tables). Use SortedDictionary when data is frequently inserted/removed. For most cases, a regular Dictionary with OrderBy at query time is simpler and sufficient.Modern C# Types
Q23: Explain Inheritance vs Composition
Inheritance ("is-a") creates a subtype relationship. Composition ("has-a") builds types by combining smaller, focused components.
// Inheritance — "Worker is a Person"
public class Person { public string Name { get; set; } }
public class Worker : Person { public string Company { get; set; } }
// Composition — "Car has an Engine"
public class Engine { public int HorsePower { get; set; } }
public class Car
{
private readonly Engine _engine; // Car HAS an Engine
public Car(Engine engine) => _engine = engine;
}
Prefer composition because:
- Avoids deep, fragile inheritance hierarchies
- Supports runtime behavior changes (swap components)
- Follows Single Responsibility Principle
- Enables multiple "capabilities" via composition (vs single inheritance)
Inheritance should model true "is-a" relationships. If the relationship is "has-a" or "can-do", use composition with interfaces.
Q24: Difference between class, record, and struct
| Feature | class | record | struct |
|---|---|---|---|
| Type | Reference (heap) | Reference (heap) | Value (stack) |
| Mutability | Mutable by default | Immutable by default | Mutable by default |
| Equality | Reference equality | Value-based equality | Value-based equality |
| Inheritance | Yes (single) | Yes (single) | No (can implement interfaces) |
with expression | No | Yes | Yes (C# 10+) |
| Auto-generates | Nothing | Equals, GetHashCode, ToString, with | Nothing |
// class — reference type, reference equality
public class PointClass { public int X { get; set; } public int Y { get; set; } }
// record — reference type, value-based equality, immutable-friendly
public record PointRecord(int X, int Y);
// struct — value type, stack-allocated
public struct PointStruct { public int X { get; set; } public int Y { get; set; } }
var r1 = new PointRecord(1, 2);
var r2 = new PointRecord(1, 2);
Console.WriteLine(r1 == r2); // True — value-based equality
Console.WriteLine(ReferenceEquals(r1, r2)); // False — different references
var c1 = new PointClass { X = 1, Y = 2 };
var c2 = new PointClass { X = 1, Y = 2 };
Console.WriteLine(c1 == c2); // False — reference equality
Q25: What are ref struct used for?
ref struct is a struct that is guaranteed to live only on the stack — it can never escape to the heap. This enables safe, zero-allocation operations with pointers and spans:
public ref struct SpanReader<T>
{
private readonly ReadOnlySpan<T> _span;
private int _position;
public SpanReader(ReadOnlySpan<T> span)
{
_span = span;
_position = 0;
}
public bool TryRead(out T value)
{
if (_position >= _span.Length) { value = default; return false; }
value = _span[_position++];
return true;
}
}
Restrictions — a ref struct cannot:
- Be boxed (no casting to
object,dynamic, or interface) - Be a field of a regular class
- Implement interfaces
- Be used in
asyncmethods or lambdas (unlessref-safe) - Be captured by closures
The primary use case is Span<T> and ReadOnlySpan<T> — high-performance, zero-allocation slicing over arrays, native memory, or stackalloc buffers.
Q26: Name the two forms of records
- Positional record — primary constructor defines properties:
public record Person(string Name, int Age);
// Compiler auto-generates: init-only properties, constructor, Deconstruct()
var p = new Person("Alice", 30);
var (name, age) = p; // Deconstruct
- Record with standard syntax — you define properties manually:
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
Both forms produce the same compiler-generated Equals, GetHashCode, ToString, and with support. The positional form is more concise; the standard form gives you more control over property definitions.
Q27: What is the with keyword used for?
with creates a copy of a record (or struct, C# 10+) with one or more properties changed — non-destructive mutation:
public record Person(string Name, int Age, string City);
var alice = new Person("Alice", 30, "Hanoi");
var bob = alice with { Name = "Bob", Age = 25 };
Console.WriteLine(alice); // Person { Name = Alice, Age = 30, City = Hanoi }
Console.WriteLine(bob); // Person { Name = Bob, Age = 25, City = Hanoi }
Under the hood, the compiler generates a copy constructor and then applies the with assignments. The original object is never modified.
Q28: What is the purpose of Primary Constructors?
Primary constructors (C# 12) let you declare constructor parameters directly on the class/struct declaration. Parameters are available throughout the class body:
// Before C# 12 — explicit constructor + field
public class Service
{
private readonly IRepository _repo;
private readonly ILogger _logger;
public Service(IRepository repo, ILogger logger)
{
_repo = repo;
_logger = logger;
}
}
// C# 12 — primary constructor
public class Service(IRepository repo, ILogger logger)
{
public void DoWork()
{
repo.GetAll(); // available everywhere
logger.Log("Working");
}
}
If you need to capture them (e.g., for lazy initialization or mutable operations), assign them to explicit fields or properties. Parameters that are only used during initialization are fine as-is.
Q29: Explain how Nullable Reference Types work
Nullable Reference Types (NRT), enabled in C# 8+, make reference type nullability explicit at compile time. The compiler warns when a non-nullable reference might be null:
#nullable enable
string name = "Alice"; // non-nullable — cannot be null
string? nickname = null; // nullable — can be null
// Compiler warnings
name = null; // Warning: Converting null to non-nullable type
int length = nickname.Length; // Warning: Possible null reference
// Defensive checks
if (nickname is not null)
length = nickname.Length; // OK — compiler knows it's not null
// Null-forgiving operator (!)
length = nickname!.Length; // "Trust me, it's not null" — suppresses warning
Key points:
- NRT is a compile-time feature — no runtime behavior changes
- Enabled per-project via
<Nullable>enable</Nullable>in.csproj - Method signatures communicate intent:
string Get()vsstring? Find()
Q30: Do switch expressions have any return type limitations?
No. Switch expressions can return any type, but all arms must return the same type (or a type that all arms can be implicitly converted to):
// Returns string
string category = age switch
{
< 13 => "Child",
< 20 => "Teenager",
< 65 => "Adult",
_ => "Senior"
};
// Returns double
double discount = customerType switch
{
"VIP" => 0.3,
"Regular" => 0.1,
_ => 0.0
};
// Returns object (common base type)
object result = input switch
{
int i => i,
string s => s,
bool b => b,
_ => "unknown"
};
The compiler infers the return type from all arms. If arms return incompatible types, it's a compile error.
Q31: What is yield return used for?
yield return produces a value lazily — items are generated one at a time as the caller enumerates, without creating a full collection upfront:
// Eager — builds entire list in memory
public static List<int> GetEvens(int max)
{
var result = new List<int>();
for (int i = 0; i <= max; i++)
if (i % 2 == 0) result.Add(i);
return result;
}
// Lazy — yields one at a time
public static IEnumerable<int> GetEvensLazy(int max)
{
for (int i = 0; i <= max; i++)
if (i % 2 == 0) yield return i;
}
// Usage — only computes values as needed
foreach (var n in GetEvensLazy(1_000_000).Take(5))
Console.WriteLine(n); // 0, 2, 4, 6, 8 — never computes all 1M
yield break exits the iterator early (equivalent to return from a normal method). The compiler transforms yield return methods into state machines.
Advanced C#
Q32: How many generations does the Garbage Collector have?
The .NET GC has 3 generations (0, 1, 2) plus the Large Object Heap (LOH):
| Generation | What it holds | Collection frequency | Cost |
|---|---|---|---|
| Gen 0 | Short-lived objects | Most frequent | Cheapest |
| Gen 1 | Survived Gen 0 | Moderate | Moderate |
| Gen 2 | Long-lived objects | Least frequent | Most expensive |
| LOH | Objects >= 85,000 bytes | With Gen 2 | Very expensive |
// Short-lived → Gen 0, collected quickly
void Process()
{
var temp = new byte[100]; // Gen 0
} // eligible for Gen 0 collection
// Long-lived → survives to Gen 2
static byte[] cache = new byte[100]; // promoted to Gen 2
// Large → LOH (85KB+)
var large = new byte[100_000]; // goes directly to LOH
How promotion works: Objects that survive a Gen 0 collection are promoted to Gen 1. Gen 1 survivors move to Gen 2. The GC uses a generational hypothesis — new objects die young, so collecting Gen 0 is fast and reclaims the most memory.
Q33: What is the Interlocked class used for?
Interlocked provides atomic, thread-safe operations on variables — without explicit locks. It's faster than lock for simple numeric operations:
private static int _counter = 0;
// Thread-safe increment (atomic)
Interlocked.Increment(ref _counter); // _counter++
Interlocked.Decrement(ref _counter); // _counter--
// Atomic add
Interlocked.Add(ref _counter, 5);
// Atomic exchange (set and return old value)
int old = Interlocked.Exchange(ref _counter, 0); // reset and get old value
// Compare-and-swap (CAS)
int expected = 10;
Interlocked.CompareExchange(ref _counter, 20, expected); // if _counter==10, set to 20
Interlocked for simple atomic operations (increment, exchange). For complex multi-step critical sections, use lock or SemaphoreSlim.Q34: What is the code generated by compiler for auto properties?
An auto-property like public string Name { get; set; } is expanded by the compiler into a property with a hidden backing field:
// What you write
public class Person
{
public string Name { get; set; }
public int Age { get; init; }
}
// What the compiler generates (simplified IL → C# equivalent)
public class Person
{
// Hidden backing fields — names are compiler-generated
[CompilerGenerated]
private string <Name>k__BackingField;
[CompilerGenerated]
private int <Age>k__BackingField;
public string Name
{
get => <Name>k__BackingField;
set => <Name>k__BackingField = value;
}
public int Age
{
get => <Age>k__BackingField;
init => <Age>k__BackingField = value; // init-only setter
}
}
The backing field names include angle brackets to prevent naming conflicts with user code.
Q35: How is Polymorphism implemented in C#?
Polymorphism in C# is achieved through:
- Subtype polymorphism —
virtual/override(runtime dispatch):
public class Animal
{
public virtual string Speak() => "...";
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
Animal a = new Dog();
a.Speak(); // "Woof!" — runtime dispatches to Dog's implementation
- Interface polymorphism — multiple types implement the same interface:
public interface IDrawable { void Draw(); }
public class Circle : IDrawable { public void Draw() => Console.WriteLine("Circle"); }
public class Square : IDrawable { public void Draw() => Console.WriteLine("Square"); }
void Render(IDrawable shape) => shape.Draw(); // works for any IDrawable
- Ad-hoc polymorphism — method overloading (compile-time):
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
Q36: How is Encapsulation implemented in C#?
Encapsulation is achieved by hiding internal state with access modifiers and exposing controlled access through properties and methods:
public class BankAccount
{
private decimal _balance; // hidden internal state
public decimal Balance => _balance; // read-only exposure
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Must be positive");
_balance += amount;
}
public bool Withdraw(decimal amount)
{
if (amount > _balance) return false;
_balance -= amount;
return true;
}
}
Mechanisms: private fields, public properties, private set, init setters, readonly fields, and file-scoped types.
Q37: What is the difference between ref and out parameters?
Both pass arguments by reference, but differ in initialization requirements:
| Feature | ref | out |
|---|---|---|
| Caller must initialize | Yes — before calling | No |
| Method must assign | No (can read existing value) | Yes — must assign before returning |
| Use case | Pass existing value for modification | Return multiple values |
// ref — caller initializes, method can modify
void Double(ref int x) => x *= 2;
int num = 5;
Double(ref num);
Console.WriteLine(num); // 10
// out — method must assign
bool TryParse(string s, out int result)
{
return int.TryParse(s, out result);
}
if (TryParse("42", out int value))
Console.WriteLine(value); // 42
Q38: How does the using statement work?
using ensures Dispose() is called on an IDisposable object, even if an exception occurs:
// Classic using statement
using (var stream = new FileStream("data.txt", FileMode.Open))
{
// use stream
} // stream.Dispose() called here — guaranteed
// Using declaration (C# 8+) — Dispose at end of scope
using var stream = new FileStream("data.txt", FileMode.Open);
// use stream...
// Dispose called automatically at end of method/scope
// What the compiler generates:
var stream = new FileStream("data.txt", FileMode.Open);
try
{
// use stream
}
finally
{
stream?.Dispose();
}
await using works the same way for IAsyncDisposable:
await using var resource = new AsyncResource();
// DisposeAsync called at end of scope
Q39: What is a delegate and how is it used?
A delegate is a type-safe function pointer — it holds a reference to a method with a matching signature:
// Define delegate type
public delegate int MathOp(int a, int b);
// Methods matching the signature
int Add(int a, int b) => a + b;
int Multiply(int a, int b) => a * b;
// Usage
MathOp op = Add;
Console.WriteLine(op(3, 4)); // 7
op = Multiply;
Console.WriteLine(op(3, 4)); // 12
// Multicast — combine multiple delegates
MathOp combined = (a, b) => a + b;
combined += (a, b) => a * b;
// Note: multicast returns the LAST result
Built-in delegates:
Action— void return, up to 16 parametersFunc<T>— typed return, up to 16 parametersPredicate<T>— returnsbool
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;
Q40: Explain method overloading and overriding
Overloading — same method name, different parameters (compile-time polymorphism):
public int Sum(int a, int b) => a + b;
public double Sum(double a, double b) => a + b;
public int Sum(int a, int b, int c) => a + b + c;
Overriding — replacing a virtual/abstract base method in a derived class (runtime polymorphism):
public class Shape
{
public virtual double Area() => 0;
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}
// Runtime polymorphism
Shape shape = new Circle { Radius = 5 };
Console.WriteLine(shape.Area()); // 78.54 — calls Circle.Area()
| Feature | Overloading | Overriding |
|---|---|---|
| Binding | Compile-time | Runtime |
| Same name | Yes | Yes |
| Parameters | Must differ | Must match exactly |
| Keyword | None | virtual + override |
| Relationship | Same class | Base → Derived |
Q41: Difference between IEnumerable<T> and IQueryable<T>
| Feature | IEnumerable<T> | IQueryable<T> |
|---|---|---|
| Namespace | System.Collections.Generic | System.Linq |
| Execution | In-memory (client-side) | Translatable to remote (e.g., SQL) |
| Filtering | Done in C# after loading data | Translated to SQL — filtered at database |
| Best for | In-memory collections (Lists, Arrays) | Database queries (EF Core) |
// IEnumerable — loads ALL products, then filters in memory
IEnumerable<Product> enumerable = context.Products;
var result = enumerable.Where(p => p.Price > 100).ToList();
// SQL: SELECT * FROM Products (loads everything!)
// IQueryable — filter is translated to SQL
IQueryable<Product> queryable = context.Products;
var result = queryable.Where(p => p.Price > 100).ToList();
// SQL: SELECT * FROM Products WHERE Price > 100 (filtered at DB)
IQueryable for database queries to avoid loading unnecessary data into memory.Q42: What are expression trees in LINQ?
Expression trees represent code as data structures (trees) that can be inspected, modified, and translated at runtime. They power IQueryable<T> — LINQ providers translate expression trees into SQL, REST API calls, etc.
// Lambda as a delegate — executable code
Func<int, bool> isLarge = x => x > 100;
// Lambda as an expression tree — data structure describing the code
Expression<Func<int, bool>> isLargeExpr = x => x > 100;
// Inspect the tree
Console.WriteLine(isLargeExpr.Body); // "x > 100"
Console.WriteLine(isLargeExpr.Parameters[0]); // "x"
// Compile and execute
Func<int, bool> compiled = isLargeExpr.Compile();
Console.WriteLine(compiled(150)); // True
EF Core uses expression trees to translate Where(p => p.Price > 100) into WHERE Price > 100 in SQL — the lambda is never executed as C# code.
Q43: How does exception handling work in C#?
C# uses a structured exception handling model with try, catch, finally, and throw:
try
{
var result = int.Parse("abc"); // throws FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
}
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
{
// Exception filter — catches only if condition is true
Console.WriteLine("Specific error");
}
finally
{
// Always runs — with or without exception
Console.WriteLine("Cleanup");
}
Key points:
- Exceptions bubble up the call stack until caught
finallyalways executes (even withreturnin try/catch)- Exception filters (
when) let you inspect without catching - Most specific exceptions should be caught first
Q44: Name all the ways to rethrow an exception
throw;— preserves the original stack trace (preferred):
catch (Exception)
{
Log("Something failed");
throw; // preserves original stack trace
}
throw ex;— resets the stack trace to this point (avoid):
catch (Exception ex)
{
Log("Something failed");
throw ex; // WARNING: stack trace starts here — original location lost
}
throw new Exception("message", innerEx);— wrap with context:
catch (SqlException ex)
{
throw new DataAccessException("Failed to load user", ex);
// Original exception preserved as InnerException
}
throw; (bare throw) to preserve the original stack trace. Use throw ex only when you intentionally want to hide the original source. Always wrap with inner exception when adding context.Q45: Explain generics
Generics allow you to write type-parameterized classes, methods, and interfaces — one implementation works for any type while maintaining type safety:
// Generic class
public class Repository<T> where T : class, new()
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public T FindById(int id) => _items[id];
}
// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;
// Generic interface
public interface IService<T>
{
T GetById(int id);
void Save(T entity);
}
// Usage — type-safe, no boxing
var userRepo = new Repository<User>();
userRepo.Add(new User { Name = "Alice" });
int max = Max(10, 20); // inferred type: int
string maxStr = Max("a", "z"); // inferred type: string
Common constraints:
| Constraint | Meaning |
|---|---|
where T : class | T must be a reference type |
where T : struct | T must be a value type |
where T : new() | T must have a parameterless constructor |
where T : BaseClass | T must derive from BaseClass |
where T : IInterface | T must implement IInterface |
where T : unmanaged | T must be an unmanaged type |
Q46: What is the difference between Dispose() and Finalize()?
Both clean up resources, but differ fundamentally in when and how they are called:
| Feature | Dispose() | Finalize() (Destructor) |
|---|---|---|
| Called by | Explicitly by developer (using, direct call) | Automatically by GC before collection |
| Timing | Deterministic — you control when | Non-deterministic — whenever GC runs |
| Interface | IDisposable | Language syntax (~ClassName()) |
| Performance | No GC overhead | Promotes object to older generation |
| Unmanaged resources | Always clean up here | Safety net only (if Dispose wasn't called) |
| Multiple calls | Must be idempotent (safe to call multiple times) | Called only once by GC |
public class DatabaseConnection : IDisposable
{
private IntPtr _handle;
private bool _disposed;
public DatabaseConnection(string connStr)
{
_handle = NativeOpenConnection(connStr);
}
// Dispose — called by developer (deterministic cleanup)
public void Dispose()
{
if (_disposed) return;
NativeCloseConnection(_handle);
_handle = IntPtr.Zero;
_disposed = true;
GC.SuppressFinalize(this); // Prevent finalizer from running
}
// Finalizer — safety net, called by GC (non-deterministic)
~DatabaseConnection()
{
Dispose(); // Delegate to Dispose
}
}
// Usage with using — Dispose called at scope end
using var conn = new DatabaseConnection("Server=...");
// conn.Dispose() called automatically
Dispose() via using statements. Implement a finalizer only as a safety net for unmanaged resources. Call GC.SuppressFinalize(this) in Dispose to prevent the GC from calling the finalizer unnecessarily.Q47: What is the Singleton design pattern?
Singleton ensures a class has only one instance and provides a global access point to it. Common use cases: configuration, logging, caching.
// Thread-safe Singleton with Lazy<T> (recommended)
public sealed class DatabaseConfig
{
// Lazy<T> ensures thread-safe, lazy initialization
private static readonly Lazy<DatabaseConfig> _instance =
new(() => new DatabaseConfig());
public static DatabaseConfig Instance => _instance.Value;
private DatabaseConfig() // Private — prevents external instantiation
{
ConnectionString = "Server=localhost;Database=MyApp;";
}
public string ConnectionString { get; }
}
// Usage
var config = DatabaseConfig.Instance;
Console.WriteLine(config.ConnectionString);
Other implementation approaches:
| Approach | Thread-Safe | Lazy | Notes |
|---|---|---|---|
| No lock | No | Yes | Simple, but not thread-safe |
lock everywhere | Yes | Yes | Performance overhead on every access |
| Double-check locking | Yes | Yes | Complex, error-prone |
Lazy<T> | Yes | Yes | Recommended — simple and correct |
| Static initializer | Yes | No | Instance created at program start |
// Alternative: Static initializer (simplest thread-safe approach)
public sealed class Singleton
{
public static readonly Singleton Instance = new();
// Static constructor ensures laziness (runs before first access)
static Singleton() { }
private Singleton() { }
}
services.AddSingleton<T>()) is a better approach — it's testable, explicit, and follows IoC principles.Q48: Why can't a private virtual method be overridden?
A private virtual method is a contradiction because:
privatemeans the member is only accessible within the declaring class — derived classes cannot see it.virtualmeans the member is designed to be overridden in derived classes — but derived classes can't access what's private.
public class Base
{
// Compile error: virtual or abstract members cannot be private
// private virtual void DoWork(); // ERROR!
// Correct: protected virtual — accessible to derived classes
protected virtual void DoWork() { }
}
public class Derived : Base
{
protected override void DoWork() { } // OK
}
The compiler enforces this — a private virtual method is inherently contradictory and produces a compile-time error.
Concurrency
Q49: What are the benefits of using Frozen collections?
FrozenSet<T> and FrozenDictionary<TKey, TValue> (.NET 8+) are immutable collections optimized for fast reads after an initial freeze phase. Once created, they never change, allowing the runtime to optimize lookups:
using System.Collections.Frozen;
var data = new Dictionary<string, int>
{
["apple"] = 1, ["banana"] = 2, ["cherry"] = 3
};
// Freeze — one-time cost, optimizes for read performance
FrozenDictionary<string, int> frozen = data.ToFrozenDictionary();
// Ultra-fast lookups — no locks, no version checks
if (frozen.TryGetValue("apple", out int value))
Console.WriteLine(value); // 1
// FrozenSet
var tags = new[] { "csharp", "dotnet", "linq" }.ToFrozenSet();
bool has = tags.Contains("csharp"); // optimized path
| Benefit | Details |
|---|---|
| Zero allocation on read | No defensive copies, no version checks |
| Optimized internals | May use perfect hashing, compact arrays |
| Thread-safe | Immutable — safe to share across threads |
| Best for | Configuration data, lookup tables, constants |
Q50: Name thread-safe collections
System.Collections.Concurrent provides thread-safe collections that use lock-free or fine-grained locking:
| Collection | Description | Use Case |
|---|---|---|
ConcurrentDictionary<TKey, TValue> | Thread-safe dictionary | Shared cache, counters |
ConcurrentQueue<T> | Thread-safe FIFO | Producer-consumer |
ConcurrentStack<T> | Thread-safe LIFO | Work stealing |
ConcurrentBag<T> | Unordered, optimized for same thread | Thread-local work pools |
BlockingCollection<T> | Bounded, blocking wrapper | Producer-consumer with limits |
var cache = new ConcurrentDictionary<string, string>();
cache.TryAdd("key1", "value1");
cache.AddOrUpdate("key1", "new", (k, old) => $"{old}_updated");
var queue = new ConcurrentQueue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
if (queue.TryDequeue(out int item))
Console.WriteLine(item); // 1
Q51: How to perform a lock for asynchronous code?
You cannot use lock with await (lock doesn't support async). Use SemaphoreSlim instead:
// WRONG — lock statement cannot be used with await
// lock (_sync) { await DoWorkAsync(); } // Compile error!
// CORRECT — SemaphoreSlim
private readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task DoWorkAsync()
{
await _semaphore.WaitAsync();
try
{
await SomeAsyncOperation();
}
finally
{
_semaphore.Release();
}
}
// C# pattern — use await using with a custom async lock
public sealed class AsyncLock
{
private readonly SemaphoreSlim _sem = new(1, 1);
public Task<IDisposable> LockAsync() =>
_sem.WaitAsync().ContinueWith(_ => (IDisposable)new Releaser(_sem));
private sealed class Releaser(SemaphoreSlim sem) : IDisposable
{
public void Dispose() => sem.Release();
}
}
Q52: Name all the ways for creating a new thread
// 1. Thread class — OS thread ( heavyweight)
var thread = new Thread(() => Console.WriteLine("New thread"));
thread.Start();
// 2. Thread pool — reuses threads (preferred)
ThreadPool.QueueUserWorkItem(_ => Console.WriteLine("Thread pool work"));
// 3. Task.Run — thread pool via TPL (most common)
Task.Run(() => Console.WriteLine("Task on thread pool"));
// 4. Task.Factory.StartNew — more options (long-running, cancellation)
Task.Factory.StartNew(() => Console.WriteLine("Custom task"),
TaskCreationOptions.LongRunning);
// 5. async/await — not a new thread, but offloads to thread pool
await Task.Run(() => Compute());
// 6. BackgroundWorker — legacy (WinForms/WPF)
var worker = new BackgroundWorker();
worker.DoWork += (s, e) => { /* background work */ };
worker.RunWorkerAsync();
Task.Run over new Thread. Tasks use the thread pool (efficient reuse), support cancellation, continuation, and exception handling. Use new Thread only when you need a dedicated OS thread (e.g., STA thread for COM interop).Q53: How to execute multiple async tasks at once?
Use Task.WhenAll to run tasks concurrently and wait for all to complete:
var urls = new[] { "api1", "api2", "api3" };
// Concurrent execution — all tasks start at once
var tasks = urls.Select(url => FetchAsync(url)).ToArray();
string[] results = await Task.WhenAll(tasks);
// With result aggregation
var userIds = new[] { 1, 2, 3, 4, 5 };
var users = await Task.WhenAll(userIds.Select(id => GetUserAsync(id)));
// Task.WhenAll vs Task.WhenAny
await Task.WhenAll(tasks); // Wait for ALL to complete
Task<string> first = await Task.WhenAny(tasks); // Wait for FIRST to complete
Error handling:
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Only the FIRST exception is caught
// To see all exceptions:
}
// Access all exceptions
var allResults = await Task.WhenAll(tasks.Select(t => TaskSafe(t)));
async Task<TResult> TaskSafe<TResult>(Task<TResult> task)
{
try { return await task; }
catch { return default; }
}
Q54: What is the difference between AutoResetEvent and ManualResetEvent?
Both are thread synchronization primitives that block threads until a signal is received:
| Feature | AutoResetEvent | ManualResetEvent |
|---|---|---|
After Set() | Automatically resets to unsignaled | Stays signaled until manually reset |
| Releases threads | One thread per Set() | All waiting threads |
| Use case | Producer-consumer (one at a time) | Gate/door (release all at once) |
// AutoResetEvent — releases ONE waiting thread, then auto-resets
var are = new AutoResetEvent(false);
// Thread 1
are.WaitOne(); // blocks until Set() is called
are.Set(); // releases ONE thread, then resets to unsignaled
// ManualResetEvent — releases ALL waiting threads, stays open
var mre = new ManualResetEvent(false);
mre.Set(); // releases ALL waiting threads — stays signaled
mre.Reset(); // manually close the gate
Q55: What are Channels in C#?
Channel<T> (System.Threading.Channels) is a high-performance, async-friendly producer-consumer queue — a modern alternative to BlockingCollection<T>:
using System.Threading.Channels;
// Unbounded — no capacity limit
var channel = Channel.CreateUnbounded<string>();
// Bounded — capacity limit with backpressure strategy
var bounded = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait // block when full
});
// Producer
await channel.Writer.WriteAsync("message1");
channel.Writer.TryComplete(); // signal no more writes
// Consumer
await foreach (var msg in channel.Reader.ReadAllAsync())
{
Console.WriteLine(msg);
}
| Feature | Channel<T> | BlockingCollection<T> |
|---|---|---|
| Async support | Native (ReadAllAsync) | Blocking only |
| Performance | Lock-free, optimized | Lock-based |
| Backpressure | Yes (bounded + FullMode) | Yes (bounded) |
| .NET version | .NET Core 3.0+ | .NET Framework |
Q56: Difference between volatile and Interlocked
Both address thread-safety issues, but at different levels:
| Feature | volatile | Interlocked |
|---|---|---|
| What it does | Prevents CPU cache/read reorder | Provides atomic operations |
| Operations | Read/Write only | Increment, Add, Exchange, CompareExchange |
| Use case | Flag checking | Counters, atomic swaps |
// volatile — ensures reads/writes are not reordered by CPU/compiler
private volatile bool _running = true;
// Thread 1
while (_running) { DoWork(); }
// Thread 2
_running = false; // guaranteed to be visible immediately
// Interlocked — atomic increment (thread-safe counter)
private int _count = 0;
Interlocked.Increment(ref _count); // atomic _count++
volatile does NOT make ++ or += atomic. Those are read-modify-write operations — use Interlocked.Increment instead.Q57: Explain how Async Streams work
Async streams (IAsyncEnumerable<T>, C# 8) let you asynchronously enumerate items one at a time — ideal when each item requires async I/O:
// Producer — yields items asynchronously
public async IAsyncEnumerable<User> GetUsersAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
int page = 0;
while (!ct.IsCancellationRequested)
{
var users = await _api.GetUsersPageAsync(page, ct);
if (users.Count == 0) yield break;
foreach (var user in users)
yield return user;
page++;
}
}
// Consumer — awaits each item
await foreach (var user in GetUsersAsync())
{
Console.WriteLine(user.Name);
}
// With cancellation
await foreach (var user in GetUsersAsync().WithCancellation(ct))
{
Console.WriteLine(user.Name);
}
When to use: Paginated API calls, database cursor streaming, real-time event feeds — anywhere you need to pull items asynchronously.
Q58: Difference between Task.Run and TaskFactory.StartNew
| Feature | Task.Run | TaskFactory.StartNew |
|---|---|---|
| Simplicity | Simple, common case | More options and control |
| Default scheduler | TaskScheduler.Default (thread pool) | Current TaskScheduler |
| Long-running | No direct option | TaskCreationOptions.LongRunning |
| Return type | Task / Task<T> | Task (not generic — must cast) |
| Cancellation | Via overload | Via overload |
// Task.Run — simple, preferred for most cases
var task = Task.Run(() => Compute());
// TaskFactory.StartNew — more control
var task = Task.Factory.StartNew(() =>
{
Compute();
}, CancellationToken.None,
TaskCreationOptions.LongRunning, // dedicated thread
TaskScheduler.Default);
Task.Run by default. Only use Task.Factory.StartNew when you need LongRunning, a custom TaskScheduler, or other advanced creation options. Be careful — StartNew returns Task (not Task<T>), so use StartNew<T> for generic tasks.