C# 8.0 nullable references: more type system transcendence with DisallowNull
In my last blog in this series, I talked about the AllowNull attribute. This post shows its converse, DisallowNull
. As with all the attributes relating to nullable references, the goal is to improve expressiveness, enabling the compiler to report more nullable-related problems without producing spurious warnings.
DisallowNull
is pretty much the mirror image of AllowNull: whereas AllowNull
lets you state that a null is allowed even when the type seems to state otherwise, DisallowNull
lets you state that a null is not allowed even when the type system seems to state that it would be allowed. Much like AllowNull
, DisallowNull
is used with asymmetric properties, and generics.
Asymmetric properties
Occasionally, you might define a property that returns null if it has not been set yet, but where if someone does set it, they must provide a non-null value. You cannot express this through the C# type system alone, because a property's get and set accessors must have the same type. If you use a nullable type, you won't get warnings when code sets the property to a null value. If you use a non-nullable type, you won't get warnings for code that uses the property without checking for null.
DisallowNull
resolves this dilemma. You can define a property with a nullable type, and then apply the DisallowNull
attribute. The nullability of the property's type means the compiler will flag any attempts to use the value returned by the property without first checking it for null. But the presence of this attribute means it will also report any attempts to set it to a value that might be null.
Generics
DisallowNull
also gets used in generic code. It enables you to state that a particular usage of a type parameter does not accept null even when a nullable type is supplied as the type argument for that parameter. You can see this in use in the IEqualityComparer<T>
interface from the .NET class library (which, as it happens, uses both AllowNull
and DisallowNull
):
The GetHashCode
method here indicates that it does not accept nulls. So even if we instantiate this as IEqualityComparer<SomeClass?>
, the effective signature of GetHashCode
makes the argument type SomeClass
(i.e., non-nullable) and not the SomeClass?
(nullable) specified in the generic type argument. (Meanwhile, the arguments for Equals
are both nullable regardless of whether the type argument is nullable or non-nullable.)
This is consistent with the non-generic version of the interface:
Here it's clear that the Equals
method accepts nulls but that GetHashCode
does not. This non-generic definition expresses this more directly through the presence or absence of ?
, but the generic interface needs to use the AllowNull
and DisallowNull
attributes.
Conclusion
DisallowNull
helps us out in two scenarios that are very similar to ones where its converse, AllowNull
, is useful. One is where a type is stated once, but used in multiple ways, and we want different nullability in different places. The usual example of this is a property: we only get to declare the type of a property once, but we may require different nullability for the get and set accessors. Generics may also create scenarios where a type is expressed just once (as the type parameter) but then used in different ways. And then there's a second scenario, specific to generics, in which we want to be able to say that in cases where the argument for a type parameter is a reference type, we want the non-nullable version of that type, regardless of the nullability of the type argument.