Search  
Always will be ready notify the world about expectations as easy as possible: job change page
Apr 7, 2023

Safer nullability in modern C#

Author:
Matt Eland
Source:
Views:
3158

Exploring nullability improvements in C# and the !, ?, ??, and ??= operators.

Null is famously quoted as being the "billion-dollar mistake" due to the quantity of NullReferenceExceptions that we see in code (particularly when just starting out).

The prevalence of null forces a significant amount of developer attention doing things like:

  • Validating that parameters are not null.
  • Writing conditional logic to prevent NullReferenceExceptions from occurring.
  • Defaulting variables to other values when null values are encountered.

Thankfully, a few years ago dotnet gave us some additional tools to detect and counteract null values at compile time, to compete with similar features in languages like F# and others.

In this article I’ll walk through some of the nullability options we have when writing C# code as well as some of the supporting syntax that is handy when working with potentially null values.

Enabling Project-wide Nullable Checks

C# 8 introduced a radically different way of enforcing null reference exceptions at compile time using the project-wide nullable context.

This is an opt-in setting that will highlight potential null reference exceptions in your code during development. This helps you identify potential NullReferenceExceptions before your program even runs.

My personal opinion is that I am almost always in favor of a stricter compiler if it can help me avoid bad code at runtime, but you may find this not as much to your liking.

In order to enable this setting, right click on your project in Visual Studio and then select “Properties”.

Once you are in properties, find the “General” blade under the “Build” menu and then change the “Nullable” setting to “Enable”. This will turn on project-wide nullable checks at compile time for that project.

Checking this modifies your .csproj file and sets Nullable to "enable" within your PropertyGroup element.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>
</Project>

Once this setting is enabled, potential NullReferenceExceptions are highlighted in the Visual Studio editor surface as shown below:

Here we see Visual Studio pointing out that _context may currently be null given what it knows about your code. This indicates that a NullReferenceException may occur when this line runs.

These warnings will also appear in your output from building the project and in your error list if you include Build + IntelliSense errors in that display as shown in the image above.

It’s worth noting that project-wide null checks are only available in C# 8 and later versions.

The Nullable Operator (?)

Once you turn on project-level null checks, all reference variables are assumed to be non-null unless you explicitly tell C# that null values are possible in that variable.

We tell C# that null are allowable in a variable using the nullable operator (?) as shown with the following declaration:

// _context may have a null value after initialization
private GameContext? _context;

// _graphics cannot have a null value after initialization
private GraphicsDeviceManager _graphics;

When you add the ? operator after a Type declaration, you tell the C# compiler that a null value is possible after the object has been instantiated. C# will then warn if you attempt to reference the value without checking for null.

If you omit the ? operator, C# will assume that once the object is instantiated, null values are not possible in that member.

Thankfully, C# will also warn you if it is possible for something to be null when it is declared as non-nullable.

See my articles on the required keyword and init-only members for more options for ensuring that values are provided at time of object initialization in more recent versions of C#.

The Non-Null Assertion (!)

While these null checks can be phenomenally helpful, they can sometimes miss the full context of your code and assume things are nullable that you know cannot be.

When this occurs, you can hint to C# that a variable is not null by adding the null-forgiving operator (!) to the end of the variable reference as shown below:

protected override void Draw(GameTime gameTime)
{
   GraphicsDevice.Clear(Color.Black);

   // _context will always be initialized before draw is called
   _context!.Sprites.Begin();
   _context.Update(gameTime, true);
   _context.Sprites.End();

   base.Draw(gameTime);
}

Here we add ! after the _context variable is referenced the first time. C# now knows to suppress its nullability warning for this variable for this line. Since _context has already been referenced before the next two lines run, they know a NullReferenceException will never occur on the following two lines. The result of this is that ! effectively mutes false-positives for that variable for that method.

Note that if I am mistaken and I add ! to something that actually is null at runtime, I will get a NullReferenceException when that line runs.

To me, having to add ! in places is the most irritating aspect of the C# nullability improvements.

