15

I was just looking at this answer, which contains the code for Nullable<T> from .NET Reflector, and I noticed two things:

  1. An explicit conversion is required when going from Nullable<T> to T.
  2. The == operator is not defined.

Given these two facts, it surprises me that this compiles:

int? value = 10;
Assert.IsTrue(value == 10);

With the code value == 10, either value is being magically converted to an int (hence allowing int's == operator to be used, or the == operator is being magically defined for Nullable<int>. (Or, I presume less likely, Reflector is leaving out some of the code.)

I would expect to have to do one of the following:

Assert.IsTrue((value.Equals(10)); // works because Equals *is* defined
Assert.IsTrue(value.Value == 10); // works because == is defined for int
Assert.IsTrue((int?)value == 10); // works because of the explicit conversion

These of course work, but == also works, and that's the part I don't get.

The reason I noticed this and am asking this question is that I'm trying to write a struct that works somewhat similarly to Nullable<T>. I began with the Reflector code linked above, and just made some very minor modifications. Unfortunately, my CustomNullable<T> doesn't work the same way. I am not able to do Assert.IsTrue(value == 10). I get "Operator == cannot be applied to operands of type CustomNullable<int> and int".

Now, no matter how minor the modification, I would not expect to be able to do...

CustomNullable<T> value = null;

...because I understand that there is some compiler magic behind Nullable<T> that allows values to be set to null even though Nullable<T> is a struct, but I would expect I should be able to mimic all the other behaviors of Nullable<T> if my code is written (almost) identically.

Can anyone shed light on how the various operators of Nullable<T> work when they appear not to be defined?

Community
  • 1
  • 1
devuxer
  • 41,681
  • 47
  • 180
  • 292
  • Perhaps the Nullable class is override the == operator. Perhaps that is what is going on? – Malk Jan 26 '12 at 01:41
  • 1
    Good question. Now ask yourself this question: why can you add an int and a nullable int and get a nullable int? The Nullable class does not define an addition operator. – Eric Lippert Jan 26 '12 at 01:55
  • @Eric, I was going to experiment with the other operators, but I figured that I'd start by posting my findings about `==`. In any case, it seems to be that `Nullable` is a "privileged" struct that the compiler treats differently than any struct I would write myself. I already knew about the magic that allows you to set a nullable to `null`, but I guess there is yet more magic. Am I on the right track? – devuxer Jan 26 '12 at 01:59
  • 2
    @DanM: Nullable is *nothing but magic*. See my answer for details. My recommendation for you is to thoroughly acquaint yourself with all the rules for operator overloading and nullable lifting before you attempt to emulate them; the specification makes fascinating reading. – Eric Lippert Jan 26 '12 at 02:06
  • Shouldn't the third line of the expecting working code be `Assert.IsTrue((int)value == 10);` instead of `Assert.IsTrue((int?)value == 10);`? Using `(int?)` is the surprise, as stated beforehand, not an expectation. – vyrp Mar 24 '17 at 04:24

5 Answers5

35

Given these two facts, it surprises me that this compiles

Given only those two facts, that is surprising.

Here's a third fact: in C#, most operators are 'lifted to nullable'.

By "lifted to nullable", I mean that if you say:

int? x = 1;
int? y = 2;
int? z = x + y;

then you get the semantics of "if either x or y is null then z is null. If both are not null then add their values, convert to nullable, and assign the result to z."

The same goes for equality, though equality is a bit weird because in C#, equality is still only two-valued. To be properly lifted, equality ought to be three-valued: x == y should be null if either x or y is null, and true or false if x and y are both non-null. That's how it works in VB, but not in C#.

I would expect I should be able to mimic all the other behaviors of Nullable<T> if my code is written (almost) identically.

You are going to have to learn to live with disappointment because your expectation is completely out of line with reality. Nullable<T> is a very special type and its magical properties are embedded deeply within the C# language and the runtime. For example:

  • C# automatically lifts operators to nullable. There's no way to say "automatically lift operators to MyNullable". You can get pretty close by writing your own user-defined operators though.

  • C# has special rules for null literals -- you can assign them to nullable variables, and compare them to nullable values, and the compiler generates special code for them.

  • The boxing semantics of nullables are deeply weird and baked into the runtime. There is no way to emulate them.

  • Nullable semantics for the is, as and coalescing operators are baked in to the language.

  • Nullables do not satisfy the struct constraint. There is no way to emulate that.

  • And so on.

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Thanks for your detailed explanation. It's great to have an insider answering our questions. I figured I was going to need to write my own operator overloads, but it's helpful to understand why I'm not getting (nearly) equivalent results from (nearly) equivalent code. – devuxer Jan 26 '12 at 02:22
  • Could an assignment to a Nullable be accidentally atomic depending on the C#/.NET versions and target architecture? I wrote a little WPF program in C# 5.0, Visual Studio 2013, .NET 4.5.2, targeting x64, to try to demonstrate tearing on regular ulong and Nullable. I had no problem getting tearing with the regular ulong in a very short amount of time, but so far I have had no luck getting tearing with a Nullable. Is it possible that the CLR is magically making the assignment atomic? – Kal Jul 16 '15 at 02:20
  • @Eric When you said "C# automatically lifts operators to nullable. There's no way to say "automatically lift operators to MyNullable". You can get pretty close by writing your own user-defined operators though.", would you consider it to be a good or bad practice to define operators accepting nullable for structs? I.e. have both public static bool operator <(MyStruct left, MyStruct right) => ... AND public static bool operator <(MyStruct? left, MyStruct? right) => .. – Jarek Kardas Jan 11 '18 at 09:17
  • 1
    @JarekKardas: I think you misunderstood me. When you provide a user-defined `<` on your struct `S`, you get a lifted one provided by the compiler for free, but *only* lifted to `S?` You don't get `Task operator <(Task x, Task y)`, or `IEnumerable operator < (IEnumerable, IEnumerable)` or `MyFavouriteMonad operator < (MyFavouriteMonad, MyFavouriteMonad)`. Nullable is special in C#. – Eric Lippert Jan 11 '18 at 14:25
4

Well, if you can use reflector why don't you compile this code:

int? value = 10;
Console.WriteLine(value == 10);

and then open it in reflector? You'll see this (make sure to select 'None' as .net version to decompile to):

int? value;
int? CS$0$0000;
&value = new int?(10);
CS$0$0000 = value;
Console.WriteLine((&CS$0$0000.GetValueOrDefault() != 10) ? 0 : &CS$0$0000.HasValue);

So basically the compiler does the heavy lifting for you. It understands what '==' operation means when used with nullables and compiles the necessary checks accordingly.

Andrew Savinykh
  • 25,351
  • 17
  • 103
  • 158
  • because I don't have Reflector...I was getting the reflected code from a different SO answer, as I mentioned at the beginning of my question. – devuxer Jan 26 '12 at 01:46
  • This shows how the compiler works, but not how the language works. – roim Mar 24 '17 at 01:21
1

Because the compiler converts Nullable<T> to T and then performs the comparison.

Arnold Zokas
  • 8,306
  • 6
  • 50
  • 76
1

Nullable<T> has this method:

public static implicit operator T?(T value)
{
  return new T?(value);
}

It looks like there is an implicit conversion from it 10 to Nullable<int> 10

To make == / != work for you. You can add

public static bool operator ==(MyNullable<T> left, MyNullable<T> right) {}
// together with:
public static implicit operator MyNullable<T>(T value) {}

should give you support for myNullable == 10 operation

THX-1138
  • 21,316
  • 26
  • 96
  • 160
  • 1
    Yes, but as I said in my question, the `==` operator is not defined for `Nullable`, so if you have two `Nullable`s, you shouldn't be able to do `==`. – devuxer Jan 26 '12 at 01:54
  • Right. I included `operator ==` just to show how you can do it. – THX-1138 Jan 26 '12 at 04:02
1

This is language dependent. C# and Visual Basic emit different code when dealing with operators on nullable value types. To udnerstand it you need to look at the actual IL code.

Jonathan Allen
  • 68,373
  • 70
  • 259
  • 447
  • You're right, but I think in this case, the issue is that the compiler is doing some special stuff for `Nullable` that it's not doing for my code. – devuxer Jan 26 '12 at 01:56