In this article, we will discuss a feature of C# known as null-conditional operators and how it can help prevent the occurrence of null-reference exceptions, which are commonly encountered by developers.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

What Are Null-Conditional Operators in C# and Why Are They Important?

Null-conditional operators were introduced in C# 6.0 to provide a way to simplify null-checking code and avoid null-reference exceptions. They also offer a safe way to access members and elements of an object or collection that may be null.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The way they work is straightforward – if the object, or collection, is null, the operator returns null instead of throwing a NullReferenceException otherwise, they return the value.

Let’s set up our example:

public class Student
{
    public string? Name { get; set; }
    public List<string> Courses { get; set; }

    public Student(string name, List<string> courses)
    {
        Name = name;
        Courses = courses;
    }

    public void Enroll(string course)
    {
        Courses.Add(course);
    }
}

In a new file, we create a simple Student class. It has two properties – Name and Courses as well as an Enroll method.

We have things ready, let’s dive in!

Safe Navigation Operator (?.) And (?[])

In our application, if objects, or their members, can be null we have to appropriately handle them:

Student? student = null;

if (student != null)
{
    Console.WriteLine($"Student name is: {student.Name}");

    if (student.Courses != null)
    {
        Console.WriteLine($"First enrolled course is: {student.Courses[2]}");
    }
}

Here, we first check if the student variable is null before printing its Name. Then we check if the Courses property is null before printing all courses to the console. We do this to guard our application against NullReferenceExpection. For simple code, this might be fine but for complex software, this becomes very tedious and lowers code readability.

The null-conditional operator for safe member access, ?., and safe element access, ?[], were introduced to tackle this problem:

Console.WriteLine($"Student name is: {student?.Name}");
Console.WriteLine($"First enrolled course is: {student?.Courses?[2]}");

Here, we don’t need to do if checks as the ?. and ?[] operators will return null if the object, or its members, are null:

Student name is:
First enrolled course is:

We don’t get the student’s name or first course but our code compiles and runs without throwing exceptions.

The first thing to remember is that null-conditional operators are short-circuiting. This means that, if one operation in a series of member or element access operations returns null, the rest do not execute. In our example, we have student?.Courses?[2], here if the student is null, we directly return null, without evaluating the rest of the expression.

The second thing we must remember is that null-conditional operators save us only from null-reference exceptions. A null-conditional operator cannot save us from a situation where student.Name or student.Courses are not null but throw an Exception. In practice, this means that if Courses is not null but index 2 is outside its bounds, an IndexOutOfRangeException will be thrown.

Null-Coalescing Operator (??) And (??=)

In some cases, not getting an Exception might do the trick. But we also have those cases where we need a default value to be returned:

var name = string.Empty;

if (!string.IsNullOrWhiteSpace(student.Name))
{
    name = student.Name;
}
else
{
    name = "John";
}

We declare a name variable as an empty string. Then we check if the student‘s Name is null, empty, or white space. If it is not we assign it to our name variable, else we assign John to it.

Here comes the null-coalescing operator, ??:

var name = student?.Name ?? "John";

The initial ten lines of code are shortened to just one. The ?? operator will return the value on its left if it’s not null, otherwise, it returns the value on its right.

In other cases, we might want to assign a value to a variable, or a property of an object, if it’s null:

Student namelessStudent = new Student(null, null);

if (string.IsNullOrWhiteSpace(namelessStudent .Name))
{
    namelessStudent .Name = "John";
}

We check if the namelessStudent‘s Name is null, empty, or white space. If it is we assign John to it.

The null-coalescing assignment operator, ??=, can improve our code:

namelessStudent.Name ??= "John";

Here, the ??= operator will assign the value John, to namelessStudent.Name if it is null.

Null-Conditional Invocation Operator (?.())

Often our reference-type objects have methods that we need to invoke safely:

if (student != null)
{
    student.Enroll("Math");
}

Here, before we Enroll our student in Math class, we make sure that student is not null.

This type of check further bloats our code, but we can do something about it:

student?.Enroll("Math");

Like with the safe member access operator, the null-conditional invocation operator, ?.(), will only invoke the method if the object it belongs to is not null.

Thread Safety and Null-Conditional Operators

An essential concept in asynchronous programming is thread safety. Before we take a look at how null-conditional operators help with that, let’s expand our Student class:

public class Student
{
    public const int MaxNumberOfCourses = 10;

    public string? Name { get; set; }
    public List<string> Courses { get; set; }

    public event EventHandler MaxNumberOfCoursesReached;

    public Student(string name, List<string> courses)
    {
        Name = name;
        Courses = courses;
    }

    public void Enroll(string course)
    {
        Courses.Add(course);

        if (Courses.Count >= MaxNumberOfCourses)
        {
            if (MaxNumberOfCoursesReached != null)
            {
                MaxNumberOfCoursesReached.Invoke(this, EventArgs.Empty);
            } 
        }
    }       
}

We add a MaxNumberOfCourses constant and set it to 10 and an EventHandler called MaxNumberOfCoursesReached. Then we expand the Enroll method to invoke the event if the number of Courses reaches the limit and the MaxNumberOfCoursesReached is not null.

This is considered a thread-safe way of invocation due to the fact that if a different thread unsubscribes from our event and MaxNumberOfCoursesReached becomes null, it won’t be invoked and our object will remain unaffected.

Now, let’s utilize the null-conditional member access operator:

public void Enroll(string course)
{
    Courses.Add(course);

    if (Courses.Count >= MaxNumberOfCourses)
    {
        MaxNumberOfCoursesReached?.Invoke(this, EventArgs.Empty);
    }
}

Here, we remove the if statement and use ?. before invoking MaxNumberOfCoursesReached – this reduces the code length but has the same result.

Advantages of Using Null-Conditional Operators

Every new feature in C# is there for a good reason, and the null-conditional operators make no difference. They make our code more concise and less exception-prone, especially when we deal with complex object structures that may contain null values.

Avoiding Null-Reference Exceptions

The most valuable advantage of using the null-conditional operators is making our application less prone to throwing null-reference exceptions. The return of null values may still be a nuisance but it is better than having our program crash. Moreover, if null values are handled appropriately, the end-user won’t even know that something unexpected has happened.

Simplifying Code and Better Code Readability

Simplifying our code has the advantage of improving its readability but also makes it easier for others to understand. This is very important as we often work in a team environment and understanding each other’s code is vital.

Disadvantages of Using Null-Conditional Operators

In this section, we will explore the potential disadvantages of null-conditional operators. By understanding the limitations and risks associated with them, we can decide when to leverage null-conditional operators and when to use alternative approaches for handling null values.

Decreasing Readability

Using null-conditional operators can help us reduce the amount of boilerplate code we use to handle null values. On the other hand, overusing them can make our code harder to read and understand, especially for developers who are not familiar with this feature of C#. This can lead to more time spent deciphering code and an increased probability of introducing errors.

Harder Debugging

Debugging code that heavily relies on null-conditional operators can be more challenging. It can be hard to determine which operator, or expression, is causing problems. Also, if a null-conditional operator is used improperly, it can lead to unexpected behavior that may be difficult to find and fix.

Not Present in Older Versions of C#

Null-conditional operators were introduced in C# 6.0, which means they are not an option for developers working with legacy code, or targeting older versions of the .NET Framework. This removes the possibility for some developers and teams to take advantage of those operators.

Conclusion

In this article, we learned about the different null-conditional operators in C#. Now, we know how and when to use them to prevent null-reference exceptions and improve our code readability. We have also learned about the potential disadvantages and how to avoid them.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!