Modern C#
Records and value equality, pattern matching, init-only and required members, nullable reference types, collection expressions, and C# 14 features.
16 questions
JuniorTheoryCommonUnder #nullable enable, what is the difference between string and string??
Under #nullable enable, what is the difference between string and string??
With nullable enabled, string? declares a reference that may hold null, while a plain string is one the compiler treats as never null. Dereferencing a maybe-null value warns (CS8602), as does assigning null to a non-nullable one.
Common mistakes
- ✗Thinking
string?isNullable<string>— that wrapper exists only for value types - ✗Expecting hard compile errors; the analysis reports warnings by default
- ✗Believing a non-nullable annotation makes it impossible for
nullto arrive at run time
Follow-up questions
- →What does the null-forgiving
!operator tell the compiler, and what does it cost you? - →Why can a non-nullable property still hold
nullright after deserialization?
SeniorTheoryCommonIn C# 14, how many times is customer evaluated in customer?.Order += GetOrder()?
In C# 14, how many times is customer evaluated in customer?.Order += GetOrder()?
Once. ?. evaluates its receiver a single time and short-circuits on null: nothing else runs — not the getter, not the setter, not GetOrder(). When non-null, the compound assignment reads via get and writes via set, once each.
Common mistakes
- ✗Thinking a compound assignment re-evaluates the receiver for the read and again for the write
- ✗Believing the right-hand side runs before the null check and is merely discarded
- ✗Forgetting that both accessors are skipped entirely when the receiver is null
Follow-up questions
- →Why can the language not lift
customer?.Order++the way it lifts+=? - →How would you rewrite this by hand to get the same short-circuiting without
?.?
JuniorTheoryOccasionalHow does an auto-implemented property differ from an expression-bodied one?
How does an auto-implemented property differ from an expression-bodied one?
{ get; set; } is auto-implemented: the compiler emits a hidden backing field, and the accessors write into it and read it back. An expression-bodied => expr property declares a get only, emits no field, and re-evaluates the expression on every read. Both forms compile to accessor methods.
Common mistakes
- ✗Thinking an expression-bodied property stores its value rather than recomputing it
- ✗Believing an auto-property compiles to a plain field with no accessors
- ✗Expecting to name the auto-property's generated backing field in source
Follow-up questions
- →Why can source code not name the backing field the compiler generates?
- →What does adding a
private setto an auto-property change for callers?
JuniorTheoryOccasionalHow does the constant pattern x is null differ from writing x == null?
How does the constant pattern x is null differ from writing x == null?
is null is a constant pattern: the compiler lowers it to a direct reference comparison and never calls a user-defined operator ==. x == null invokes that overload when the type declares one, so it can answer differently or even throw. is not null is the negated form.
Common mistakes
- ✗Assuming
== nullalways ends in a reference comparison, ignoring operator overloads - ✗Thinking
is nullcan reach a user-defined equality operator - ✗Writing
!(x is null)instead of theis not nullform
Follow-up questions
- →How can an overloaded
operator ==makex == nullrecurse forever? - →What does
x is nulltest for aNullable<T>such asint??
JuniorTheoryOccasionalWhat problem do raw string literals solve, and how do they treat indentation?
What problem do raw string literals solve, and how do they treat indentation?
A raw string literal is delimited by three or more double quotes and needs no escaping: quotes, backslashes and braces stay literal, so JSON, regex and SQL stay readable. In a multi-line one, the closing delimiter's indent is stripped per line.
Common mistakes
- ✗Thinking embedded quotes still need escaping or doubling inside a raw literal
- ✗Not knowing the closing delimiter's indentation is what gets stripped from each line
- ✗Believing the delimiter is fixed at three quotes rather than three or more
Follow-up questions
- →How do you embed a run of three double quotes inside a raw string literal?
- →What changes when you prefix a raw string literal with one or two
$signs?
JuniorTheoryOccasionalWhat does target-typed new() infer from, and where can it not be used?
What does target-typed new() infer from, and where can it not be used?
Target-typed new() takes the type from the assignment target, not the arguments, so you may omit it wherever the target type is known: a field, a typed local, a parameter, a return. With nothing to infer from — var x = new(); — it is an error.
Common mistakes
- ✗Thinking the type is inferred from the constructor arguments instead of from the target
- ✗Trying
var x = new();—varsupplies no target type to infer from - ✗Assuming the substitution happens at run time; it is done by the compiler
Follow-up questions
- →How does target-typed
new()behave when the target has two applicable overloads? - →When does omitting the type name with
new()hurt readability rather than help?
JuniorTheoryOccasionalWhat are top-level statements, and what does the compiler generate for them?
What are top-level statements, and what does the compiler generate for them?
Since C# 9 the entry point may be bare statements at the top of one file, with no class and no Main. The compiler generates the Program class and its Main around them. args and await work inside, and only one file per project may use them.
Common mistakes
- ✗Thinking several files in one project can each carry top-level statements
- ✗Believing
argsandawaitare unavailable inside top-level statements - ✗Assuming no
Programclass exists at all — the compiler still generates one
Follow-up questions
- →How do you read the command-line arguments and return an exit code from top-level statements?
- →Where do local functions and
usingdirectives go in a file that uses top-level statements?
MiddleTheoryOccasionalIn C# 14, what happens in customer?.Order = GetOrder() when customer is null?
In C# 14, what happens in customer?.Order = GetOrder() when customer is null?
C# 14 allows ?. and ?[] on the left of an assignment or compound assignment. When customer is null the assignment is skipped and the right-hand side is never evaluated, so GetOrder() is not called. customer?.Order++ remains a compile error.
Common mistakes
- ✗Thinking the right-hand side still runs when the receiver is null — it is not evaluated
- ✗Expecting a
NullReferenceExceptioninstead of a skipped assignment - ✗Assuming
customer?.Order++is now legal — increment and decrement are still errors
Follow-up questions
- →How many times is the receiver evaluated in
customer?.Order += GetOrder()? - →Why do increment and decrement stay disallowed on a null-conditional target?
MiddleTheoryOccasionalWith #nullable enable, what is enforced at compile time and what at run time?
With #nullable enable, what is enforced at compile time and what at run time?
Nothing is enforced at run time: annotations are metadata, no null checks are emitted, IL is unchanged. At compile time a flow analysis tracks null-state and reports warnings, not errors, so null still arrives from unannotated code or a !.
Common mistakes
- ✗Expecting run-time null checks — none are emitted
- ✗Treating a clean build as proof that no
NullReferenceExceptioncan happen - ✗Forgetting that unannotated or oblivious dependencies escape the analysis entirely
Follow-up questions
- →How do you turn nullable warnings into build errors, and why is that a project-level decision?
- →What does the null-forgiving
!operator actually change in the emitted IL?
MiddleTheoryRareWhat does the collection expression [1, 2, 3] compile to, and what does .. do?
What does the collection expression [1, 2, 3] compile to, and what does .. do?
A collection expression is target-typed: [1, 2, 3] becomes an array, a List<T>, a Span<T> or a type with a CollectionBuilder — whatever the target is — and the compiler picks how to build it. The spread ..other splices its items in place.
Common mistakes
- ✗Thinking the type is inferred from the elements rather than from the target
- ✗Expecting
..otherto nest the collection instead of splicing its elements - ✗Assuming it always allocates a
List<T>, even when the target is aSpan<T>or an array
Follow-up questions
- →How does the compiler build a collection expression assigned to a
ReadOnlySpan<T>? - →What does a
CollectionBuilderattribute let your own collection type do with[...]?
MiddleTheoryRareWhat can a C# 14 extension block declare that a classic extension method cannot?
What can a C# 14 extension block declare that a classic extension method cannot?
An extension(Receiver r) block in a static class names the receiver once and can declare extension properties, static members and operators — not only instance methods. Classic this-parameter methods keep working, and neither form adds state.
Common mistakes
- ✗Thinking an extension block can add fields or state to the receiving type
- ✗Believing classic
this-parameter extension methods stop working - ✗Assuming only instance methods can be declared — properties, operators and static members can too
Follow-up questions
- →Why can an extension property have no backing field of its own?
- →How does the compiler lower an
extensionblock, and what does that mean for binary compatibility?
MiddleTheoryRareWhat does the C# 14 field keyword refer to inside a property accessor?
What does the C# 14 field keyword refer to inside a property accessor?
field names the compiler-synthesized backing field of the property declared, so one accessor can carry logic while the other stays auto-implemented. It works in get, set and init. Being contextual, it can shadow a variable named field.
Common mistakes
- ✗Thinking
fieldcreates a new field rather than naming the existing synthesized one - ✗Expecting it to be unusable in
setorinitaccessors - ✗Forgetting it is contextual, so a variable genuinely named
fieldcan be shadowed
Follow-up questions
- →How do you refer to a variable genuinely named
fieldinside an accessor that uses the keyword? - →What does
fieldchange for a property that already has a hand-written backing field?
SeniorTheoryRareWhy is the C# 14 field keyword a breaking change, and how do you escape it?
Why is the C# 14 field keyword a breaking change, and how do you escape it?
Inside a property accessor field now binds to the compiler's backing field, so existing code with an identifier named field in scope can change meaning or stop compiling. Escape it as @field, or qualify a class field with this.field.
Common mistakes
- ✗Thinking
fieldis fully reserved everywhere rather than contextual inside a property accessor - ✗Assuming no existing code can be affected, when an identifier named
fieldin scope is exactly the hazard - ✗Not knowing
@field— orthis.fieldfor a class field — disambiguates the identifier
Follow-up questions
- →What does
fieldbind to in a property that already has a hand-written backing field? - →Why did the language team choose a contextual keyword rather than a brand-new syntax form?
SeniorTheoryRareWhich conversions do C# 14 first-class spans add, and which direction is impossible?
Which conversions do C# 14 first-class spans add, and which direction is impossible?
C# 14 makes span conversions language-level: they work for extension receivers and type inference. Widening only — T[] → Span<T>/ReadOnlySpan<T>, Span<T> → ReadOnlySpan<T>, string → ReadOnlySpan<char>. Never back to Span<T> or T[].
Common mistakes
- ✗Assuming a span round-trips — there is no
ReadOnlySpan<T>toSpan<T>and noSpan<T>toT[] - ✗Thinking it is a library-level implicit operator rather than a language conversion
- ✗Missing that the payoff is extension receivers and generic type inference
Follow-up questions
- →Why does making the conversion a language conversion change generic type inference?
- →How would you obtain a writable
Span<T>from aReadOnlySpan<T>if you truly needed one?
SeniorTheoryRareWhat does the numeric abstraction generic math need from the library and from the language?
What does the numeric abstraction generic math need from the library and from the language?
Two halves: .NET 7's System.Numerics interfaces — notably INumber<T> — abstract the operators, and C# 11's static abstract interface members let an interface demand them. One generic method constrained on INumber<T> then sums any numeric T.
Common mistakes
- ✗Conflating the halves —
INumber<T>is .NET 7,static abstractmembers are C# 11 - ✗Thinking operator dispatch happens at run time rather than being resolved per type argument
- ✗Believing it needs a special operator constraint rather than an interface constraint
Follow-up questions
- →Why does a
static abstractmember work only through a generic type parameter, not an interface reference? - →How does the JIT keep a generic math method fast when
Tis a value type?
SeniorTheoryRareWhy does a compile-time source generator beat reflection for JSON under ahead-of-time AOT compilation?
Why does a compile-time source generator beat reflection for JSON under ahead-of-time AOT compilation?
A source generator runs inside the compiler and emits ordinary C#: the serializer's code is real, analysable source with no start-up metadata build. Reflection finds types at run time, so the trimmer cannot see usage and AOT has nothing to compile.
Common mistakes
- ✗Thinking a source generator runs at start-up or post-build rather than inside the compiler
- ✗Believing reflection survives trimming and AOT without annotations or rooting
- ✗Assuming the only benefit is speed, missing the static analysability that AOT requires
Follow-up questions
- →What does a
JsonSerializerContextchange about how the serializer resolves a type? - →Which uses of reflection can a source generator not replace?