Skip to content
  1. We help small teams achieve big things.
  2. Our Technical Fellow just became a Microsoft MVP!
  3. We run the Azure Weekly newsletter.
  4. We just won 2 awards.
  5. We just published Programming C# 8.0 book.
  6. We run the Power BI Weekly newsletter.
  7. Our NDC London 2020 talk is now available online!
  8. We are school STEM ambassadors.
Ian Griffiths By Ian Griffiths Technical Fellow I
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.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 8.0, and has written Pluralsight courses on WPF (and here) and the TPL. Technology brings him joy.