Default Values of Generic Type Parameters in C

In C#, generic type parameters make it impossible to know in advance whether a type argument will be a value type or a reference type. This article provides an overview of how the default(T) expression and the modern default literal bridge this gap, ensuring type safety by producing the correct zero-initialized state for any given generic parameter T.

The Need for default(T)

When working with a generic parameter T, you cannot simply assign null because T might be a value type such as int, bool, or a custom struct. Value types cannot be null unless explicitly declared as nullable (T?). Similarly, you cannot assign 0 because T might be a reference type like string or a custom class.

The default(T) operator solves this problem at compile time by dynamically substituting the appropriate default value depending on what T resolves to.

How default(T) Evaluates Different Types

The value produced by default(T) depends entirely on the underlying classification of T:

  • Reference Types: If T is a class, interface, delegate, or record class, default(T) evaluates to null.
  • Numeric Value Types: If T is a primitive numeric type (int, float, double, decimal, byte, etc.), default(T) evaluates to zero (0, 0.0, etc.).
  • Boolean: If T is bool, default(T) evaluates to false.
  • Enumerations: If T is an enum, default(T) returns the underlying integral zero cast to the enum type, which corresponds to whichever enum member is assigned the value 0 (or simply 0 if no member has that value).
  • Structs (Custom Value Types): If T is a struct, default(T) allocates a struct instance where every internal field is initialized to its own default value (binary zero).
  • Nullable Value Types (Nullable<T> or T? for structs): default(T?) evaluates to a nullable instance where HasValue is false.

Syntax Evolution: The default Literal

Starting with C# 7.1, the language introduced the target-typed default literal. Where the compiler can infer the type, you can omit (T):

public class Repository<T>
{
    // C# 7.0 and earlier
    public T GetOldDefault()
    {
        return default(T);
    }

    // C# 7.1 and later
    public T GetModernDefault()
    {
        T item = default;
        return item;
    }
}

Both syntaxes produce identical Intermediate Language (IL) instructions and runtime behavior.

Checking for Default Values

Checking whether a variable of type T holds its default value requires caution. You cannot always use the == operator on an unconstrained type parameter T, because the compiler cannot guarantee that == is defined for T.

To reliably compare a generic variable against its default value, use EqualityComparer<T>.Default:

public bool IsDefault<T>(T value)
{
    return EqualityComparer<T>.Default.Equals(value, default);
}

This pattern safely handles both reference types (checking for null) and value types (checking for zero or all-zero bit patterns) without boxing overhead for built-in types.