Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

This browser is no longer supported.

Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.

Download Microsoft EdgeMore info about Internet Explorer and Microsoft Edge
Table of contentsExit focus mode

Attributes for null-state static analysis interpreted by the C# compiler

  • 2023-05-05
Feedback

In this article

In a nullable enabled context, the compiler performs static analysis of code to determine thenull-state of all reference type variables:

  • not-null: Static analysis determines that a variable has a non-null value.
  • maybe-null: Static analysis can't determine that a variable is assigned a non-null value.

These states enable the compiler to provide warnings when you may dereference a null value, throwing aSystem.NullReferenceException. These attributes provide the compiler with semantic information about thenull-state of arguments, return values, and object members based on the state of arguments and return values. The compiler provides more accurate warnings when your APIs have been properly annotated with this semantic information.

This article provides a brief description of each of the nullable reference type attributes and how to use them.

Let's start with an example. Imagine your library has the following API to retrieve a resource string. This method was originally compiled in anullable oblivious context:

bool TryGetMessage(string key, out string message){    if (_messageMap.ContainsKey(key))        message = _messageMap[key];    else        message = null;    return message != null;}

The preceding example follows the familiarTry* pattern in .NET. There are two reference parameters for this API: thekey and themessage. This API has the following rules relating to thenull-state of these parameters:

  • Callers shouldn't passnull as the argument forkey.
  • Callers can pass a variable whose value isnull as the argument formessage.
  • If theTryGetMessage method returnstrue, the value ofmessage isn't null. If the return value isfalse, the value ofmessage is null.

The rule forkey can be expressed succinctly:key should be a non-nullable reference type. Themessage parameter is more complex. It allows a variable that isnull as the argument, but guarantees, on success, that theout argument isn'tnull. For these scenarios, you need a richer vocabulary to describe the expectations. TheNotNullWhen attribute, described below describes thenull-state for the argument used for themessage parameter.

Note

Adding these attributes gives the compiler more information about the rules for your API. When calling code is compiled in a nullable enabled context, the compiler will warn callers when they violate those rules. These attributes don't enable more checks on your implementation.

AttributeCategoryMeaning
AllowNullPreconditionA non-nullable parameter, field, or property may be null.
DisallowNullPreconditionA nullable parameter, field, or property should never be null.
MaybeNullPostconditionA non-nullable parameter, field, property, or return value may be null.
NotNullPostconditionA nullable parameter, field, property, or return value will never be null.
MaybeNullWhenConditional postconditionA non-nullable argument may be null when the method returns the specifiedbool value.
NotNullWhenConditional postconditionA nullable argument won't be null when the method returns the specifiedbool value.
NotNullIfNotNullConditional postconditionA return value, property, or argument isn't null if the argument for the specified parameter isn't null.
MemberNotNullMethod and property helper methodsThe listed member won't be null when the method returns.
MemberNotNullWhenMethod and property helper methodsThe listed member won't be null when the method returns the specifiedbool value.
DoesNotReturnUnreachable codeA method or property never returns. In other words, it always throws an exception.
DoesNotReturnIfUnreachable codeThis method or property never returns if the associatedbool parameter has the specified value.

The preceding descriptions are a quick reference to what each attribute does. The following sections describe the behavior and meaning of these attributes more thoroughly.

Preconditions:AllowNull andDisallowNull

Consider a read/write property that never returnsnull because it has a reasonable default value. Callers passnull to the set accessor when setting it to that default value. For example, consider a messaging system that asks for a screen name in a chat room. If none is provided, the system generates a random name:

public string ScreenName{    get => _screenName;    set => _screenName = value ?? GenerateRandomScreenName();}private string _screenName;

When you compile the preceding code in a nullable oblivious context, everything is fine. Once you enable nullable reference types, theScreenName property becomes a non-nullable reference. That's correct for theget accessor: it never returnsnull. Callers don't need to check the returned property fornull. But now setting the property tonull generates a warning. To support this type of code, you add theSystem.Diagnostics.CodeAnalysis.AllowNullAttribute attribute to the property, as shown in the following code:

