What is the behaviour of the '==' operator for a generic type value and the 'default' keyword?

Viewed 535

Part 1 of the question: In the following code why does value == default compile fine but the other alternatives do not?

bool MyEqual<T>(T value)
{
    T value2 = default;
    if (value == value2)     // Error: Operator '==' cannot be applied to operands of type 'T' and 'T'
        return true;
    if (value == default(T)) // Error: Operator '==' cannot be applied to operands of type 'T' and 'T'
        return true;
    if (value == default)    // No error
        return true;
    return false;
}

Part 2 of the question: In the following code, why do the first three prints show false and the other three show true?

bool MyEqual<T>(T value)
{
    if (value == default)
        return true;
    return false;
}

Console.WriteLine($"{MyEqual<int>(0)}");                  // False
Console.WriteLine($"{MyEqual<int>(default)}");            // False
Console.WriteLine($"{MyEqual<int>(default(int))}");       // False

Console.WriteLine($"{MyEqual<string>(null)}");            // True
Console.WriteLine($"{MyEqual<string>(default)}");         // True
Console.WriteLine($"{MyEqual<string>(default(string))}"); // True

To sum it up: What is the behaviour of the expression value == default?

EDIT: Please do not mark it as a duplicate of this other question, because that one addresses a different case, value == default(T) and not value == default: What does default(object); do in C#? Also, my question is regarding the odd behaviour when using the '==' operator as I explain above.

1 Answers

In the context of == where the operand type is a generic parameter, value == default seems to emit equivalent IL to value == null, which always evaluates to false for a non-nullable value type operand.

Given:

static bool IsDefault<T>(T value) => value == default;
static bool IsNull<T>(T value) => value == null;

We get the IL:

.method private hidebysig static 
    bool IsDefault<T> (
        !!T 'value'
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: box !!T
    IL_0006: ldnull
    IL_0007: ceq
    IL_0009: ret
} // end of method C::IsDefault

.method private hidebysig static 
    bool IsNull<T> (
        !!T 'value'
    ) cil managed 
{
    // Method begins at RVA 0x205b
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: box !!T
    IL_0006: ldnull
    IL_0007: ceq
    IL_0009: ret
} // end of method C::IsNull

You could be forgiven for finding this surprising. It means, for example, that when T is bound to a non-nullable value type like int, the expression value == default evaluates to false for a value of 0. This contrasts with the inlined expression 0 == default, which evaluates to true.

Console.WriteLine(IsDefault<int>(0));     // False
Console.WriteLine(IsNull<int>(0));        // False
Console.WriteLine(IsDefault<int?>(null)); // True
Console.WriteLine(IsNull<int?>(null));    // True
Console.WriteLine(IsDefault<int?>(0));    // False
Console.WriteLine(IsNull<int?>(0));       // False

So, clearly, for a value of an unconstrained generic parameter type, the expressions value == default and value == default(T) are not equivalent. If legal, the latter would (presumably) evaluate to true if the value were null, false, or a "zeroed-out" value type (e.g., a value type where all constituent values are also defaults).

As to why value == default(T) does not compile, the answer is simple: the compiler does not know how to evaluate == for a type that is not known at compile time. If you were to add the constraint where T : class, then the compiler could at least perform a reference comparison. But as long as T could be a primitive type, custom value type, or reference type, the compiler doesn't know how to emit the comparison. The correct implementation for a given instantiation of T might be a built-in primitive comparison, an op_Equality overload, or a reference comparison. More importantly, it may not be a supported operator at all. It's that last possibility that really poses a problem.

While the C#/.NET engineers could have come up with a way to defer figuring out the correct comparison until runtime, that would also mean trading a compile-time error for a runtime exception in cases where the operator is simply not applicable, and I don't find that trade very appealing

Related