C# 2.0: Generics, default(T) and compare to null weirdness


<Added additional stuff after a discussion on internal CSharp user list>


I was going through the generics spec in C#2.0 and found something really weird.


default(t)


Consider the following code.

class MyGenClass<T>

{

public void Method(ref T t)

{

t = null;

}

}


This will not compile because because T may be a value type as well and there is no implicit conversion of null to a value-type. So to handle such situation where you want to reset a type in a generic class the default-value expression was instroduced.

class MyGenClass<T>

{

public void Method(T t)

{

t = default(T);

}

}


If the type in default-value expression at run-time evaluates to be a reference type then it’ll return null, if its a value-type then it will return the value-types default value (essentially new T() which boils down to re-setting all bits to 0).


null comparison weirdness


Peculiarly though, you are allowed to compare t with null.

class MyGenClass<T>

{

public void Method(T t)

{

if (t == null)

Console.WriteLine(“null”);

// do some processing*/

}

}


I got a bit confused here, as I always thought that you cannot compare value-types to null. Then I tried the following code.

int i = 5;

if (i == null)

Console.WriteLine(“null”);


and it compiled fine, with a warning that i == null will always be false. But the weird thing is that it compiled and I just cannot figure out why does the compiler allow comparing a value type with null. However, I accepted it and thought I figured out that since value-type can be compared to null it explains why in the generic class I was able to compare t to null. Things got even weirder when I tried to put a value-type constraint on the generic class

class MyGenClass<T> where T: struct

{

public void Method(T t)

{

if (t == null)

Console.WriteLine(“null”);

// do some processing*/

}

}


Compilation failed, with the statement that == operator cannot be applied to T. Since compiler cannot validate that == operator is overloaded on T it fails. But what I cannot figure out is that even in the case where there were no constrains the compiler in no way can validate the same thing, then why did it allow the comparison with null.


Time to send an email to internal CSharp alias to figureout whats going on….



I did finally send the email to CSharp usergroup and after some discussions this is what came out.


The comparison of the form int a = 5; if (a==null) was invalid in VS 2003, but this has changed with nullable types. The int gets converted to a nullable type and the lifted operator bool operator ==(int? x, int? y); is used (see 2.0 spec 24.3.1) .


This answers why null comparison works in value types like int. Why null comparison is allowed in generic classes has a more involved reasoning. First of all there is an explicit rule in the spec which goes as


There are special rules for determining when a reference type equality operator is applicable. For an equality-expression with operands of type A and B, […] if B is the null type, […] A is a type parameter and at runtime, the type parameter is a value type, the result of the comparison is false.


This rule makes the comparison valid. However, I was not convinced about the reasoning behind the rule. Special cases like this break the flow and intuition one develops in a language. Moreover, it the above rule is valid then I’d argue the following code should also be valid

class MyGenClass<T> where T: struct

{

public void Method(T t)

{

if (t == null)

Console.WriteLine(“null”);

// do some processing*/

}

}


Here for the comparison t == null, T should have been auto-converted to its nullable counterpart T? and the comparison should have gone through (evaluated to false). I think the fact that a generic class with no constraint allows an expression and a generic class with additional constraint do not allow the same is un-intuitive.


 

Comments (25)

  1. David McCabe says:

    Doesn’t the value type get autoboxed to object, then compared with null?

    Not sure why the generic version fails, in that case, though.

  2. even if its auto-boxes what purpose can it serve? I mean an int boxed will always be non-null….

  3. James Arendt says:

    I ran into a similar problem in a piece of code recently. I had a indexer of type T where I wanted to check to see if the value provided in the setter was the default value (ex. null for reference types, 0 for ints, etc).

    I wrote something along the lines of:

    if (value == default(T)) {

    // do something

    }

    The compiler does not accept either the == or != operators for comparisons between two values of type T. The solution in my case was to put a constraint for either IEquatable<T> or IComparable<T> to perform comparisons.

  4. SOS says:

    And of course, this gem is nice and weird (I know it worked in 1.0, pretty sure it does in 2.0 as well):

    int i = (int)(object)null;

    I never really tested what i will be but it certainly is weird. Null to value type covnersion…

  5. Jesse McGrew says:

    It doesn’t autobox the int, it converts it to int?. Here’s the warning you get when you compare an int variable to null:

    warning CS0472: The result of the expression is always ‘false’ since a value of type ‘int’ is never equal to ‘null’ of type ‘int?’

    If you explicitly cast i to int?, you’ll get a different warning: the code inside the if statement is unreachable (since the condition is always false). The same IL is produced.

  6. After some digging around and emails to the CSharp devs, I have located the culprit as lifted operators due to nullable types. So

    int i = 5

    if (i == null) works due to this lifted operators.

    This will surely be my next blog topic. However, I am still not able to get why

    class MyGenClass<T>

    {

    public void Method(T t)

    {

    if (t == null)

    Console.WriteLine("null");

    }

    }

    is allowed….

  7. Jesse McGrew says:

    Or why it isn’t allowed when you add the struct constraint. Seems like it should lift T to T? just like it lifts int to int?.

    This is definitely an ugly corner of the language.

  8. Sean Chase says:

    Abhinaba, maybe I’m misunderstanding your question, but it seems like you would want to allow it to work so can do this…

    MyGenClass<int?> x = new MyGenClass<int?>();

    int? i = null;

    x.Method(ref i);

  9. Sean Chase says:

    Here’s another weird snippet that compiles…

    public struct Foo { public int Bar;}

    public struct Nullable<Foo> {}

    What’s the point of allowing the latter struct declaration?

  10. I have updated the post with more findings.

    To restate :- The whole issue is involving lifted operators and the int getting auto-converted to int?. This explains why the comparison with int works. But I absolutely agree with Jesse McGrew. The T should be auto-converted to T? so that even with struct constraint the generic class should compile.

  11. TAG says:

    Sean,

    Becouse Nullable<Foo> is same as Nullable<T>. Foo in this case is simply typename.

    Nullable in this case is new generic struct type declaration in your namespace.

  12. Tobias says:

    The int to int? lifting is a speciall case as

    given:

    struct TestStruct { }

    void Test {

    TestStruct s

    if(s == null) {

    Console.WriteLine("null");

    }

    }

    I get a compile error.

  13. twvaldez says:

    I tried this and it worked

    T obj

    default(T).Equals(obj);

  14. Jochen says:

    default(T).Equals(obj);

    DOES NOT WORK if T is a reference type since you cannot call .Equals on null…

  15. Sunny says:

    I want to do something like:

    class myObject{

    public T Fetch<T>(ICriteria oCriteria){

      T fetchResult = default(T);

      if(T == something){

        fetchResult = doSomething();

      }

     }

    }

    private something doSomething(){

     return new something();

    }

    }

    which gives me a compile time error at fetchResult = doSomething();

    Any help?

    Regards,

    Sunny

  16. jokiz says:

    "The int gets converted to a nullable type" – nope, i checked the IL and it’s not, still int32

  17. Kc Joe says:

    So If You’d Like To Check Inside A Generic Class If A Variable Eqauls To Default You Should Do:

    public bool IsDefault(TInterpretationResult res)

           {

               //Compiler Error

               //return (default(T) == res);

               var obj = default(T) as object;

               if (obj == null) //Is Not Value Type

                   return (res == null);

               if (obj.GetType().IsValueType)

               {

                   return (ValueType)obj == (ValueType)obj;

               }

               Debug.Fail("Should Not Reach Here!");

               return false;

           }

    Seems Kinda Ugly, It’s Strange That MS Didn’t Handle It In A More Elegent Way

  18. Kc Joe says:

    Oops … Fixed:

    public static bool IsDefaultValue(T res)

           {

               //Compiler Error

               //return (default(T) == res);

               var obj = default(T) as object;

               if (obj == null) //Is Not Value Type

                   return (res == null);

               if (obj.GetType().IsValueType)

               {

                   if (res == null) //res Is Not A Value Type

                       return false;

                   return res.Equals(obj);

               }

               Debug.Fail("Should Not Reach Here!");

               return false;

           }

    Still Ugly!

  19. Jonathan Delorme says:

    // The clean way 😉

    public static bool IsDefaultValue(T res)

    {

    return Object.Equals(res, null);

    }

  20. Thanks for the info!  I just ran into this exact problem.

  21. Dan says:

    If a Generic Type T is implementing an interface, it would therefore assume it was a reference type, and thus nullable? isn’t this correct.

    i.e.

    public T x<T>() where T : IMyInterface

    {

     return null;

    }

  22. hagen says:

    to avoid comparing to "null", you might use EqualityComparer<T>.Default.Equals(T x, default(T))

  23. Ari says:

    for those who might stumble upon this through search, this might help:

    here is a solution: http://www.geekality.net/…/generics-and-checking-for-null