[AllowNull]public string ScreenName{    get => _screenName;    set => _screenName = value ?? GenerateRandomScreenName();}private string _screenName = GenerateRandomScreenName();

You may need to add ausing directive forSystem.Diagnostics.CodeAnalysis to use this and other attributes discussed in this article. The attribute is applied to the property, not theset accessor. TheAllowNull attribute specifiespre-conditions, and only applies to arguments. Theget accessor has a return value, but no parameters. Therefore, theAllowNull attribute only applies to theset accessor.

The preceding example demonstrates what to look for when adding theAllowNull attribute on an argument:

  1. The general contract for that variable is that it shouldn't benull, so you want a non-nullable reference type.
  2. There are scenarios for a caller to passnull as the argument, though they aren't the most common usage.

Most often you'll need this attribute for properties, orin,out, andref arguments. TheAllowNull attribute is the best choice when a variable is typically non-null, but you need to allownull as a precondition.

Contrast that with scenarios for usingDisallowNull: You use this attribute to specify that an argument of a nullable reference type shouldn't benull. Consider a property wherenull is the default value, but clients can only set it to a non-null value. Consider the following code:

public string ReviewComment{    get => _comment;    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");}string _comment;

The preceding code is the best way to express your design that theReviewComment could benull, but can't be set tonull. Once this code is nullable aware, you can express this concept more clearly to callers using theSystem.Diagnostics.CodeAnalysis.DisallowNullAttribute:

[DisallowNull]public string? ReviewComment{    get => _comment;    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");}string? _comment;

In a nullable context, theReviewCommentget accessor could return the default value ofnull. The compiler warns that it must be checked before access. Furthermore, it warns callers that, even though it could benull, callers shouldn't explicitly set it tonull. TheDisallowNull attribute also specifies apre-condition, it doesn't affect theget accessor. You use theDisallowNull attribute when you observe these characteristics about:

  1. The variable could benull in core scenarios, often when first instantiated.
  2. The variable shouldn't be explicitly set tonull.

These situations are common in code that was originallynull oblivious. It may be that object properties are set in two distinct initialization operations. It may be that some properties are set only after some asynchronous work has completed.

TheAllowNull andDisallowNull attributes enable you to specify that preconditions on variables may not match the nullable annotations on those variables. These provide more detail about the characteristics of your API. This additional information helps callers use your API correctly. Remember you specify preconditions using the following attributes:

Postconditions:MaybeNull andNotNull

Suppose you have a method with the following signature:

public Customer FindCustomer(string lastName, string firstName)

You've likely written a method like this to returnnull when the name sought wasn't found. Thenull clearly indicates that the record wasn't found. In this example, you'd likely change the return type fromCustomer toCustomer?. Declaring the return value as a nullable reference type specifies the intent of this API clearly:

public Customer? FindCustomer(string lastName, string firstName)

For reasons covered underGenerics nullability that technique may not produce the static analysis that matches your API. You may have a generic method that follows a similar pattern:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

The method returnsnull when the sought item isn't found. You can clarify that the method returnsnull when an item isn't found by adding theMaybeNull annotation to the method return:

[return: MaybeNull]public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

The preceding code informs callers that the return valuemay actually be null. It also informs the compiler that the method may return anull expression even though the type is non-nullable. When you have a generic method that returns an instance of its type parameter,T, you can express that it never returnsnull by using theNotNull attribute.

You can also specify that a return value or an argument isn't null even though the type is a nullable reference type. The following method is a helper method that throws if its first argument isnull:

public static void ThrowWhenNull(object value, string valueExpression = ""){    if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);}

You could call this routine as follows:

public static void LogMessage(string? message){    ThrowWhenNull(message, $"{nameof(message)} must not be null");    Console.WriteLine(message.Length);}

After enabling null reference types, you want to ensure that the preceding code compiles without warnings. When the method returns, thevalue parameter is guaranteed to be not null. However, it's acceptable to callThrowWhenNull with a null reference. You can makevalue a nullable reference type, and add theNotNull post-condition to the parameter declaration:

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = ""){    _ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);    // other logic elided

