Back
Keentune
C# curriculum 26 chapters
·
221 concepts
·
free
Everything the adaptive question bank can teach and test in C#, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
Start practicing C#
New here? Read the C# guide A free 18-minute primer — the mental model, the mistakes beginners make, and what to practice first.
A. The type system: value types, reference types, boxing •
A value-type variable holds the data; a reference-type variable holds a reference to it.
•
Assigning a struct copies every field; assigning a class copies only the reference.
•
Where a value lives follows containment (field of a class, captured local), not the value/reference keyword.
•
Value types default to all-fields-zero, reference types to null ; default(T) names it.
•
Converting a value type to object or an interface allocates a heap copy.
•
A boxed int will not unbox to long ; you get InvalidCastException .
•
Boxing inside a loop allocates once per iteration and erases the reason you chose a struct.
•
var infers the compile-time type; it is not dynamic and the type never changes.
•
dynamic moves member resolution to runtime and gives up every compile-time check.
Practice this section →
B. Numbers and arithmetic •
int / int discards the fraction; cast one operand to get a fractional result.
•
Integer arithmetic wraps silently unless a checked context is active.
•
The checked operator and statement turn wraparound into OverflowException .
•
Two binary approximations versus an exact base-10 type with ~28 significant digits.
•
decimal avoids the representation error that makes 0.1 + 0.2 != 0.3 in double .
•
Comparing double with == is a bug; compare within a tolerance.
•
double.NaN != double.NaN , so only double.IsNaN detects it; division by zero differs for double and int .
•
byte + byte evaluates as int , so assigning the result back needs a cast.
•
Widening is implicit; narrowing needs a cast and may silently lose data.
Practice this section →
C. Strings and text •
Every "modification" allocates a new string; the original is untouched.
•
Concatenating in a loop is quadratic allocation; StringBuilder is the fix.
•
Identical literals share one instance, which makes reference equality accidentally "work".
•
== on string compares characters, unlike every other reference type.
•
StringComparison.Ordinal compares code units; culture rules differ per locale and per OS.
•
$"..." evaluates each hole at runtime and applies its format specifier.
•
@"..." keeps backslashes; """...""" keeps newlines and quotes without escaping.
•
char is 16 bits, so an emoji is two chars and Length is not a count of characters.
•
ToString and parsing follow CurrentCulture unless you pass InvariantCulture .
Practice this section →
D. Null: nullable value types and nullable reference types •
int? is Nullable<int> , a struct with HasValue and Value — still a value type.
•
Reading .Value on an empty nullable throws InvalidOperationException , not a null reference.
•
An operator on nullables propagates null: any null operand makes the whole result null.
•
With a null operand both < and > are false, so !(a < b) is not a >= b .
•
Nullable reference annotations are static analysis; nothing is checked or enforced at runtime.
•
string? changes no IL; it changes what the analyzer warns about.
•
The ! suffix silences the warning and asserts nothing; it is a promise, not a check.
•
?. short-circuits the entire chain and yields a nullable result.
•
?? supplies a fallback; ??= assigns only when the target is null.
•
MaybeNull , NotNullWhen and friends describe flow the compiler cannot infer.
•
ArgumentNullException.ThrowIfNull on a public boundary, because callers may not be nullable-aware.
Practice this section →
E. Structs and value semantics •
A struct passed by value is copied; mutating the parameter cannot be seen by the caller.
•
Calling a mutating method on a struct held in a readonly field or a collection mutates a hidden copy.
•
readonly struct promises no member mutates, letting the compiler skip defensive copies.
•
default(T) and new T[n] zero the fields without running any constructor.
•
ValueType.Equals falls back to reflection; override Equals and GetHashCode .
•
Implementing IEquatable<T> lets generic code compare without boxing each operand.
•
in passes a large struct by read-only reference instead of copying it.
•
A struct cannot inherit or be inherited from; reuse comes from interfaces and composition.
Practice this section →
F. Records and immutable data •
A record's generated Equals compares every field, not the reference.
•
record alone is a reference type; record struct is a value type with the same generated members.
•
with copies the instance and replaces only the named members.
•
A positional record generates init-only properties, a constructor, and Deconstruct .
•
init allows assignment during object initialization and never afterwards.
•
required forces the caller to set a member without adding a constructor parameter.
•
A base and a derived record are never equal, because the generated EqualityContract differs.
•
The printed form lists the type name and every property, which makes records good log payloads.
Practice this section →
G. Members: fields, properties, constructors, indexers •
The compiler emits a hidden field; a property is still a pair of methods, not storage.
•
A property can be virtual, appear on an interface, and change implementation without breaking callers.
•
=> on a property recomputes on every read; it is not a cached value.
•
: this(...) and : base(...) , and where field initializers run in that order.
•
Runs once, lazily, before first use, and cannot be called or ordered by you.
•
const is inlined at compile time; readonly is assigned at construction and can differ per instance.
•
Changing a public const requires recompiling every consumer, because the old value is baked in.
•
this[...] gives a type array-like access and can be overloaded by key type.
Practice this section →
H. Inheritance and polymorphism •
A virtual member dispatches on the object's runtime type.
•
A hidden member dispatches on the compile-time type, so the same object behaves differently through two references.
•
An abstract member has no body and obliges every concrete derived class to supply one.
•
sealed override ends the chain and lets the JIT devirtualize the call.
•
base.M() invokes the parent implementation directly, with no further virtual dispatch.
•
The base constructor runs first, so a virtual call reaches a derived object whose fields are still unset.
•
Override both or dictionaries break; the hash must not change while the object is a key.
•
A boolean test, a null-returning conversion, and a throwing conversion.
•
private , protected , internal , protected internal (either) and private protected (both).
Practice this section →
I. Interfaces •
A public member satisfies the interface and stays callable on the concrete type.
•
An explicitly implemented member is reachable only through an interface-typed reference.
•
Two interfaces declaring the same signature are separated by implementing each explicitly.
•
An interface member with a body is inherited but callable only through the interface.
•
Interfaces hold no fields, so a default member can only call other members.
•
An interface can require a static member, which is what makes generic math possible.
•
Many contracts versus one implementation inheritance, and the versioning cost of each.
Practice this section →
J. Methods, parameters and overloads •
Passing a class copies the reference, so reassigning the parameter is invisible to the caller.
•
ref aliases the caller's variable, so an assignment inside the method is visible outside.
•
out needs no prior value but must be definitely assigned before the method returns.
•
A variadic call allocates an array, and an empty call still passes one.
•
The default value is compiled into the call site, so changing it needs every caller recompiled.
•
The best conversion wins; a genuine tie is a compile error, never a runtime choice.
•
A static method bound by its first parameter, which always loses to an applicable instance method.
•
A nested function that can recurse and capture without allocating a delegate.
•
Returning a reference into an array or field, and the escape rules that stop it outliving its storage.
Practice this section →
K. Operators and expressions •
Multiplicative before additive, relational before equality, assignment last and right-associative.
•
&& and || skip the right operand; & and | always evaluate both.
•
Both arms must convert to one common type, and the result is an expression, not a branch.
•
The value of the expression differs from the effect on the variable.
•
x += y inserts an implicit cast back to x 's type, hiding a narrowing conversion.
•
Overloading == obliges you to overload != , and to reconsider Equals and GetHashCode .
•
implicit and explicit conversion operators, and why an implicit one surprises readers.
•
A compile-time string and a compile-time Type , both resolved without executing anything.
•
is null uses the pattern, so an overloaded == cannot intercept it.
Practice this section →
L. Statements and control flow •
One is a statement with cases, the other an expression that produces a value.
•
Every non-empty switch section must break , return , throw or goto .
•
An unmatched value throws SwitchExpressionException ; the compiler only warns.
•
Any type with a suitable GetEnumerator works; implementing IEnumerable is not required.
•
You cannot assign to the foreach variable, and mutating the collection invalidates the enumerator.
•
A local must be provably assigned on every path before it is read.
•
using var x =... disposes at end of scope without another level of nesting.
Practice this section →
M. Pattern matching •
is Foo f tests the type and introduces the typed variable in one step.
•
Matching a literal, and is null versus is not null .
•
is > 0 and < 10 , with not binding tightest and or loosest.
•
Matching nested members directly, without a chain of null checks.
•
A tuple-shaped pattern requires a tuple or a Deconstruct method.
•
Matching a sequence's shape, with .. for the unexamined middle.
•
Always matches and binds, which is how you name a computed value for a guard.
•
An extra boolean tested only after the pattern itself matches.
•
The first matching arm wins, so a broader pattern above makes a narrower one unreachable.
Practice this section →
N. Exceptions •
Entering a try is nearly free; throwing and walking the stack is not.
•
A base-type catch placed above a derived one is a compile error.
•
On normal exit, on return , and while an exception unwinds.
•
A bare throw preserves the original stack trace; throw ex resets it to here.
•
when inspects the exception before the stack unwinds, so a logging filter sees the original frames.
•
Wrapping preserves the cause; diagnosis means walking InnerException .
•
Wait and .Result wrap failures in one; await rethrows only the first.
•
Derive from Exception , keep the standard constructors, and carry data as properties.
•
An empty catch (Exception) converts a crash into corrupted state you find much later.
Practice this section →
O. Generics •
Generics give compile-time type safety and skip the boxing that an object -based API forces.
•
where T: class , struct , new() , a base type, or an interface.
•
The newer constraints, and exactly which types each admits.
•
Each value-type instantiation gets its own JIT-compiled code; reference types share one.
•
List<> versus List<int> , and what typeof returns for each.
•
Inferred from the arguments only; the return type never participates.
•
Cache<int> and Cache<string> hold entirely separate static fields.
•
default(T) is null for a reference type and a zeroed value for a struct, which is why == null is the wrong test.
•
With no constraint, only object 's members are available on T .
Practice this section →
P. Variance •
IEnumerable<Derived> is an IEnumerable<Base> because T only comes out.
•
Action<Base> is an Action<Derived> because T only goes in.
•
List<Derived> is not a List<Base> , and writing to it is why.
•
Arrays are covariant at compile time and throw ArrayTypeMismatchException on the store.
•
Classes cannot declare in or out type parameters.
•
IEnumerable<int> is not IEnumerable<object> , because boxing is not a reference conversion.
Practice this section →
Q. Delegates, lambdas and events •
The declaration defines a signature; an instance holds a target object and a method.
•
A combined delegate invokes in order and returns only the last result.
•
The built-in generic delegates, and which of them returns a value.
•
A lambda captures the variable itself, not its value at the moment of capture.
•
The foreach variable is fresh per iteration; a for variable is shared by every lambda.
•
The compiler lifts them into a display class, which is what keeps them alive.
•
Expression<Func<...>> compiles to inspectable data a provider can translate, not to executable IL.
•
The event keyword limits outside code to += and -= .
•
A publisher's handler list keeps every subscriber alive until it unsubscribes.
Practice this section →
R. Collections •
Length never changes; Array.Resize allocates and copies a new array.
•
Count is elements, Capacity is allocated slots, and growth reallocates by doubling.
•
A key whose GetHashCode changes after insertion becomes unreachable.
•
The indexer throws on a missing key; TryGetValue reports it without an exception.
•
Set semantics with amortized O(1) containment, and the in-place set operations.
•
FIFO, LIFO, and O(1) insertion at a node you already hold.
•
Throws InvalidOperationException ; iterate a snapshot or collect the changes.
•
What IEnumerable , ICollection , IList and IReadOnlyList each add to the contract.
Practice this section →
S. Iterators •
The compiler rewrites the method into a class whose MoveNext resumes where it left off.
•
Nothing in the method executes until the first MoveNext , so argument checks are delayed too.
•
Split into a normal method that validates and a private iterator, so guards throw at call time.
•
Ends the sequence without producing a value.
•
A finally inside an iterator runs when the enumerator is disposed, which foreach does on exit.
•
Enumerating the same query twice executes the body twice.
•
await foreach over an async sequence, each element awaited as it arrives.
Practice this section →
T. LINQ •
Building a query executes nothing; the work happens when it is enumerated.
•
Changing a captured variable after composing the query changes the result you get later.
•
ToList , ToArray , Count , First and the aggregates run the query on the spot.
•
Where and Select stream one element at a time; OrderBy and GroupBy must read the whole source first.
•
The from / where / select form compiles to exactly the same method calls.
•
A one-to-one projection versus flattening a sequence of sequences.
•
Throwing on an empty sequence versus returning the element type's default.
•
Single throws when a second match exists, which First happily ignores.
•
Any stops at the first match; Count() > 0 walks the entire sequence.
•
A second OrderBy replaces the ordering; ThenBy refines it.
•
One builds an expression tree for a remote provider, the other runs delegates in memory.
•
Mutating the source between composing and enumerating changes or invalidates the results.
Practice this section →
U. Asynchronous programming •
await releases the current thread while waiting; it does not start a new one.
•
The compiler rewrites everything after each await into a continuation on a state machine.
•
An async void method's exception cannot be awaited or caught by the caller; it crashes the process.
•
The Task an async method returns is already running; awaiting only observes it.
•
.Result or .Wait() on a captured context blocks the very thread the continuation needs.
•
The captured context is what makes the continuation resume on the UI thread.
•
Declines to capture the context; the default choice inside a library.
•
The exception is stored and rethrown at the await , not at the call.
•
A Task that is never awaited can swallow its failure entirely.
•
Start both operations, then await, instead of awaiting each in turn.
•
Awaiting WhenAll surfaces only the first exception; the rest are on the returned Task .
•
Avoids an allocation on the usually-synchronous path, and may be awaited only once.
•
Cancellation is cooperative: the callee must observe the token or pass it further down.
•
Bridges a callback-based API into a Task , with RunContinuationsAsynchronously to avoid inline resumption.
Practice this section →
V. Resource management and the garbage collector •
Liveness is reachability from roots, which is why reference cycles are collected.
•
Most objects die young, so gen0 collections are frequent and cheap and gen2 is not.
•
Objects above the LOH threshold are allocated separately and are not compacted by default.
•
A finalizer may run late or never, and it keeps the object alive one extra collection.
•
Deterministic release of a resource the collector knows nothing about.
•
Dispose(bool disposing) plus GC.SuppressFinalize , for a type that owns an unmanaged handle.
•
The using statement compiles to try / finally , so an exception cannot skip cleanup.
•
Wraps an OS handle so the runtime cannot leak it across a P/Invoke or an abort.
•
Static collections, event subscriptions and captured closures keep objects reachable forever.
Practice this section →
W. Spans, memory and allocation •
Span<T> points into existing memory — array, stack or native — without copying it.
•
A ref struct cannot be boxed, stored in a field, or captured by a lambda.
•
The stack-only rule is exactly why an async method cannot hold a Span<T> across a suspension.
•
The heap-storable counterpart that async code can hold, with .Span to get the view.
•
AsSpan() slices with no allocation where Substring allocates a new string.
•
A small buffer allocated on the stack, and why an unbounded size is a stack-overflow bug.
•
.. and ^ produce a copy from an array but a view from a span.
•
Renting and returning buffers to remove steady-state allocation from a hot path.
Practice this section →
X. Reflection, attributes and unsafe code •
An attribute changes nothing until some code reflects over it.
•
Declares where an attribute may appear, whether it repeats, and whether it is inherited.
•
CallerMemberName and friends are filled in by the compiler at the call site, not by reflection.
•
Member lookup and Invoke are orders of magnitude slower than a direct call; cache or emit a delegate.
•
The compile-time type named in source versus the runtime type of an instance.
•
MakeGenericType and MakeGenericMethod close an open type at runtime.
•
Compile-time code generation, which removes the reflection cost and works with trimming and AOT.
•
Pointers require an unsafe context, and fixed pins an object so the GC cannot move it.
Practice this section →
Y. Namespaces and using directives •
namespace X; with a semicolon applies to the whole file and needs no braces or indentation; the block form nests and can hold more than one namespace per file.
•
A plain using applies only to the file it appears in; global using declares it once for the entire project, from any file.
•
The SDK adds a set of global using directives for you, so a name resolves with no visible directive anywhere in the file.
•
using static System.Math; imports a type's static members so PI and Pow are callable without the type name prefix.
•
using CustomerList = System.Collections.Generic.List<...>; names a long generic or resolves a collision; the alias is file-local unless declared global.
Practice this section →
Z. Tuples and deconstruction •
System.ValueTuple is a mutable value type whose elements are public fields; System.Tuple is an immutable reference type whose elements are Item1 -style properties.
•
Element names are given in the initializer or the tuple type and are a compile-time convenience; the underlying fields stay Item1 , Item2 , ….
•
Assignment needs the same element count and each right-hand element implicitly convertible to its left-hand counterpart, matched by position rather than by name.
•
== and != compare element-wise in order, converting where needed, so (int, byte) can equal (long, int) .
•
A tuple is usually the refactor that REMOVES an out parameter, but a tuple can itself be the out type, e.g. a Dictionary<int, (int Min, int Max)> lookup.
Practice this section →
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Start practicing C#
All about C# practice
Browse all free study guides
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC