Thursday, January 7, 2010

Comparing C# structs to null. Can you or not?

A while back I posted a blog about creating a Splash Screen in WinForms. While writing that blog, I was running into problems when calling BeginInvoke on the form, and I was getting this error:

Invoke or BeginInvoke cannot be called on a control until the window handle has been created.


Every Form has a Handle that gets assigned to it by Windows. Therefore, I figured I'd just check to see if the handle is null:



if(this.Handle == null)
{
return; //don't do anything
}


When that didn't work, I started digging in a little deeper, and found out that the Form.Handle is of type IntPtr which is actually a struct. Structs as everyone knows are value types which can't be null so therefore, the null check would always fail and therefore the return statement never got hit.(The fix for that isn't related to this post, so I won't go into detail.)

The question that came to mind though right away was, why did that compile? If you try to compare a struct to null, the compiler complains right? So just to make sure I wasn't going nuts, I quickly whipped up this code sample:



class Program
{
static void Main(string[] args)
{
MyStruct ms = new MyStruct();
if (ms == null)
{
Console.WriteLine("Can't get here.");
}
}
}

public struct MyStruct { }


And sure enough, as soon as I tried compiling this, I got this error:

Operator '==' cannot be applied to operands of type 'StructDemo.MyStruct' and ''


OK, so then how the heck can IntPtr be compared to null, but my struct can't? What's Microsoft know that I don't? So then I decided to look at the metadata for IntPtr:



[Serializable]
[ComVisible(true)]
public struct IntPtr : ISerializable
{
public static readonly IntPtr Zero;

[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public IntPtr(int value);

[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public IntPtr(long value);

[CLSCompliant(false)]
[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public IntPtr(void* value);

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static bool operator !=(IntPtr value1, IntPtr value2);

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static bool operator ==(IntPtr value1, IntPtr value2);

[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public static explicit operator IntPtr(int value);

public static explicit operator int(IntPtr value);

public static explicit operator long(IntPtr value);

[CLSCompliant(false)]
public static explicit operator void*(IntPtr value);

[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public static explicit operator IntPtr(long value);

[CLSCompliant(false)]
[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public static explicit operator IntPtr(void* value);

public static int Size { get; }

public override bool Equals(object obj);

public override int GetHashCode();

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public int ToInt32();

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public long ToInt64();

[CLSCompliant(false)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public void* ToPointer();

public override string ToString();

public string ToString(string format);
}


OK, so I decided to try and copy bit by bit to see if I can make the error go away. First I tried making it Serializable:



public struct MyStruct : ISerializable
{
#region ISerializable Members

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new NotImplementedException();
}

#endregion
}


No luck. Still won't compile. Next, I decided to add the Equals and GetHashCode overloads:



public struct MyStruct : ISerializable
{
#region ISerializable Members

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new NotImplementedException();
}

#endregion

public override int GetHashCode()
{
return base.GetHashCode();
}

public override bool Equals(object obj)
{
return base.Equals(obj);
}
}


Still no luck. Finally, this is what made the error go away:



public struct MyStruct : ISerializable
{
#region ISerializable Members

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new NotImplementedException();
}

#endregion

public override int GetHashCode()
{
return base.GetHashCode();
}

public override bool Equals(object obj)
{
return base.Equals(obj);
}

public static bool operator ==(MyStruct m1, MyStruct m2)
{
return true;
}

public static bool operator !=(MyStruct m1, MyStruct m2)
{
return false;
}
}



Huh? Overriding the equals and not equals operator? Why? Both of those methods still only take MyStruct as arguments which can't be null?! I still can't pass null to either one of those methods, so why does the compiler allow it all of a sudden?

After much digging, I finally found my answer and I'll try to do my best to explain. Since .Net 2.0 with the introduction of Nullable Types, there's now an implicit conversion between any value type and it's nullable type. Meaning, I can always do this safely:



int x = 10;
Nullable<int> y = x;


Therefore, whenever we override the == operator, the C# compiler gives us another one for free. It also allows us to compare Nullable versions of that type. Meaning, when you do this:



public static bool operator ==(MyStruct m1, MyStruct m2)
{
return true;
}

public static bool operator !=(MyStruct m1, MyStruct m2)
{
return false;
}


You're also getting this for free:



public static bool operator ==(Nullable<MyStruct> m1, Nullable<MyStruct> m2)
{
return true;
}

public static bool operator !=(Nullable<MyStruct> m1, Nullable<MyStruct> m2)
{
return false;
}


So that's why, when you compare your struct instance to null, it casts your struct instance to a nullable type (implicitly), and calls the overload which takes two nullables. Therefore, your code compiles. Of course it can never be null and the if check will always fail, but the compiler now thinks it's OK. Heh, you learn something new every day I guess...

1 comment:

DmitryT said...

Thanks, that helped. Most of my teammates have been busy for ten minutes trying to understand why "DateTime.Now > null" compiles.