The preceding code expresses the existing contract clearly: Callers can pass a variable with thenull value, but the argument is guaranteed to never be null if the method returns without throwing an exception.

You specify unconditional postconditions using the following attributes:

  • MaybeNull: A non-nullable return value may be null.
  • NotNull: A nullable return value will never be null.

Conditional post-conditions:NotNullWhen,MaybeNullWhen, andNotNullIfNotNull

You're likely familiar with thestring methodString.IsNullOrEmpty(String). This method returnstrue when the argument is null or an empty string. It's a form of null-check: Callers don't need to null-check the argument if the method returnsfalse. To make a method like this nullable aware, you'd set the argument to a nullable reference type, and add theNotNullWhen attribute:

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

That informs the compiler that any code where the return value isfalse doesn't need null checks. The addition of the attribute informs the compiler's static analysis thatIsNullOrEmpty performs the necessary null check: when it returnsfalse, the argument isn'tnull.

string? userInput = GetUserInput();if (!string.IsNullOrEmpty(userInput)){    int messageLength = userInput.Length; // no null check needed.}// null check needed on userInput here.

TheString.IsNullOrEmpty(String) method will be annotated as shown above for .NET Core 3.0. You may have similar methods in your codebase that check the state of objects for null values. The compiler won't recognize custom null check methods, and you'll need to add the annotations yourself. When you add the attribute, the compiler's static analysis knows when the tested variable has been null checked.

Another use for these attributes is theTry* pattern. The postconditions forref andout arguments are communicated through the return value. Consider this method shown earlier (in a nullable disabled context):

bool TryGetMessage(string key, out string message){    if (_messageMap.ContainsKey(key))        message = _messageMap[key];    else        message = null;    return message != null;}

The preceding method follows a typical .NET idiom: the return value indicates ifmessage was set to the found value or, if no message is found, to the default value. If the method returnstrue, the value ofmessage isn't null; otherwise, the method setsmessage to null.

In a nullable enabled context, you can communicate that idiom using theNotNullWhen attribute. When you annotate parameters for nullable reference types, makemessage astring? and add an attribute:

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message){    if (_messageMap.ContainsKey(key))        message = _messageMap[key];    else        message = null;    return message is not null;}

In the preceding example, the value ofmessage is known to be not null whenTryGetMessage returnstrue. You should annotate similar methods in your codebase in the same way: the arguments could equalnull, and are known to be not null when the method returnstrue.

There's one final attribute you may also need. Sometimes the null state of a return value depends on the null state of one or more arguments. These methods will return a non-null value whenever certain arguments aren'tnull. To correctly annotate these methods, you use theNotNullIfNotNull attribute. Consider the following method:

string GetTopLevelDomainFromFullUrl(string url)

If theurl argument isn't null, the output isn'tnull. Once nullable references are enabled, you need to add more annotations if your API may accept a null argument. You could annotate the return type as shown in the following code:

string? GetTopLevelDomainFromFullUrl(string? url)

That also works, but will often force callers to implement extranull checks. The contract is that the return value would benull only when the argumenturl isnull. To express that contract, you would annotate this method as shown in the following code:

[return: NotNullIfNotNull(nameof(url))]string? GetTopLevelDomainFromFullUrl(string? url)

The previous example uses thenameof operator for the parameterurl. That feature is available in C# 11. Before C# 11, you'll need to type the name of the parameter as a string. The return value and the argument have both been annotated with the? indicating that either could benull. The attribute further clarifies that the return value won't be null when theurl argument isn'tnull.

