The default keyword and default values in C# 14: all-zero memory, default(T) and the default literal, struct and enum pitfalls, generic code that returns default, and the other meanings of the word default in C#.
Every type has a default value: the value you get from all-zero memory. That is null for reference types and nullable types, 0 for numbers, false for bool, and for structs every field zeroed. default(T) gives it explicitly and the default literal infers the type, as in CancellationToken ct = default. Generic code uses it to mean "no value", which is why FirstOrDefault returns 0 rather than null for int. The traps are that default skips struct constructors, gives enums the value 0 even if it is not a defined member, and makes some structs like ImmutableArray<T> unusable until initialised.
default gives you the zero-initialised value of a type: all bits zero. For reference types that is null; for numeric types 0; for bool false; for structs, every field set to its own default. It matters most in generics, where you do not know the type, and in optional parameters, where it is often the only legal default for structs like CancellationToken.
default(T) and the default literalint a = default(int); // 0 (C# 2 operator form)
int b = default; // 0 (C# 7.1 literal, target-typed)
string? s = default; // null
CancellationToken ct = default; // CancellationToken.None
default(T) names the type explicitly.default literal takes its type from the context (assignment target, parameter type, return type). If there is no target type (var x = default;) it does not compile.| Type | default |
Notes |
|---|---|---|
int, long, double, decimal |
0 / 0.0 / 0m |
|
bool |
false |
|
char |
'\0' (U+0000) |
Not a space |
DateTime |
0001-01-01T00:00:00 (DateTime.MinValue) |
A real, valid date — often a bug when stored |
Guid |
Guid.Empty (00000000-...) |
|
TimeSpan |
TimeSpan.Zero |
|
| Enums | The value 0, even if no member has value 0 | |
Nullable<T> |
null (HasValue == false) |
|
Reference types (string, classes, arrays, delegates) |
null |
Even if non-nullable with NRT |
| Structs | All fields defaulted, constructors not run | |
ImmutableArray<T> |
A "default" (uninitialised) array that throws on most operations | Use IsDefault / IsDefaultOrEmpty |
defaultThe CLR can always create a struct with all-zero memory — in arrays (new Money[10]), uninitialised fields, default(Money) — without calling any constructor. Since C# 10 you can declare a parameterless struct constructor and field initialisers, but they run only for new Money(), not for default(Money) or array elements. So:
public readonly struct Money(decimal amount, string currency)
{
public decimal Amount { get; } = amount;
public string Currency { get; } = currency ?? throw new ArgumentNullException(nameof(currency));
}
var m = default(Money); // Amount 0, Currency null - the validation never ran
A well-designed struct must either treat the all-zero value as valid (TimeSpan.Zero) or guard against it (IsDefault).
defaultdefault(OrderStatus) is (OrderStatus)0. If your enum starts at 1 (Pending = 1), default is an unnamed value, which shows up as 0 in JSON, databases and switch statements. Guidance: define a meaningful zero member (None/Unknown = 0), especially for [Flags] enums where None = 0 is required.
default in genericspublic static T? FirstOrNone<T>(IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (var item in source)
if (predicate(item)) return item;
return default; // null for reference types, 0 for int, zero struct for structs
}
The problem: for value types, default is indistinguishable from a real value — FirstOrDefault() on List<int> returns 0 for "not found" and for "found 0". Senior solutions:
bool TryFind(..., [MaybeNullWhen(false)] out T value).T? with a where T : struct overload that returns Nullable<T>.FirstOrDefault(predicate, defaultValue) to choose a sentinel.EqualityComparer<T>.Default.Equals(value, default) — value == default does not compile for unconstrained T.default in optional parametersOptional parameters need compile-time constants, and for most structs default is the only option:
public Task<Order?> GetAsync(int id, CancellationToken cancellationToken = default);
public void Schedule(TimeSpan delay = default); // TimeSpan.Zero
public void Log(DateTime at = default); // can't write DateTime.UtcNow here
If you need "now" or a computed default, use DateTime? at = null and resolve it inside: var when = at ?? _clock.UtcNow;.
An integration accepted JSON orders where deliveryDate was optional, but the DTO declared DateTime DeliveryDate. Missing values deserialised to 0001-01-01, which passed validation (it is a valid date) and was written to SQL Server datetime2. Downstream, a scheduling job sorted by delivery date and treated those orders as 2,000 years overdue, escalating them to the top of a warehouse queue. The fix was making the property DateOnly?, adding a validation rule rejecting default for required dates, and a database check constraint. The broader lesson: default is a value, not "missing" — use nullable types when absence is meaningful.
Checks understanding of the runtime's zero-initialisation guarantee and its design consequences.
Green flags
default bypasses struct constructors.FirstOrDefault ambiguity traps.EqualityComparer<T>.Default in generic code.Red flags
default(int) is null.default (switch label, interface methods).The CLR guarantees that newly allocated memory is zeroed: heap objects, arrays, and (by default) stack frames via the .locals init flag. Zeroed memory is a valid value for every type:
null.int/double/decimal is 0.This is what makes default(T) cheap: the JIT emits an initobj instruction (for structs) or ldnull / ldc.i4.0, often just zeroing a register or using xor reg, reg. It is also why the runtime does not need to call struct constructors for arrays — new Point[1_000_000] is simply a zeroed memory block.
default vs new() for structspublic struct Config
{
public int Retries = 3; // field initialiser (C# 10)
public Config() { } // explicit parameterless ctor
}
var a = new Config(); // Retries = 3 (ctor runs)
var b = default(Config); // Retries = 0 (no ctor)
var array = new Config[1];
var c = array[0]; // Retries = 0 (array elements are zeroed, no ctor)
Activator.CreateInstance<T>() and new T() with the new() constraint call the parameterless constructor if one exists. Rule: never rely on a struct constructor running.
C# 11 also made the compiler auto-default struct fields that are not assigned in a constructor (previously error CS0171), so constructors no longer need to assign every field.
default on a non-nullable reference type produces null with a warning (CS8625 for literal assignment, CS8603 for returns). In generic code, return default; from a method returning T (unconstrained) gives CS8603 under NRT; the fix is a T? return type or [return: MaybeNull].
static bool IsDefault<T>(T value) => EqualityComparer<T>.Default.Equals(value, default!);
value is null works for unconstrained T (always false for non-nullable value types, and the JIT removes the branch entirely).typeof(T).IsValueType checks are constant-folded per instantiation in modern JITs.default and pattern matching / switchcase default: is not valid (default in switch statements is the fallback label); to match the default value use a constant or var x when x == default._ is the discard, not default.x is default(Money) does not compile for non-constant structs; compare with Equals or check IsDefault.ImmutableArray<T> — a default-value trap in the BCLImmutableArray<T> is a struct wrapping a T[]. Its default has a null inner array, so calling Length or enumerating throws NullReferenceException or InvalidOperationException. That is why it exposes IsDefault and IsDefaultOrEmpty, and why record properties of type ImmutableArray<T> should be initialised to ImmutableArray<T>.Empty or [].
ArraySegment<T> and Memory<T> have similar but safe defaults (empty).
System.Text.Json: missing properties stay at their default unless marked required ([JsonRequired] or the C# required keyword). DefaultIgnoreCondition = WhenWritingDefault omits 0, false, null from output — which breaks clients that rely on explicit false.0, Guid.Empty) is treated as not set, so EF generates a value; this is why inserting an entity with Id = 0 creates a new identity. Sentinel configuration (HasSentinel) exists in EF Core 8+ for non-default sentinels.datetime cannot store 0001-01-01 (minimum is 1753); datetime2 can. A DateTime default can therefore either throw or silently store a nonsense date depending on column type.new(), default and an array element.FirstOrDefault() on new List<int> { 0 } and on an empty list, and design an API that tells them apart.ImmutableArray<int> a = default; and call a.Length.{} into a class with an int Quantity property and a required int Quantity property.using System.Collections.Immutable;
namespace Types;
public enum OrderStatus { Unknown = 0, Pending = 1, Shipped = 2 } // 0 is meaningful
public struct Money
{
public decimal Amount;
public string Currency = "AUD";
public Money() { } // C# 10: runs for new Money(), not for default
}
public static class Defaults
{
public static void Main()
{
Console.WriteLine(default(DateTime)); // 1/01/0001 12:00:00 AM
Console.WriteLine(default(OrderStatus)); // Unknown
Console.WriteLine(new Money().Currency); // AUD
Console.WriteLine(default(Money).Currency is null); // True: constructor skipped
Console.WriteLine(new Money[1][0].Currency is null); // True
// FirstOrDefault ambiguity on value types
int[] scores = [10, 20];
Console.WriteLine(scores.FirstOrDefault(s => s > 50)); // 0: real or missing?
Console.WriteLine(scores.FirstOrDefault(s => s > 50, -1)); // -1: explicit sentinel
// Invalid default struct
ImmutableArray<int> arr = default;
Console.WriteLine(arr.IsDefault); // True; arr.Length would throw
Console.WriteLine(IsDefault(0) && IsDefault<string?>(null) && !IsDefault("x"));
}
// Generic default comparison
public static bool IsDefault<T>(T value) => EqualityComparer<T>.Default.Equals(value, default);
public static async Task SaveAsync(string path, CancellationToken ct = default)
=> await File.WriteAllTextAsync(path, "ok", ct);
}
default as "missing"What happens: DateTime, int or Guid properties default when data is absent, and code treats 0 or 0001-01-01 as real values.
Why it is wrong: the default is a valid value, so missing data flows through validation, databases and reports.
Do instead: use nullable types (DateTime?, int?) when absence is meaningful, or required members.
What happens: a struct validates or initialises fields in its constructor and assumes every instance went through it.
Why it is wrong: default(T), arrays and uninitialised fields bypass constructors.
Do instead: make the all-zero state valid, or check IsDefault where the struct is consumed.
What happens: enum Status { Pending = 1, Paid = 2 }.
Why it is wrong: default(Status) is 0, an unnamed value that appears from uninitialised fields and deserialisation and breaks switches.
Do instead: add None = 0 / Unknown = 0, and for [Flags] enums always define None = 0.
FirstOrDefault on value types to detect "not found"What happens: var id = ids.FirstOrDefault(x => x > 100); if (id == 0) ...
Why it is wrong: 0 might be a legitimate value; the result is ambiguous.
Do instead: use Any + First, a Try-pattern, or project to nullable (ids.Cast<int?>().FirstOrDefault(...)).
==What happens: if (value == default) in an unconstrained generic method.
Why it is wrong: it does not compile for unconstrained T; casting to object compares references and boxes.
Do instead: use EqualityComparer<T>.Default.Equals(value, default).
What happens: trying DateTime at = DateTime.UtcNow or TimeSpan timeout = TimeSpan.FromSeconds(30).
Why it is wrong: optional defaults must be compile-time constants; only default is allowed for these structs.
Do instead: use a nullable parameter (TimeSpan? timeout = null) and resolve it inside the method.
ImmutableArray<T> fieldsWhat happens: a record or class has an uninitialised ImmutableArray<T> property.
Why it is wrong: most operations on a default ImmutableArray<T> throw.
Do instead: initialise to [] / ImmutableArray<T>.Empty, and check IsDefaultOrEmpty at boundaries.
What happens: DateTime.MinValue is saved to a datetime column (throws SqlTypeException) or a datetime2 column (stores year 0001).
Why it is wrong: either a runtime failure or nonsense data that breaks sorting and reporting.
Do instead: use nullable dates, validate before saving, and add database constraints.
What happens: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault is set globally.
Why it is wrong: clients stop receiving false, 0 and empty values and may treat missing as different from false.
Do instead: use WhenWritingNull globally, and per-property attributes where omission is intended.
What happens: an update is attempted with Id = 0 because a DTO lost its ID.
Why it is wrong: EF Core treats the default key as "new" and inserts a duplicate instead of updating.
Do instead: validate IDs on update endpoints and use strongly-typed IDs that cannot be accidentally defaulted.
Seniors design types so that the default value is safe, because the runtime will create it whether you like it or not.
| Type design | Default-safe approach |
|---|---|
| Enums | Unknown = 0, validate on input |
| Value objects (struct) | zero state valid, or IsDefault check |
| Optional data | T? instead of sentinels |
| Generic APIs | return T? or bool TryX(out T) instead of ambiguous default |
| Async APIs | CancellationToken ct = default as the last parameter |
default literal plus target typing keeps code short, but explicit default(T) can be clearer in generic code reviews.[SkipLocalsInit] and the zero-initialisation guarantee shows understanding of the runtime.int? is Nullable<int>, a struct that pairs a value with a HasValue flag, so a value type can represent "no value" (a nullable database column, an optional filter). Reading .Value when it is empty throws, so I use ??, GetValueOrDefault() or if (x is int v). Operators are lifted: null + 1 is null, and comparisons like null > 0 are false. Boxing is special: a null int? boxes to a null reference and a non-null one boxes to a plain boxed int. Unlike string?, int? is a real run-time type.
A value type holds its data directly, so assigning or passing it copies the data; a reference type variable holds a reference to an object on the managed heap, so copying the variable makes two references to the same object. Value types are stored inline where they are declared: a local usually sits on the stack or in a register, but a struct field of a class lives inside that object on the heap, and captured or boxed values also end up on the heap. By default classes compare by identity and structs by their fields. I use structs for small, immutable values like Money or coordinates, and classes for everything with identity or behaviour.
Last updated 30 Oct 2026.