Nullable Attributes

Rather than disable nullable reference types or nullable warnings, occasionally it is helpful to provide the compiler with hints about your nullable intent. This is possible using metadata that you can place directly into your code with a construct called an attribute (see Chapter 18). There are seven different nullable attributes, each defined in the System.Diagnostics.CodeAnalysis namespace and identified as either pre-conditions or post-conditions (Table 6.1).

Table 6.1: Nullable Attributes






Non-nullable input argument may be null.



Nullable input argument should never be null.



Non-nullable return value may be null.



Nullable return value will never be null.



A non-nullable input argument may be null when the method returns the specified bool value.



Nullable input argument will not be null when the method returns the specified bool value.



Return value isn’t null if the argument for the specified parameter isn’t null.

It is helpful to have such attributes because occasionally, the nullability of the data type is insufficient. You can overcome this insufficiency with an attribute that decorates either incoming (a pre-condition nullable attribute) or outgoing (a post-condition nullable attribute) data on a method. The pre-condition communicates to the caller whether the value specified is intended to be null, whereas the post-condition communicates to the caller about the nullability of the outgoing data. Consider, for example, the methods that follow the try-get pattern shown in Listing 6.41.

Listing 6.41: Using NotNullWhen and NotNullIfNotNull Attributes
using System.Diagnostics.CodeAnalysis;
// ...
    public static bool TryGetDigitAsText(
        char number, [NotNullWhen(true)]out string? text) =>
            (text = number switch
                '1' => "one",
                '2' => "two",
                '3' => "three",
                '4' => "four",
                // ...
                '9' => "nine",
                _ => null
            }) is not null;
    // nameof() on parameters is not supported prior to C# 11/.NET 7.0
    [return: NotNullIfNotNull(nameof(text))]
    public static string? TryGetDigitsAsText(string? text)
        if (text == nullreturn null;
        string result = "";
        foreach (char character in text)
            if (TryGetDigitAsText(character, out string? digitText))
                if (result != "") result += '-';
                result += digitText.ToLower();
        return result;

Notice that the call to digitText.ToLower() from TryGetDigitAsText() has no coalescing operator and does not issue a warning even though text is declared as nullable. This is possible because the text parameter in TryGetDigitAsText() is decorated with the NotNullWhen(true) attribute, which informs the compiler that, if the method returns true (the value specified with the NotNullWhen attribute), then your intent is that digitText will not be null. The NotNullWhen attribute is a post-condition declaration, informing the caller that output (text) is not null if the method returns true.

Similarly, for TryGetDigitsAsText(), if the value specified for the text parameter is not null, then the return value will not be null. This is possible because the pre-condition nullable attribute, NotNullIfNotNull, uses whether the input value of the text parameter is null to determine whether the return value may potentially be null.

AdVanced Topic
Decorating Generic Type Parameters with the Null Modifier

When declaring a generic member or type, you will occasionally want to decorate the type parameter with a nullable modifier. The problem is that a nullable value type (a Nullable<T>) is a different data type than a nullable reference type. As a result, type parameters decorated with nullability will require a constraint that restricts the type parameter to be either a value type or a reference type. Without this constraint, you will receive the following error:

Error CS8627 A nullable type parameter must be known to be a value

  type or non-nullable reference type. Consider adding a 'class',

  'struct', or type constraint.

However, if the logic is identical for both value types and reference types, it can be frustrating to implement two different methods—especially since different constraints do not result in different signatures to allow overloading. Consider the code in Listing 6.42.

Listing 6.42: Potential Null Return With MaybeNull Attribute
// ...
[return: MaybeNull]
public static T GetObject<T>(
  IEnumerable<T> sequence, Func<T, bool> match)
// ...

Imagine that the behavior is to return an item from the collection if one satisfies the match predicate. However, if no such item exists, the intent would be to return default(T), which is null for a reference type. Unfortunately, the compiler won’t allow T? without a constraint. To avoid the warning while still declaring to callers that the return could be null, we use the post-condition MaybeNull attribute and leave the return type as T (with no nullable modifier).

{{ snackbarMessage }}