You specify conditional postconditions using these attributes:

  • MaybeNullWhen: A non-nullable argument may be null when the method returns the specifiedbool value.
  • NotNullWhen: A nullable argument won't be null when the method returns the specifiedbool value.
  • NotNullIfNotNull: A return value isn't null if the argument for the specified parameter isn't null.

Helper methods:MemberNotNull andMemberNotNullWhen

These attributes specify your intent when you've refactored common code from constructors into helper methods. The C# compiler analyzes constructors and field initializers to make sure that all non-nullable reference fields have been initialized before each constructor returns. However, the C# compiler doesn't track field assignments through all helper methods. The compiler issues warningCS8618 when fields aren't initialized directly in the constructor, but rather in a helper method. You add theMemberNotNullAttribute to a method declaration and specify the fields that are initialized to a non-null value in the method. For example, consider the following example:

public class Container{    private string _uniqueIdentifier; // must be initialized.    private string? _optionalMessage;    public Container()    {        Helper();    }    public Container(string message)    {        Helper();        _optionalMessage = message;    }    [MemberNotNull(nameof(_uniqueIdentifier))]    private void Helper()    {        _uniqueIdentifier = DateTime.Now.Ticks.ToString();    }}

You can specify multiple field names as arguments to theMemberNotNull attribute constructor.

TheMemberNotNullWhenAttribute has abool argument. You useMemberNotNullWhen in situations where your helper method returns abool indicating whether your helper method initialized fields.

Stop nullable analysis when called method throws

Some methods, typically exception helpers or other utility methods, always exit by throwing an exception. Or, a helper may throw an exception based on the value of a Boolean argument.

In the first case, you can add theDoesNotReturnAttribute attribute to the method declaration. The compiler'snull-state analysis doesn't check any code in a method that follows a call to a method annotated withDoesNotReturn. Consider this method:

[DoesNotReturn]private void FailFast(){    throw new InvalidOperationException();}public void SetState(object containedField){    if (containedField is null)    {        FailFast();    }    // containedField can't be null:    _field = containedField;}

The compiler doesn't issue any warnings after the call toFailFast.

In the second case, you add theSystem.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute attribute to a Boolean parameter of the method. You can modify the previous example as follows:

private void FailFastIf([DoesNotReturnIf(true)] bool isNull){    if (isNull)    {        throw new InvalidOperationException();    }}public void SetFieldState(object? containedField){    FailFastIf(containedField == null);    // No warning: containedField can't be null here:    _field = containedField;}

When the value of the argument matches the value of theDoesNotReturnIf constructor, the compiler doesn't perform anynull-state analysis after that method.

Summary

Adding nullable reference types provides an initial vocabulary to describe your APIs expectations for variables that could benull. The attributes provide a richer vocabulary to describe the null state of variables as preconditions and postconditions. These attributes more clearly describe your expectations and provide a better experience for the developers using your APIs.

As you update libraries for a nullable context, add these attributes to guide users of your APIs to the correct usage. These attributes help you fully describe the null-state of arguments and return values.

  • AllowNull: A non-nullable field, parameter, or property may be null.
  • DisallowNull: A nullable field, parameter, or property should never be null.
  • MaybeNull: A non-nullable field, parameter, property, or return value may be null.
  • NotNull: A nullable field, parameter, property, or return value will never be null.
  • MaybeNullWhen: A non-nullable argument may be null when the method returns the specifiedbool value.
  • NotNullWhen: A nullable argument won't be null when the method returns the specifiedbool value.
  • NotNullIfNotNull: A parameter, property, or return value isn't null if the argument for the specified parameter isn't null.
  • DoesNotReturn: A method or property never returns. In other words, it always throws an exception.
  • DoesNotReturnIf: This method or property never returns if the associatedbool parameter has the specified value.
Collaborate with us on GitHub
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, seeour contributor guide.

Feedback

Was this page helpful?

YesNo

In this article

Was this page helpful?

YesNo