Introduction
Recently nullable reference types have become trendy. Meanwhile, the good old nullable value types are still here and actively used. How well do you remember the nuances of working with them? Let's jog your memory or test your knowledge by reading this article. Examples of C# and IL code, references to the CLI specification, and CoreCLR code are provided. Let's start with an interesting case.

Note
If you are interested in nullable reference types, you can read several articles by my colleagues: "Nullable Reference types in C# 8.0 and static analysis", "Nullable Reference will not protect you, and here is the proof".
Take a look at the sample code below and answer what the output to the console will be, and just as importantly, why. Let's agree right away that you will answer as it is: without compiler hints, documentation, reading literature, or anything like that. :)
- static void NullableTest()
- {
- int? a = null;
- object aObj = a;
- object bObj = b;
- int? b = new int?();
- Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // True or False?
- }

Well, let's do some thinking. Let's take a few main lines of thought that I think may arise.
Assume that int? is a reference type.
Let's reason, that int? is a reference type. In this case, null will be stored in a, and it will also be stored in aObj after assignment. A reference to an object will be stored in b. It will also be stored in bObj after the assignment. As a result, the Object.ReferenceEquals will take null and a non-null reference to the object as arguments, so...
There's no need to say that the answer is False!
Assume that int? is a value type.
Or maybe you doubt that int? is a reference type? And you are sure of this, despite the int? a = null expression? Well, let's go from the other side and start from the fact that int? is a value type.
In this case, the expression int? a = null looks a bit strange, but let's assume that C# got some extra syntactic sugar. Turns out, a stores an object. So does b. When initializing aObj and bObj variables, objects stored in a and b will be boxed, resulting in different references being stored in aObj and bObj. So, in the end, Object.ReferenceEquals takes references to different objects as arguments, therefore...
There's no need to say that the answer is False!
We assume that here we use Nullable<T>.
Let's say you didn't like the options above. Because you know perfectly well that there is no int?, but there is a value type Nullable<T>, and in this case, Nullable<int> will be used. You also realize that a and b will actually have the same objects. With that, you remember that storing values in aObj and bObj will result in boxing. At long last, we'll get references to different objects. Since Object.ReferenceEquals has references to different objects...
There's no need to say that the answer is False!
4. ;)
For those who started from value types - if a suspicion crept into your mind about comparing links, you can view the documentation for Object.ReferenceEquals at docs.microsoft.com. In particular, it also touches on the topic of value types and boxing/unboxing. Except for the fact that it describes the case, when instances of value types are passed directly to the method, whereas we made the boxing separately, but the main point is the same.
When comparing value types, if objA and objB are value types, they are boxed before they are passed to the ReferenceEquals method. This means that if both objA and objB represent the same instance of a value type, the ReferenceEquals method nevertheless returns false, as the following example shows.
Here we could have ended the article, but the thing is that... the correct answer is True.
Well, let's figure it out.
Investigation
There are two ways - simple and interesting.
Simple way
int? is Nullable<int>. Open documentation on Nullable<T>, where we look at the section "Boxing and Unboxing". Well, that's all, see the behavior description. But if you want more details, welcome to the interesting path. ;)
Interesting way
There won't be enough documentation on this path. It describes the behavior, but does not answer the question 'why'?
What is int? and null in the given context? Why does it work like this? Are there different commands used in the IL code or not? Is behavior different at the CLR level? Is it another kind of magic?
Let's start by analyzing the int? entity to recall the basics, and gradually get to the initial case analysis. Since C# is a rather "sugary" language, we will sometimes refer to the IL code to get to the bottom of things (yes, C# documentation is not our cup of tea today).
int?, Nullable<T>
Here we will look at the basics of nullable value types in general: what they are, what they are compiled into in IL, etc. The answer to the question from the case at the very beginning of the article is discussed in the next section.
Let's look at the following code fragment:
- int? aVal = null;
- int? bVal = new int?();
- Nullable<int> cVal = null;
- Nullable<int> dVal = new Nullable<int>();
Although the initialization of these variables looks different in C#, the same IL code will be generated for all of them.
- .locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
- valuetype [System.Runtime]System.Nullable`1<int32> V_1,
- valuetype [System.Runtime]System.Nullable`1<int32> V_2,
- valuetype [System.Runtime]System.Nullable`1<int32> V_3)
- // aVal
- ldloca.s V_0
- initobj valuetype [System.Runtime]System.Nullable`1<int32>
- // bVal
- ldloca.s V_1
- initobj valuetype [System.Runtime]System.Nullable`1<int32>
- // cVal
- ldloca.s V_2
- initobj valuetype [System.Runtime]System.Nullable`1<int32>
- // dVal
- ldloca.s V_3
- initobj valuetype [System.Runtime]System.Nullable`1<int32>
As you can see, in C# everything is heartily flavored with syntactic sugar for our greater good. But in fact:
- int? is a value type.
- int? is the same as Nullable<int>. The IL code works with Nullable<int32>.
- int? aVal = null is the same as Nullable<int> aVal =new Nullable<int>(). In IL, this is compiled to an initobj instruction that performs default initialization by the loaded address.
Let's consider this code:
- int? aVal = 62;
We're done with the default initialization - we saw the related IL code above. What happens here when we want to initialize aVal with the value 62?
Look at the IL code:
- .locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0)
- ldloca.s V_1
- ldc.i4.s 62
- call instance void valuetype
- [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
Again, nothing complicated - the aVal address pushes onto the evaluation stack, as well as the value 62. After the constructor with the signature Nullable<T>(T) is called. In other words, the following two statements will be completely identical:
- int? aVal = 62;
- Nullable<int> bVal = new Nullable<int>(62);
You can also see this after checking out the IL code again:
- // int? aVal;
- // Nullable<int> bVal;
- .locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
- valuetype [System.Runtime]System.Nullable`1<int32> V_1)
- // aVal = 62
- ldloca.s V_0
- ldc.i4.s 62
- call instance void valuetype
- [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
- // bVal = new Nullable<int>(62)
- ldloca.s V_1
- ldc.i4.s 62
- call instance void valuetype
- [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
And what about the checks? What does this code represent?
- bool IsDefault(int? value) => value == null;
That's right, for better understanding, we will again refer to the corresponding IL code.
- .method private hidebysig instance bool
- IsDefault(valuetype [System.Runtime]System.Nullable`1<int32> 'value')
- cil managed
- {
- .maxstack 8
- ldarga.s 'value'
- call instance bool valuetype
- [System.Runtime]System.Nullable`1<int32>::get_HasValue()
- ldc.i4.0
- ceq
- ret
- }
As you may have guessed, there is actually no null - all that happens is accessing the Nullable<T>.HasValue property. In other words, the same logic in C# can be written more explicitly in terms of the entities used, as follows:
- bool IsDefaultVerbose(Nullable<int> value) => !value.HasValue;
IL code

Join the conversation! Your thoughts help the community grow.