30

Consider the following code:

int? x = null;
Console.Write ("Hashcode: ");
Console.WriteLine(x.GetHashCode());
Console.Write("Type: ");
Console.WriteLine(x.GetType());

When executed, it writes that Hashcode is 0, but fails with NullReferenceException in attempt to determine type of x. I know that methods called on nullable types are actually called on underlying values, so I expected program to fail during x.GetHashCode().

So, what is the fundamental difference between those two methods and why doesn't the first of them fail?

  • Only difference I can find, is that `GetHashCode` is `virtual`... – Bart Friederichs May 02 '18 at 06:34
  • 1
    [ILSpy](https://github.com/icsharpcode/ILSpy) is a handy little tool to help answer these sorts of questions. – Sam Axe May 02 '18 at 06:36
  • 3
    I find it odd that `GetType()` from a `Nullable` returns `System.Int32`, and not `System.Nullable`. – Lasse V. Karlsen May 02 '18 at 06:36
  • 1
    Also worth noting `int? x = null` is syntactic sugar for `Nullable x = new Nullable(null)`. So, `x` is an actual object, not a null reference. – Bart Friederichs May 02 '18 at 06:46
  • Reference source - https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/nullable.cs - does not in any way show GetType being handled, nor does the documentation detail this - https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1?view=netframework-4.7.2 – Lasse V. Karlsen May 02 '18 at 06:49

4 Answers4

34

This is because int? x = null; essentially creates an instance of the value type System.Nullable<int>, with an "inner" null value (you can check it via .HasVaue Property). When GetHashCode is invoked, the override Nullable<int>.GetHashCode is the method candidate (since the method is virtual), now we have an instance of Nullable<int>, and execute its instance method, perfect.

When invoking GetType, the method is non-virtual, so the instance of Nullable<int> is boxed to System.Object first, according to the document, and boxed value is null, hence the NullReferenceException.

Cheng Chen
  • 42,509
  • 16
  • 113
  • 174
  • 1
    All of this matches the behavior we see, but where is it documented? The page you linked to details how boxing behavior works for `Nullable`, but it doesn't say that `GetType()` will require the boxing operation. I'm guessing we need to find documentation for how inherited methods work on value types to find the relevant pieces. – Lasse V. Karlsen May 02 '18 at 07:16
  • 1
    @LasseVågsætherKarlsen that is because `GetType` is defined on `object` instead of `struct`, wich causes an implicit boxing. You can verify by assigning `int? x = 1` and then calling `GetType()`. The result is `System.Int32`, **not** `Nullable`. – Toxantron May 02 '18 at 07:34
  • 1
    @LasseVågsætherKarlsen You can read C# language specification section 4.3 "boxing and unboxing" for more details. – Cheng Chen May 02 '18 at 08:25
  • If I type int? x = null; and then I try to read it: x.Value then I am getting exception, not the null "value". If I check if "x" itself is null, then the answer is positive. – Eru May 08 '18 at 19:14
  • @Eru I used incorrect expressions in the answer, thanks for point that out. – Cheng Chen May 09 '18 at 03:02
  • @DannyChen Okay, but what should be then correct answer for this? – Eru May 09 '18 at 15:35
  • @Eru Correct answer for what question? – Cheng Chen May 10 '18 at 01:59
  • @DannyChen x.HasValue this is false; x.GetHashCode is 0; x.GetType returns System.NullReferenceException; x.GetValueOrDefault() also is 0; So what else can I do with it? – Eru May 10 '18 at 08:11
  • @Eru I don't quite understand, let's move the discussion to [this chat room](https://chat.stackoverflow.com/rooms/170821/question-50128422), and please describe your questions again there. – Cheng Chen May 11 '18 at 02:24
17

To clarify Danny Chen's correct answer:

  • Nullable<T> is a value type. The value type consists of a bool, which indicates nullity (false means null) and a T, the value.
  • Unlike all other value types, nullable types do not box to a boxed Nullable<T>. They box to either a boxed T or a null reference.
  • A method implemented by a value type S is implemented as though it has an invisible ref S argument; that is how this is passed.
  • A method implemented by a reference type C is implemented as if there was an invisible C argument; that is how this is passed.
  • The interesting case is then a virtual method defined in a reference base class and overridden by a struct that inherits from the base class.

Now you have enough information to deduce what happens. GetHashCode is virtual and overridden by Nullable<T> so when you call it, you call it as though there was an invisible ref Nullable<T> argument for this. No boxing happens.

GetType is not virtual and so cannot be overridden and is defined on object. Therefore it expects an object for this, When called on a Nullable<T> the receiver must be boxed, and therefore can box to null, and therefore can throw.

If you called ((object)x).GetHashCode() then you'd see an exception.

Nisarg Shah
  • 14,151
  • 6
  • 34
  • 55
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • The third and fourth bullet points about the invisible `ref S` and `C` are a bit unclear. Can you expand on them? – Nisarg Shah May 02 '18 at 11:26
  • 3
    @Nisarg: Just that when you have `class C { void M(int x) { ... } }` and you have `C c = new C(); c.M(123);` then somehow `c` has to become `this` inside `M`. The way that happens is the call is logically the same as if you had `class C { static void M(C _this, int x) { ... }} ` and a call `C.M(c, 123)`. `this` is logically *just another argument*. Similarly for structs, except for structs `this` is a `ref` alias to a variable. – Eric Lippert May 02 '18 at 14:26
5

The implementation of Nullable<T>.GetHashCode() is as follows:

public override int GetHashCode()
{
    if (!this.HasValue)
    {
        return 0;
    }
    return this.value.GetHashCode();
}

So, when the value is null, it will always get you 0.

x.GetType() is same as null.GetType() which will throw Object reference not set to an instance of an object

Sunil
  • 3,404
  • 10
  • 23
  • 31
1

Seems like GetHashCode has got a null check. (Used JetBrains to view defenition)

public override int GetHashCode()
{
  if (!this.hasValue)
    return 0;
  return this.value.GetHashCode();
}
Captain0
  • 2,583
  • 2
  • 28
  • 44
  • The implementation of `GetHashCode()` only comes into play once it's established that `GetHashCode()` can be called at all though. –  May 02 '18 at 20:48