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.
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
.
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.