C# 8.0 nullable references: when methods don't return
This is the last in the sub-series on attributes for getting better results from C# 8's nullable references feature. (Next time I'll be getting back to some other aspects of nullable references.) As always, the goal here is to annotate our code with attributes that enable the C# compiler to do a better job with its nullable reference analysis—detecting more real problems, and reporting fewer false positives. The attributes I'm describing in this post help handle situations where a method might never return (e.g., because it throws an exception). The compiler needs to know when this can happen, because otherwise, it could incorrectly think that we're attempting to dereference a value that could be null.
With all the other posts in this sub-series, I've described attributes that were added specifically for nullable reference support. The attributes I'm describing here have been around since long before C# 8 added nullable references. They were originally designed to serve other purposes, but now have an additional role in a nullable-references-aware world.
DoesNotReturn
Earlier in this series I wrote about NotNull
. This was primarily concerned with telling the compiler what it could infer about its inputs once the method returns, but as I also showed, it can be used in cases where a method might never return.
The DoesNotReturn
attribute is also concerned with methods that don't return. But unlike NotNull
which lets the compiler infer what will be true if the method returns, DoesNotReturn
is simpler: it indicates that the annotated method will not return under any circumstances. This is typically used for methods that throw exceptions.
C# 8's nullable reference checking depends on the flow control analysis that C# has had since v1.0. This analysis supports the "definite assignment" rules that warn you when you might be trying to use a variable that has not yet been initialized. Some of the analysis the compiler was already performing for those purposes also has an impact on null handling warnings. This is why the DoesNotReturn
attribute, which has been around for a long time, can now have an impact in null-aware code, as this example shows:
public static int Measure(string? pieceOf)
{
if (pieceOf == null) { ThrowHelper(); }
return pieceOf.Length;
}
[DoesNotReturn]
private static void ThrowHelper() => throw new ArgumentNullException();
This is somewhat contrived, but in codebases that localize exception messages, the use of helper methods to throw exceptions is common. Without the DoesNotReturn
attribute, the pieceOf.Length
expression will cause a compiler warning. But with that attribute present, the compiler correctly deduces that that line can only be reached if pieceOf
is non-null (exactly as it would have done if the exception had been thrown directly from this method, without using a helper).
DoesNotReturnIf
The similar DoesNotReturnIf
can be applied to a parameter, along with a boolean value indicating that the method will only fail to return if the argument in question has the specified boolean value. This is even closer in spirit to NotNull
, in that it indicates that the method won't return under certain circumstances. But in this case, it's based purely on a boolean value, and not the nullness. Even so, the compiler's flow analysis can sometimes deduce null-related information from this attribute, as in this example:
private static void Test(string? arg)
{
OnlyReturnsIfTrue(arg != null);
Console.WriteLine(arg.Length);
}
private static void OnlyReturnsIfTrue([DoesNotReturnIf(false)] bool flag)
{
if (!flag)
{
throw new InvalidOperationException();
}
}
If the DoesNotReturnIf
attribute were not present, this example would produce a warning on line 4—args.Length
dereferences a variable of type string?
. However, the attribute lets the compiler know that the method will not return in the case where its argument is false
, and in this example, that means it won't return if arg
is null. This enables the compiler to deduce that if the method does return, args
must be non-null, and that it should not raise a warning for args.Length
at that point in the code.
Conclusion
This is the last entry in the subseries on attributes that can improve the usefulness of C# 8's nullable references feature. Each of these enables us to make more detailed statements about our code's handling of nulls, improving the detail and quality of the compiler's analysis. Here's a quick roundup of each of them:
- Overview – a quick tour round all of the attributes (the TL;DR version of the series)
- AllowNull and DisallowNull – expressing distinctions the type system cannot capture for inputs
- NotNull – enabling the compiler to infer that a reference is not null at a particular point in the code, even when the type system indicates that it is nullable
- MaybeNull – indicating potentially null outputs in generic code regardless of the nullability of the type arguments
- NotNullWhen, MaybeNullWhen, and NotNullIfNotNull – enabling the compiler to infer (non-)nullness conditionally
- DoesNotReturn and DoesNotReturnIf (this post) – avoiding false warnings by letting the compiler know when methods won't return