The feature and concept of Nullable reference types were introduced in C# 8.0 and it basically made all types non-nullable by default and ensured that these types could never be assigned the value null
. This is one of my favorite features in C# recently, but there are scenarios where a mixed nullable environment could cause confusion.
To enable the assignment of the value null
to a type, you have to explicitly mark that type. This uses the same concept of nullable as introduced in C# 2.0, where you, for example, make an int
nullable by adding a question mark after it: int?
.
When we look at a regular example service-class, we can see which benefits can be had from Nullable reference types:
public class ProductService
{
// This method accepts a non-null string for 'productId'
// and always returns a string
public string FormatProductId(string productId)
{
// ...
}
// This method accepts a nullable 'formattedProductId'
// and returns a string or null
public string? TryGetProductName(string? formattedProductId)
{
// ...
}
}
This makes things all fine and clear. We know that the method FormatProductId
never returns null
and that it doesn't accept null
in its parameter. We also know that the method TryGetProductName
returns a string
which could be null
and that the parameter accepts a string
which could be null
.
This is great, this means that we don't have to perform a null
-check on productId
-parameter of the FormatProductId
-method, right? Well, not exactly...
Confusion: Mixed nullable environments
In an environment where all your code has Nullable reference types enabled, you can trust the output of a method and the input to its parameters. In a mixed nullable environment, things are not as straight forward, especially when you look at how IntelliSense in Visual Studio signals what to expect from the code.
Scenario 1: Modern app & legacy library
Imagine that your new modern app has Nullable reference types enabled, but you're using an external library that is legacy and does not have this enabled. This external library can be your own old library or something you've included from NuGet.
The problem now becomes that the external library is signaling, for example, that it has a method that returns a string
and not a string?
, so you should be able to trust that it is not null
, right? Unfortunately not. Even with a local non-nullable project, IntelliSense tells me that the returned string
is not null
, even when it is.
Scenario 2: Legacy app & modern library
Imagine that you have just put together a nice library that you want others to use, either in your project, organization or publicly through NuGet. One of the best parts about using Nullable reference types is that the compiler will warn you if you try to send in a null
value as a parameter to a method that explicitly states that it doesn't support null
.
Nice, now you can clean out all those noisy null
-checks at the top of all the methods, right? Unfortunately not. Your code might be used by another assembly (or an older version of Visual Studio), which doesn't detect the non-nullability.
In a way, this means you have to reverse the way you do null
-checks in your code.
public class ProductService
{
// This method does not accept a null-value
// and if it does, it should throw an exception
public string FormatProductId(string productId)
{
if (productId == null)
throw new ArgumentNullException(productId);
// ...
}
// This method accepts null-values
// and should adjust its logic accordingly
public string? TryGetProductName(string? formattedProductId)
{
return
formattedProductId != null
? GetProductName(formattedProductId)
: null;
}
}
Key takeaways
My own take-aways from exploring this aspect of Nullable reference types are:
- When building a library, always check for
null
in incoming method-arguments, even when Nullable reference types is enabled - When consuming an external legacy library, don't trust the return-type to not be
null
(even if it says it's not) - In a mixed nullable environment, the feature to guard us against
NullReferenceException
s is likely to mistakenly cause some more of them - When this feature is fully adopted, there will be a reduction in a lot of the overhead in
null
-handling code
Thoughts
Hopefully, in .NET 5, this feature is enabled by default and these kinds of confusions, and described associated errors can be avoided.
One idea for an improvement to the IntelliSense-behavior around assemblies that are not known to have Nullable reference types enabled could be to show all these types as nullable. Both because it makes things super-clear, but also because of the fact that they actually are nullable.
This change would make everything in the whole .NET Core CLR light up as nullable, but as of .NET Core 3.1, it all is nullable, by definition.