C# 8.0 nullable references: MaybeNull
As with all the other entries so far in this sub-series on attributes for getting better results from C# 8's nullable references feature, the goal is to enable the compiler to improve depth and precision when detecting null-related programming errors: these attributes all make it possible to find more mistakes, while also lowering the chances of a false positive.
The subject of this post is useful for revealing information about possible nullness that might otherwise have been lost as a result of the use of generics.
Last time, we looked at NotNull
. This looked superficially similar to DisallowNull
, but whereas DisallowNull
made a statement about the prerequisites for being allowed to use a member, NotNull
tells the compiler a post-condition that can safely be deduced.
Today we're going to look at MaybeNull
, which has a similar relationship to AllowNull
: whereas AllowNull
tells the compiler about a prerequisite, MaybeNull
tells the compiler about a post-condition—something it can infer if the relevant method returns.
Expressing nullability for generic type arguments
When you first try to enable nullable references for generic code, you will typically run into a problem: you will find yourself wanting to express the nullability of the usual entities (parameters, return types, etc.), but using a generic type parameter.
We've already seen how AllowNull
lets us deal with this problem for arguments where we pass information in, but what about when information flows in the other direction?
For example, suppose you have some method that attempts to find an item that matches certain criteria—this might want to return null if nothing matches. So the obvious return type would be T?
. But if you write that, you'll find it doesn't work.
This is symptomatic of the fact that nullability has been added rather late in the day. C# 1.0 just had reference types (always nullable) and value types (never nullable). Then C# 2.0 added the ?
suffix for value types, introducing things like int?
to the type system.
Now, C# 8.0 has retrofitted this idea to reference types, so that just like value types they come in nullable (string?
) and non-nullable (string
) forms.
Unfortunately, because of the history of these developments, they work in completely different ways. The meaning of T?
is radically different when T
is a value type from when T
is a reference type. (With value types, the runtime representation changes completely: instead of just, say, an int
, you get a Nullable<int>
, which is actually a combination of an int
and a bool
.
But with reference types, the runtime representation for nullable and non-nullable references is identical.) And the MSIL that needs to be generated for these two cases is consequently also quite different.
So if you want to be able to write T?
for a type parameter, you need to constrain it to be either a value type or a reference type, so that the C# compiler knows which kind of code to generate.
But that's a huge limitation. Consider what this might mean for the LINQ FirstOrDefault
operator: that needs to be able to return nothing in cases where the source is empty, so logically its return type wants to be T?
.
But to be able to have that return type, it would need to constrain T
to be either value types only, or reference types only.
That would be a huge breaking change, and extremely limiting.
So instead, we express the nullability for uses of generic type arguments differently. Methods such as FirstOrDefault
just use a return type of T
, but they are annotated with [return: MaybeNull]
to indicate that in cases where someone has supplied a non-nullable reference type as the type argment, the return type should be treated as the nullable form of that type.
So if you have an IEnumerable<string>
(and assuming that you're in a nullable annotation context, this promises that if the enumerable contains anything, everything it contains will be non-null), the effective return type of FirstOrDefault
is string?
You can use MaybeNull
on properties and out
arguments—anywhere that the type may be returned, in other words.
Note that if you apply MaybeNull
to a read/write property, this does not indicate that callers are allowed to set it to null. This attribute is strictly a statement about the result.
If you want to indicate that it is also acceptable to set it to null, you would apply both MaybeNull
and AllowNull
.