I’m finding that the irritation of using ! is subtly changing my coding style slightly to ensure that non-null things remain non-null at the end of initialization. Honestly, I think this shift in design philosophy is a key benefit of turning on nullability analysis.

By enforcing nullability warnings and irritating me with having to use the ! operator, C# is pushing me towards better runtime code quality.

Null-Conditional Operator (?)

Finally, let’s look at the ? and ?? operators that help you safely deal with null without having to use a lot of additional if statements.

Without these operators, if you wanted to safely call something on a variable that might be null, you had to write an if statement as shown below:

if (_context != null)
{
   _context.Update();
}

However, with the ? operator, you can condense this down into a single line of code:

_context?.Update();

This will invoke Update on _context if _context is not currently null. If _context is null, nothing will happen and the program will advance to the next statement.

See Microsoft’s documentation on the Null-conditional operator for more information.

Null Coalescing Operator (??)

The ?? operator is somewhat related. If you wanted to initialize a variable to one variable if it isn't null or a fallback value if it is null, you'd have to write some code like the following:

string displayText;

if (value != null)
{
   displayText = value;
}
else
{
   displayText = "Fallback value";
}

Now, you could write this with a ternary operator as string displayText = value != null ? value : "Fallback value", but ternary operators are fairly hard to read, even if you're familiar with them.

Instead, we have the null coalescing operator that lets us write the code as the following:

// use Fallback Value if value is null
string displayText = value ?? "Fallback value";

While this may be hard to read the first time you see it, I find this significantly more readable over time than the often-maligned ternary operator.

Null Coalescing Assignment Operator (??=)

Because the ?? operator caught on so nicely, and developers like concise code, we now have a related null coalescing assignment operator that helps us simplify code.

If you wanted to assign a value to a variable if the variable is currently null, you could write it as follows:

if (displayText == null)
{
   displayText = "Fallback value";
}

Here we are simply assigning a value if something is currently null in a few lines of code.

The null coalescing assignment operator lets us do this in a single statement:

// Assign "Fallback value" to displayText only if displayText is currently null
displayText ??= "Fallback value";

I find the null-coalescing assignment operator a bit harder to read than the other operators I mention in this article, but even it isn’t bad and you’re not required to use it.

Final Thoughts

In this article I outlined how C# 8 and beyond allow you to use more advanced null checking at compiler time and give you additional language features to help deal with the issues that can arise out of null values and assignment statements.

As with any new language feature, these benefits of additional null safety come with the price of additional complexity in the form of new compiler warnings and new operators in our code. This added complexity can slow down and cause anxiety in new developers approaching the C# language.

However, the added benefits of early detection of null values and support for null-safe code patterns likely make up for the added complexity. Still, teams should be cautious about their use of any new language features and pick only the ones that give them the greatest value, knowing that they will need to teach new developers the meaning of some of these ever-increasing operators present in the C# language.

Similar
Mar 29, 2023
Author: Anna Monus
The HTTP protocol lets browsers and other applications request resources from a server on the internet, for example, to load a web page. HTTP/3 is the latest version of this protocol, which was published by the Internet Engineering Task Force...
Jun 27, 2023
Author: Anton Selin
Introduction Performance optimization is a key concern in software development, regardless of the programming language or platform you’re using. It’s all about making your software run faster or with less memory consumption, leading to better user experience and more efficient...
Apr 3, 2013
Introduction This is just a simple article visually explaining SQL JOINs. Background I'm a pretty visual person. Things seem to make more sense as a picture. I looked all over the Internet for a good graphical representation of SQL JOINs,...
24 марта
Автор: Елена Капаца
Разобрали ключевые отличия фреймворка от библиотеки и другими типами импортируемых объектов в Python с применением диаграмм. Реальные программы сложны и даже элементарный симулятор игральных костей требует большого количества кода. Чтобы упростить процесс, разработчики используют модульное программирование — разбивают задачи на...
Send message
Type
Email
Your name
*Message