2

So we can have nullable primitive types

bool? x = null;

Which is great. But my understanding has always been that it's not actually a null reference since

x.HasValue; //false

is possible.

I had always assumed that nullables were classes or structures with operator and Equals overloading implemented but today I checked

Object.ReferenceEquals(null, x); //true

And I have no clue how that can be.

How can it at the same time be a reference that points to null and have non-extension properties on it that are invokable?!

If this is a compiler trick, does anyone know documentation on what exactly it does?

George Mauer
  • 117,483
  • 131
  • 382
  • 612
  • 1
    `null` nullable values are boxed to null references. – Lee Mar 14 '18 at 19:22
  • Well hold on, if we're boxed to an `Object` then sure, I get how you can be a `null` reference but how on earth then can you `x.HasValue` on a `null`? Unless the compiler is doing some really crazy stuff with what it emits I don't get it. And if it does do crazy stuff, then what exactly does it do? Do we have an authoritative source on this? – George Mauer Mar 14 '18 at 19:24
  • 3
    `Nullable` is a struct, and `x.HasValue` doesn't involve boxing and just accesses the `HasValue` field directly. When calling `object.ReferenceEquals`, `x` is boxed since both parameters are `object` which is a reference type. – Lee Mar 14 '18 at 19:26
  • Interesting, so if I might restate what you are saying @Lee, is it that `bool?` doesn't actually inherit `Object` (in the same way that `bool` does not), so when you pass that into `ReferenceEquals` (which takes `object`) the compiler produces a boxed version with something like `(object)x`? And if that's the case, is there then an explicit conversion of booleans to objects that will produce `null`? – George Mauer Mar 14 '18 at 19:29
  • 2
    Both `bool` and `bool?` are structs but they do inherit from `object`. Assigning a struct to a variable of type `object` causes boxing - `bool` gets boxed to a non-null value containing the bool. `bool?` values get boxed to `null` if `HasValue` is false or a boxed `bool` if `HasValue` is `true`. See section 4.3.1 'boxing conversions` of the specification for the details. – Lee Mar 14 '18 at 19:32
  • 2
    @GeorgeMauer: The source of your confusion is a lack of clarity on what "inherits from" means. **Inherits from means that some members of the base type are also members of the derived type**. That is **all** that it means. `Object` has a method `ToString`. It is an inheritable member. Therefore any type that inherits from `Object` also has a member `ToString`. Inheritance also implies a convertibility relationship, but **that convertibility relationship need not be representation preserving**. Indeed, any boxing conversion is not representation preserving. – Eric Lippert Mar 14 '18 at 20:03
  • 1
    So let's sum up. We have a `struct Nullable { T value; bool hasValue; }` and some public helper functions that I'm sure you can see how they work. It's just a perfectly ordinary struct. An `int?` is literally nothing more than either a pair of some integer and true, or zero and false. The latter we call "null". – Eric Lippert Mar 14 '18 at 20:06
  • @EricLippert Doesn't it mean that *all* of the members of the base type are members of the derived type, not (just) that some of the members of the base type are members of the derived type. Are there any non-inheritable members in C#? – Servy Mar 14 '18 at 20:07
  • Then we have compiler magic. `T?` is a synonym for `Nullable`. `x != null` is a synonym for `x.HasValue`. `x + 1` is a synonym for `x.HasValue ? new Nullable(x.Value + 1) : new Nullable()` And so on. See my blog series on how nullable arithmetic is optimized if you want the details of that compiler magic. – Eric Lippert Mar 14 '18 at 20:07
  • Thanks Eric, authoritative as always. So can you confirm my understanding that what happens when you do `ReferenceEquals` is that the `Nullable` is boxed to an `Object` which *can* be a null reference? – George Mauer Mar 14 '18 at 20:08
  • @Servy: constructors and destructors are members but are not heritable. *Private* members *are* heritable, which many people forget. The reason you (normally) can't call a private member from a derived class is because that private member is not *accessible* from the derived class, not because it was not inherited. – Eric Lippert Mar 14 '18 at 20:09
  • Finally we have runtime magic. `Nullable` is the only value type which (1) does not meet the `where T : struct` constraint on a generic, and (2) which is boxed to either a normal boxed `T`, or a null reference. – Eric Lippert Mar 14 '18 at 20:09
  • 2
    So yes, `ReferenceEquals()` takes two `object` arguments and therefore they are boxed. Now, exercise: what happens when you call `x.ToString()` on an `int?` vs what happens when you call `x.GetType()`? Can you predict the behaviour? Can you explain the behaviour that you observe? – Eric Lippert Mar 14 '18 at 20:12
  • 1
    Woah....`GetType` throws a NullRef but `ToString` works? We are getting wild here... – George Mauer Mar 14 '18 at 20:13
  • Yep. Now figure out why. – Eric Lippert Mar 14 '18 at 20:14
  • (Creates another SO and @s Eric). No ok, I thing I see. `ToString` is implemented directly on `Nullable<>` which is not a null reference so yay, it works. But `GetType` is on `Object` so it gets boxed and you get `null` resulting in an exception – George Mauer Mar 14 '18 at 20:17
  • 5
    Correct. More specifically: `ToString()` is a *virtual method on `object` which is overridden by `Nullable`*. So when you call `x.ToString()` you can think about that as being actually realized as `Nullable.ToString(ref x)`, where the parameter is the "this" that is given to `ToString`, and therefore the implementation of `ToString` operates on the *not boxed* value. But `GetType` is *not virtual*, and therefore you can think of this as `object.GetType((object)x)`, because again, we need a `this` that is `object`. So there's boxing, and hey, it boxes to null! – Eric Lippert Mar 14 '18 at 20:20

0 Answers0