GAP·MAP

Modern C# (records, patterns, nullable)

[ middle depth ]

Records and value-based equality

A record (a record class, still a reference type) or a record struct (a value type, C# 10) makes the compiler generate value-based equality, a readable ToString, and copy support. Two records are equal when their runtime types match and every field holds an equal value, so with record Person(string First, string Last) the comparison new Person("Ann", "Lee") == new Person("Ann", "Lee") is True while ReferenceEquals on the two stays False. Copy with a with expression: p with { Last = "Ng" } returns a fresh instance with one member replaced and the rest carried over. A positional record also generates a Deconstruct. Keep a plain class when you need reference identity (EF Core entities rely on it) or freely mutable state.

Pattern matching and switch expressions

Patterns test the shape of a value and pull pieces out in one step. The everyday kinds are type (o is string s), property ({ Length: > 0 }), relational (< 32), logical (>= 'a' and <= 'z'), and list ([first, .., last]). A switch expression maps an input to a value:

string label = shape switch
{
    Circle { Radius: > 100 } => "big circle",  // property + relational
    Circle => "circle",                         // type pattern, no variable
    null => "nothing",                          // constant null pattern
    _ => "other"                                // catch-all discard
};

Arms are tried top to bottom and the first match wins.

Nullable reference types are compile-time only

Turn them on with <Nullable>enable</Nullable> in the project file (or a #nullable enable directive). Now string means not-null and string? means may-be-null, and the compiler warns when you dereference a maybe-null value or store null in a non-nullable one.

Wrong "string? is a separate type that blocks nulls at runtime." Both string and string? are System.String at runtime, and the feature adds no checks, so a null dereference still throws. Right clear the warning with a real null check. The null-forgiving ! operator only silences the compiler and changes nothing at runtime.

init and required members

An init accessor lets a caller assign a property in an object initializer or constructor and never again, giving immutable data that still uses initializer syntax. A required member forces the caller to set it or the code will not compile. The two switches are independent and often combined:

public required string Name { get; init; }  // must be set, exactly once

Primary constructors

C# 12 lets any class or struct declare constructor parameters on its header, in scope through the whole body:

public class Greeter(string name)
{
    public string Hi() => $"Hi {name}";
}

On a record those parameters become public properties. On a plain class they do not; a parameter is captured as a private field only when something references it. So class Point(int x, int y) exposes no x or y.

[ senior depth ]

Record equality edge cases and when a class wins

The generated Equals compares the EqualityContract (a hidden property carrying the runtime type) and then every field, calling each field's own Equals. Two consequences bite. A reference-type member with no value equality compares by reference: two records holding different arrays with identical contents are unequal, and only records sharing the same array reference are equal. Equality also demands the same runtime type, so a Person-typed variable holding a Student is unequal to a real Person with the same fields.

A with expression copies field by field, so it is shallow, and reference members stay shared with the original. A property that caches a computed value in its initializer will carry the pre-copy value into the copy; expose derived state as an expression body (=> ...) so it recomputes after with. Records also make poor EF Core entities, which the docs call out, because change tracking leans on reference identity.

Switch exhaustiveness is a compile warning

A switch expression that misses cases still compiles; the missing coverage is CS8509, only a warning. Wrong "the build fails until every case is handled." It builds, and an uncovered input throws at runtime: SwitchExpressionException on modern .NET (InvalidOperationException on .NET Framework). Right add a _ arm when you want the switch to be total. Two related traps: list patterns (C# 11) emit no exhaustiveness warning at all, and an arm already covered by an earlier one is CS8510 "unreachable." Logical-pattern precedence catches people too. not binds first, then and, then or, so c is not >= 'a' and <= 'z' reads as (not >= 'a') and <= 'z'; write c is not (>= 'a' and <= 'z') for the range test.

Nullable reference types: the holes

Annotations are erased at runtime. string and string? are the same System.String, unlike int? (Nullable<int>), which is a distinct struct with HasValue and Value. The null-forgiving ! has no runtime effect; each use is a place the compiler can no longer protect you, which is why leaning on it reads as a smell. The analysis is local and does not trace into called methods, and several constructs slip through with no warning while still producing null: default(T) for a struct that has reference fields, and new string[3], whose elements are null until assigned.

required, init, and primary constructor gotchas

required (C# 11) and init (C# 9) are orthogonal. required forces the caller to set the member or it is a compile error; init permits a set only during construction. [SetsRequiredMembers] on a constructor turns off the required-init check for that path, so it is an assertion you take responsibility for. A type with any required member cannot satisfy the new() generic constraint. Primary constructors (C# 12) also behave by kind: a record synthesizes public properties from its parameters, while a plain class captures a parameter as a private field only when it is used beyond initializers. Declaring one on a class suppresses the implicit parameterless constructor, and every other constructor must chain to it with this(...).

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more C# / .NET

was this useful?