C# Language — Basics to Advanced
A value type variable holds the data itself (int, bool, decimal, DateTime, any struct/enum). Assigning it copies the value. A reference type variable holds a reference to an object on the managed heap (class, string, arrays, delegates, record). Assigning it copies the reference, so two variables can point to the same object.
var a = new List<int>(); var b = a; b.Add(1); // a.Count == 1 — same object
int x = 1; int y = x; y++; // x is still 1Value types are stored inline (stack for locals, inside the parent object for fields); reference-type objects always live on the heap and are cleaned up by the GC.
string and StringBuilder?In short: string is immutable: every concatenation creates a new string object, so building a string in a loop causes many allocations (O(n²) copying).
StringBuilderkeeps a mutable buffer and appends in place; callToString()once at the end.- Use
StringBuilderfor loops or many appends, plain strings/interpolation for a handful of pieces.
const vs readonly vs static readonly.const: compile-time constant, implicitly static, only primitives/strings; the value is baked into callers at compile time.readonly: instance field assigned at declaration or in a constructor; can differ per object.static readonly: one value per type, computed at runtime (e.g.static readonly HttpClient Client = new()).
Gotcha: if library A has public const int Max = 10 and app B uses it, changing it to 20 in A does nothing in B until B is recompiled. Use static readonly for values that may change.
In short: Boxing converts a value type to object (or an interface) by allocating a heap object and copying the value; unboxing casts it back to the exact value type.
- It costs an allocation, a copy and GC pressure.
- Typical sources: non-generic collections (
ArrayList),string.Formatwith value-type arguments on old APIs, calling interface methods on structs via the interface. - Generics remove most boxing.
== and .Equals()?In short: For reference types, == compares references unless the operator is overloaded; Equals() is virtual and can be overridden to compare content.
stringoverloads both to compare text.- Records generate value-based
Equalsand==. - For value types,
Equalscompares content. - When you override
Equals, also overrideGetHashCode(objects that are equal must have the same hash code, or dictionaries break).
ref, out and in parameters?All three pass an argument by reference:
ref— must be initialised before the call; the method can read and change it.out— need not be initialised; the method must assign it (used byTryParse).in— read-only reference; avoids copying large structs.
var, dynamic and object?In short: var is compile-time type inference — the variable is strongly typed (var x = 5 is an int).
objectis the base type; you need casts to use members.dynamicskips compile-time checks and resolves members at runtime through the DLR; errors appear asRuntimeBinderException.- Use
dynamiconly for COM interop, dynamic JSON or scripting scenarios.
ArrayList and List<T>?In short: An array has a fixed size and a single element type.
ArrayList(non-generic, legacy) storesobject, so value types get boxed and you need casts.List<T>is generic, type-safe, resizable (it doubles its internal array when full) and has no boxing.- Always prefer
List<T>.
IEnumerable, ICollection, IList and IQueryable.IEnumerable<T>: forward-only iteration (foreach), deferred execution with LINQ to Objects, runs in memory.ICollection<T>: addsCount,Add,Remove,Contains.IList<T>: adds index access andInsert.IQueryable<T>: builds an expression tree that a provider (EF Core) translates to SQL and runs in the database.
db.Orders.Where(o => o.Total > 100).ToList(); // IQueryable: WHERE in SQL
db.Orders.AsEnumerable().Where(o => o.Total > 100).ToList(); // loads ALL rows then filters in memoryIn short: A delegate is a type-safe function pointer (Func, Action, custom delegate).
- It can be multicast.
- An event wraps a delegate so outside code can only
+=/-=handlers; only the declaring class can raise it. - This prevents subscribers from wiping out other handlers (
= null) or raising the event themselves. - Pattern: `public event EventHandler
? - Changed;
and raise withChanged?.Invoke(this, args)`.
In short: No new thread is created by await.
- The compiler turns the method into a state machine.
- When it hits
awaiton an incomplete task, the method returns to its caller and the thread goes back to the pool. - When the I/O completes, a continuation resumes the state machine — on the captured synchronisation context if there is one (UI thread), otherwise on a thread-pool thread.
- CPU-bound work still needs
Task.Runor parallelism to move off the current thread.
Task.WhenAll and awaiting tasks one by one?In short: Awaiting sequentially makes total time the sum of the calls; starting all tasks and then await Task.WhenAll(...) makes it roughly the longest single call.
WhenAllthrows the first exception when awaited, but the returned task'sExceptionholds all of them.- Use
WhenAnyfor "first to finish" or timeouts.
async void dangerous?In short: Callers can't await it, so they can't know when it finishes, and an exception thrown inside crashes the process (it is raised on the synchronisation context instead of being stored in a Task).
- Use
async Taskeverywhere except event handlers.
IDisposable and the using statement? Dispose vs Finalize?In short: IDisposable.Dispose() releases unmanaged resources (connections, file handles) deterministically.
usingguaranteesDisposeis called even if an exception is thrown (compiles totry/finally).- A finaliser is called by the GC at an unpredictable time and costs an extra GC cycle; it is only a safety net for raw unmanaged handles.
- In
DisposecallGC.SuppressFinalize(this)if a finaliser exists. - Most code just wraps
SafeHandleand never writes a finaliser.
In short: The heap is split into Gen 0 (new objects, collected very often and cheaply), Gen 1 (survivors of Gen 0, a buffer), and Gen 2 (long-lived objects, collected rarely and expensively).
- Most objects die young, so collecting Gen 0 frequently is efficient.
- Objects of 85,000 bytes or more go to the Large Object Heap, collected with Gen 2. .NET has workstation and server GC modes, plus background (concurrent) GC for Gen 2.
In short: Static methods in a static class whose first parameter has this, called as if they were instance methods.
- LINQ is built on them.
- Limitations: they can't access private members, an instance method with the same signature always wins, and they're resolved at compile time (no polymorphism).
- C# 14 adds extension members (properties, static members) via
extensionblocks.
struct and class?In short: Struct: value type, copied on assignment, no inheritance (can implement interfaces), can't have a parameterless constructor before C# 10, ideal for small immutable data.
- Class: reference type, supports inheritance and polymorphism, lives on the heap, compared by reference by default.
- Large mutable structs are a common bug source (you modify a copy).
In short: record (C# 9) is a class with value-based equality, a generated ToString, deconstruction and with expressions for non-destructive copies.
record struct(C# 10) is the value-type version.- Use them for DTOs, messages, events and value objects.
- Avoid them for EF Core entities that are tracked by identity and mutated.
In short: Generics let you write type-safe code once for many types (List<T>, Repository<T>).
- They remove casts and boxing, and errors show up at compile time.
- Constraints (
where T : class, new()) restrict which types can be used. - For value types the JIT generates specialised code; for reference types the code is shared.
throw and throw ex?In short: throw; rethrows the current exception and preserves the original stack trace.
throw ex;throws it again from the current line, losing where it really failed.- To add context, throw a new exception and pass the original as
innerException.
decimal instead of double for money?In short: double is binary floating point and can't represent many base-10 fractions exactly (0.1 + 0.2 != 0.3), so totals drift by tiny amounts that show up in invoices and reports.
decimalstores base-10 digits exactly with 28–29 significant digits.- Trade-off:
decimalis slower and uses 16 bytes, so usedoublefor scientific/graphics maths anddecimalfor money and quantities that must add up exactly. - In SQL Server map it to
DECIMAL(18,2), notFLOAT.
Parse, TryParse and Convert?In short: int.Parse("x") throws FormatException for bad input (and ArgumentNullException for null).
int.TryParse(s, out var n)returnsfalseinstead of throwing — use it for user input.Convert.ToInt32(value)accepts many types (object, double, string), returns 0 fornull, and rounds doubles to the nearest even number (Convert.ToInt32(2.5)→ 2) whereas a cast truncates ((int)2.9→ 2).- Pass a
CultureInfowhen parsing numbers and dates from files or APIs.
Dictionary and Hashtable?In short: Dictionary<TKey,TValue> is generic: type-safe, no boxing, faster; a missing key with the indexer throws KeyNotFoundException (use TryGetValue).
Hashtableis the old non-generic version storingobjectkeys/values: needs casts, boxes value types, and returnsnullfor a missing key.- Both are hash tables with O(1) average lookup.
- For multiple threads use
ConcurrentDictionary.
In short: A multidimensional (rectangular) array int[,] is one object where every row has the same length, accessed as a[r, c].
- A jagged array
int[][]is an array of arrays — each row is a separate array and can have a different length, accessed asa[r][c]. - Jagged arrays are more flexible and often faster in .NET; rectangular arrays suit fixed grids such as matrices.
In short: for when you need the index or a counted loop;
foreachto read every item of anyIEnumerable(cleanest, but you can't add/remove items of aListinside it);whileto repeat while a condition is true (may run zero times);do-whilewhen the body must run at least once (menus, retry prompts).breakexits the loop,continueskips to the next iteration.
using.In short: A namespace groups related types and prevents name clashes (Shop.Models.Order vs Billing.Order).
- The
usingdirective (using System.Text;) imports a namespace — plususing static, aliases andglobal using. - The
usingstatement/declaration (using var conn = new SqlConnection(...)) callsDispose()automatically at the end of the scope, even if an exception is thrown.
In short: Compiler-generated, unnamed classes with read-only properties, created with new { Name = "Asha", Age = 28 } and used through var.
- They have value-based
Equals/GetHashCode, and are mostly used for LINQ projections within one method. - They can't be returned in a typed way or have methods — use a record or a tuple for that.
In short: Stack<T> is LIFO — Push adds to the top, Pop removes the most recent, Peek reads it (undo, expression parsing, DFS).
Queue<T>is FIFO —Enqueueadds at the back,Dequeueremoves the oldest (job processing, BFS, buffering).- Both are O(1) for add/remove;
TryPop/TryDequeueavoid exceptions on empty collections;PriorityQueue<T,P>dequeues by priority.
In short: Derive from Exception, end the name with Exception, provide the standard constructors (parameterless, message, message + inner exception) plus constructors/properties for useful data (e.g. Balance, Requested).
- Create one only when callers need to catch that specific situation (e.g.
InsufficientFundsException→ show a friendly message or return 409); - otherwise throw the built-in ones (
ArgumentException,InvalidOperationException). - Always pass the inner exception when wrapping.
Tuple and ValueTuple?In short: Tuple<T1,...> (.NET 4) is a class on the heap with read-only Item1, Item2 properties.
ValueTuple(C# 7, syntax(int Id, string Name)) is a struct — no allocation — with named, mutable fields, deconstruction (var (id, name) = ...) and==support.- Use ValueTuple for returning several values from private/internal methods; use a record for public APIs because tuple element names are not kept at runtime.
Func, Action and Predicate?In short: All are built-in generic delegate types.
Func<T1..., TResult>takes up to 16 inputs and returns a value (Func<int,int,int> add).Action<T1...>takes inputs and returnsvoid(Action<string> log).Predicate<T>takes one input and returnsbool, used byList<T>.Find/FindAll/RemoveAll; LINQ usesFunc<T,bool>for the same idea.
In short: A delegate whose invocation list contains several methods, built with += and reduced with -=.
- Invoking it calls each method in order; for delegates with a return value only the last result is returned, and an exception in one handler stops the rest.
- Events are built on multicast delegates — that's how many subscribers receive one event.
In short: Stream it instead of loading it all: File.ReadLines(path) (lazy IEnumerable<string>) or a StreamReader with ReadLineAsync in a loop, a FileStream with a reasonable buffer for binary data, and CopyToAsync to move data between streams.
- Avoid
File.ReadAllText/ReadAllLinesfor large files (whole file in memory), dispose streams withusing, and use async I/O in server apps.
In short: DateTime holds a date/time with a Kind (Local, Utc, Unspecified) but no offset, so the same value can mean different instants on different servers.
DateTimeOffsetstores the instant plus the UTC offset, so it is unambiguous.- Store UTC (
DateTime.UtcNoworDateTimeOffset) in databases and APIs, convert to the user's time zone only for display, useDateOnlyfor pure dates like birthdays, andTimeProviderto make "now" testable.
[Flags] enum?In short: An enum whose values are powers of two (Read = 1, Write = 2, Delete = 4) so several can be combined in one value with bitwise OR (Read | Write).
- Test with
(value & Flag) != 0orHasFlag, remove withvalue &= ~Flag. [Flags]also makesToString()print "Read, Write".- Typical for permissions and options.
lock(sugar forMonitor.Enter/Exit): mutual exclusion inside one process, synchronous only.SemaphoreSlim: limits concurrency to N and supportsWaitAsync, so use it when you mustawaitinside the critical section.Interlocked: atomic operations on single variables (increment, compare-exchange), lock-free.ConcurrentDictionary/ConcurrentQueue/Channel<T>: built-in thread-safe structures.- Immutability: data that never changes needs no locks.
- Across processes/servers: database locks, distributed locks (Redis, blob leases),
Mutexfor the same machine.
In short: Classic case: in a UI app or old ASP.NET (with a synchronisation context), code calls task.Result.
- The blocked thread holds the context; the awaited method's continuation needs the same context to resume, so both wait forever.
- Fixes: go async all the way (
awaitinstead of.Result), useConfigureAwait(false)in library code, and never mix blocking and async calls. - ASP.NET Core has no synchronisation context, but blocking still causes thread-pool starvation under load.
Span<T> and Memory<T>?In short: Span<T> is a stack-only (ref struct) view over contiguous memory: arrays, stackalloc or native memory.
- You can slice and parse without allocating new arrays or strings.
- It can't be a class field, boxed or used across
await. Memory<T>is the heap-storable counterpart, usable in async methods.- Both are used heavily inside ASP.NET Core and System.Text.Json for performance.
Task and ValueTask?In short: Task is a class, so every async call that returns one allocates (except cached completed tasks).
ValueTask<T>is a struct that avoids the allocation when the result is often available synchronously (e.g. from a cache).- Rules: await a
ValueTaskonly once, don't await it concurrently, and convert with.AsTask()if you need to. - The default is still
Task.
In short: They allow implicit conversion of generic type arguments.
- Covariance (
out T) works for outputs:IEnumerable<string>→IEnumerable<object>. - Contravariance (
in T) works for inputs:Action<object>→Action<string>,IComparer<Animal>used asIComparer<Dog>. - Only interfaces and delegates support this, and only for reference types.