C# ‘is null’ vs ‘== null’

Explaining the difference between pattern matching or equality comparison to null

When C# 7.0 was officially released in March 2017 it introduced several new features to make the life of developers easier, like tuples and deconstruction, local functions, expression body definitions and, the focus of this article, pattern matching.

One of the advantages of pattern matching is a more concise syntax for testing expressions and taking actions when there’s a match, increasing the readability and correctness of your code.

Checking for nulls is one of the most common usages. Before C# 7.0, if a developer wanted to check if a given value was null, usually the equality comparison would be used.

1
2
3
4
5
6
7
8
9
public static class PersonExtensions
{
public static string FullName(this Person person)
{
if (person == null) throw new ArgumentNullException(nameof(person));

return $"{person.GivenName} {person.Surname}";
}
}

With pattern matching, the extension method would be using the is operator and the null constant to check if the person variable is null.

1
2
3
4
5
6
7
8
9
public static class PersonExtensions
{
public static string FullName(this Person person)
{
if (person is null) throw new ArgumentNullException(nameof(person));

return $"{person.GivenName} {person.Surname}";
}
}

The change is so small that you are probably questioning if changing the way you code is really worthy for such a small readability improvement, to have the same results?

And you are right in thinking that way since that’s probably true for 99% of use cases (if not 99.999%) but what if you really want to be sure it works every time?

You see, reference types can overload the == operator and, if the developer decides to always return false when comparing to null, the first example would throw a NullReferenceException.

Let’s test that by overloading the == operator to always return false, whatever the parameters received:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person
{
public string GivenName { get; set; }

public string Surname { get; set; }

public static bool operator ==(Person left, Person right) => false;

public static bool operator !=(Person left, Person right) => !(left == right);
}

public static class PersonExtensions
{
public static string FullName(this Person person)
{
if (person == null) throw new ArgumentNullException(nameof(person));

return $"{person.GivenName} {person.Surname}";
}
}

If we now call the extension method FullName with a null argument a NullReferenceException will be thrown instead of the expected ArgumentNullException. This happens because the compiler knows there’s an equality overload, calls it and gets false, so if won’t enter the if statement.

If you look at the generated IL Code, you’ll see the Person equality operator being called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// [36 5 - 36 6]
IL_0000: nop

// [37 9 - 37 28]
IL_0001: ldarg.0 // person
IL_0002: ldnull
IL_0003: call bool Person::op_Equality(class Person, class Person)
IL_0008: stloc.0 // V_0

// [37 29 - 37 77]
IL_0009: ldloc.0 // V_0
IL_000a: brfalse.s IL_0017
IL_000c: ldstr "person"
IL_0011: newobj instance void [System.Runtime]System.ArgumentNullException::.ctor(string)
IL_0016: throw
IL_0017: ldarg.0 // person
IL_0018: callvirt instance string Person::get_GivenName()
IL_001d: ldstr " "
IL_0022: ldarg.0 // person
IL_0023: callvirt instance string Person::get_Surname()
IL_0028: call string [System.Runtime]System.String::Concat(string, string, string)

// [40 5 - 40 6]
IL_002d: stloc.1 // V_1
IL_002e: br.s IL_0030
IL_0030: ldloc.1 // V_1
IL_0031: ret

Before C# 7.0 if you wanted to be sure the == operator overload was ignored, you had to use object.ReferenceEquals to compare the object reference to null.

1
2
3
4
5
6
7
8
9
public static class PersonExtensions
{
public static string FullName(this Person person)
{
if (ReferenceEquals(person, null)) throw new ArgumentNullException(nameof(person));

return $"{person.GivenName} {person.Surname}";
}
}

But when using pattern matching the compiler always generates code that compares to null, ignoring any == operator overload. This means you have the same behavior as using the object.ReferenceEquals method without writing so much code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person
{
public string GivenName { get; set; }

public string Surname { get; set; }

public static bool operator ==(Person left, Person right) => false;

public static bool operator !=(Person left, Person right) => !(left == right);
}

public static class PersonExtensions
{
public static string FullName(this Person person)
{
if (person is null) throw new ArgumentNullException(nameof(person));

return $"{person.GivenName} {person.Surname}";
}
}

This ensures the method FullName always throws ArgumentNullException when a null reference is received.

Looking at the generated IL Code, we can see the person reference is being compared to null using the ceq instruction (this is exactly the same code if object.ReferenceEquals was being used — it would be inlined).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// [36 5 - 36 6]
IL_0000: nop

// [37 9 - 37 43]
IL_0001: ldarg.0 // person
IL_0002: ldnull
IL_0003: ceq
IL_0005: stloc.0 // V_0

IL_0006: ldloc.0 // V_0
IL_0007: brfalse.s IL_0014

// [37 44 - 37 92]
IL_0009: ldstr "person"
IL_000e: newobj instance void [System.Runtime]System.ArgumentNullException::.ctor(string)
IL_0013: throw

// [39 9 - 39 55]
IL_0014: ldarg.0 // person
IL_0015: callvirt instance string Person::get_GivenName()
IL_001a: ldstr " "
IL_001f: ldarg.0 // person
IL_0020: callvirt instance string Person::get_Surname()
IL_0025: call string [System.Runtime]System.String::Concat(string, string, string)
IL_002a: stloc.1 // V_1
IL_002b: br.s IL_002d

// [40 5 - 40 6]
IL_002d: ldloc.1 // V_1
IL_002e: ret

Conclusion

In this article I explained why using pattern matching for null checks is preferred to using the equality operator.

Not only it’s a small readability improvement but it also ensures the object reference is always compared to null even if the class has overloaded the == operator (same behavior as using the object.ReferenceEquals method but without writing so much code).

Even if the equality operator overload is properly implemented, we can also consider this a small performance increase because executing the ceq operation is faster that a call to the class equality operator.

As stated before, this may not affect 99% of your use cases, but for such small a small change in the way you code, why not?