Поделиться через


Атрибуты для статического анализа состояния со значением NULL, интерпретируемые компилятором C#

В контексте, допускающем значения NULL, компилятор выполняет статический анализ кода для определения состояния со значением NULL всех переменных ссылочного типа.

  • not-null: статический анализ определяет, что переменной присвоено значение, отличное от NULL.
  • maybe-null: статический анализ не может определить, что переменной присвоено значение, отличное от NULL.

Эти состояния позволяют компилятору предоставлять предупреждения, если вы можете разыменовать указатель на NULL, вызывая исключение System.NullReferenceException. Эти атрибуты предоставляют компилятору семантическую информацию о состоянии со значением NULL аргументов, возвращаемых значений и элементах объекта на основе состояния аргументов и возвращаемых значений. Компилятор предоставляет более точные предупреждения, если к API правильно добавлены эти семантические сведения.

В этой статье дается краткое описание каждого из атрибутов ссылочного типа, допускающего значение NULL, и способа их применения.

Начнем с примера. Представьте себе, что библиотека содержит указанный ниже API для получения строки ресурса. Этот метод был первоначально скомпилирован в неизменяемом контексте null:

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

В предыдущем примере применяется знакомый шаблон Try* в .NET. Для этого API существует два ссылочных параметра: key и message. У этого API есть следующие правила, относящиеся к состоянию со значением NULL этих параметров:

  • Вызывающие объекты не должны передавать null в качестве аргумента для key.
  • Вызывающие объекты могут передавать переменную, значение которой равно null, в качестве аргумента для message.
  • Если метод TryGetMessage возвращает true, значение message не равно NULL. Если возвращаемое значение равно falseNULL.message

Правило для key выражения может быть кратко выражено: key должен быть ненулевой ссылочный тип. Параметр message является более сложным. Он допускает переменную, которая использует null в качестве аргумента, но гарантирует, что в случае успеха аргумент out не будет иметь значение null. В таких случаях требуется более широкое описание ожиданий. Атрибут NotNullWhen, описанный ниже, описывает состояние со значением NULL для аргумента, используемого для параметра message.

Примечание.

При добавлении этих атрибутов компилятор получает дополнительные сведения о правилах для API. Если вызывающий код компилируется в контексте, допускающем значение NULL, и вызывающие объекты нарушают эти правила, компилятор выведет соответствующие предупреждения. Эти атрибуты не используются для выполнения дополнительных проверок в вашей реализации.

Атрибут Категория Значение
AllowNull Предусловие Параметр, поле или свойство, не допускающие значения NULL, могут иметь значение NULL.
DisallowNull Предусловие Параметр, поле или свойство, допускающие значения NULL, не должны иметь значение NULL.
MaybeNull Постусловие Параметр, поле, свойство или возвращаемое значение, не допускающие значения NULL, могут иметь значение NULL.
NotNull Постусловие Параметр, поле, свойство или возвращаемое значение, допускающие значения NULL, не будут иметь значение NULL.
MaybeNullWhen Условное постусловие Аргумент, не допускающий значение NULL, может иметь значение NULL, если метод возвращает указанное значение bool.
NotNullWhen Условное постусловие Аргумент, допускающий значение NULL, не будет иметь значение NULL, если метод возвращает указанное значение bool.
NotNullIfNotNull Условное постусловие Возвращаемое значение, свойство или аргумент не равны NULL, если аргумент для указанного параметра не равен NULL.
MemberNotNull Вспомогательные методы для методов и свойств Если метод возвращает значение, включенный в список элемент не будет иметь значение NULL.
MemberNotNullWhen. Вспомогательные методы для методов и свойств Если метод возвращает указанное значение bool, включенный в список элемент не будет иметь значение NULL.
DoesNotReturn Недостижимый код Метод или свойство никогда не возвращает значение. Другими словами, он всегда создает исключение.
DoesNotReturnIf Недостижимый код Этот метод или свойство никогда не возвращает значение, если связанный параметр bool имеет указанное значение.

Приведенные выше сведения лишь кратко описывают каждый атрибут. В следующих разделах более подробно описывается поведение и значение этих атрибутов.

Предусловия: AllowNull и DisallowNull

Рассмотрим свойство для чтения и записи, которое никогда не возвращает null, так как имеет разумное значение по умолчанию. Вызывающие объекты передают null методу доступа set при задании ему этого значения по умолчанию. Например, рассмотрим систему обмена сообщениями, запрашивающую имя экрана в комнате чата. Если имя не указано, система создает случайное значение.

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

При компиляции предыдущего кода в контексте, игнорирующем допустимость значения NULL, не возникает никаких проблем. После включения ссылочных типов, допускающих значения NULL, свойство ScreenName преобразуется в ссылку, не допускающую значение NULL. Это верно для метода доступа get: он никогда не возвращает null. Вызывающим объектам не нужно проверять возвращаемое свойство на наличие значения null. Но теперь при задании свойству значения null создается предупреждение. Чтобы включить поддержку этого типа кода, добавьте атрибут System.Diagnostics.CodeAnalysis.AllowNullAttribute к свойству, как показано в следующем коде.

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

Чтобы использовать этот и другие атрибуты, описанные в этой статье, может потребоваться добавить директиву using для пространства имен System.Diagnostics.CodeAnalysis. Атрибут применяется к свойству,а не методу доступа set. Атрибут AllowNull позволяет определить предусловия и применяется только к аргументам. Метод доступа get имеет возвращаемое значение, но у него нет параметров. Таким образом, атрибут AllowNull применяется только к методу доступа set.

В предыдущем примере показано, что следует искать при добавлении атрибута AllowNull к аргументу.

  1. Общий контракт для этой переменной заключается в том, что она не должна принимать значение null, поэтому нужен ссылочный тип, не допускающий значение NULL.
  2. Существуют ситуации, когда вызывающий объект передается в null в качестве аргумента, хотя это не самый распространенный способ использования.

Чаще всего этот атрибут будет необходим для свойств или аргументов in, out и ref. Атрибут AllowNull лучше всего применять в ситуации, когда переменная обычно имеет значение, отличное от NULL, но необходимо разрешить null в качестве предусловия.

Сравните этот вариант со сценариями использования DisallowNull: этот атрибут используется для указания того, что аргумент ссылочного типа, допускающего значение NULL, не должен иметь значение null. Рассмотрим свойство, где null является значением по умолчанию, но клиенты могут задать для него только значение, отличное от NULL. Рассмотрим следующий код:

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

Предыдущий код лучше всего демонстрирует ваше намерение выразить то, что атрибут ReviewComment может принимать значение null, но ему невозможно задать значение null. В коде, допускающем значения NULL, эту концепцию можно представить вызывающим объектам более четко с помощью System.Diagnostics.CodeAnalysis.DisallowNullAttribute.

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

В контексте, допускающем значения NULL, метод доступа ReviewComment get может возвращать значение по умолчанию, равное null. Компилятор предупреждает, что перед доступом необходимо проверить это значение. Более того, он предупреждает вызывающие объекты о том, что это может быть null, им не следует явно задавать его равным null. Атрибут DisallowNull также задает предварительное условие. Он не влияет на метод доступа get. Атрибут DisallowNull используется при отслеживании следующих моментов.

  1. Переменная может принимать значение null в основных сценариях, часто при первом создании экземпляра.
  2. Переменной не следует явно задавать значение null.

Эти ситуации распространены в коде, который изначально игнорировал допустимость значений NULL. Может быть так, что свойства объекта задаются в двух отдельных операциях инициализации. Может быть так, что некоторые свойства задаются только после завершения какой-либо асинхронной операции.

Атрибуты AllowNull и DisallowNull позволяют указать, что предусловия для переменных могут не соответствовать заметкам, допускающим значение NULL, для этих переменных. Они предоставляют более подробные сведения о характеристиках API. Эта информация помогает вызывающим объектам правильно использовать API. Помните, что для указания предусловий используются следующие атрибуты.

  • AllowNull. Аргумент, не допускающий значение NULL, может принимать значение NULL.
  • DisallowNull. Аргумент, допускающий значение NULL, никогда не должен принимать значение NULL.

Постусловия: MaybeNull и NotNull

Предположим, у вас есть метод со следующей сигнатурой.

public Customer FindCustomer(string lastName, string firstName)

Вероятно, вы написали такой метод для возвращения значения null в случае, если искомое имя не найдено. null четко указывает, что запись не найдена. В этом примере вы, вероятно, измените тип возвращаемого значения с Customer на Customer?. Объявление возвращаемого значения как ссылочного типа, допускающего значение NULL, четко указывает намерение этого API:

public Customer? FindCustomer(string lastName, string firstName)

По причинам, описанным в разделе Допустимость значений NULL для универсальных методов, этот метод может не создавать статический анализ, соответствующий API. У вас может быть универсальный метод, который действует по аналогичному шаблону.

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

Если искомый элемент не найден, метод возвращает null. Можно уточнить, что метод возвращает значение null, если элемент не найден, добавив заметку MaybeNull к возвращаемому значению метода:

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

Предыдущий код информирует вызывающие объекты о том, что возвращаемое значение на самом деле может быть равно NULL. Он также информирует компилятор о том, что метод может возвращать выражение null, даже если тип не допускает значения NULL. При наличии универсального метода, возвращающего экземпляр своего параметра типа, T, с помощью атрибута NotNull можно указать, что он никогда не возвращает null.

Можно также указать, что возвращаемое значение или аргумент не равны NULL, даже если используется ссылочный тип, допускающий значение NULL. Следующий метод является вспомогательным и выдает исключение, если первым аргументом является null.

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

Эту подпрограмму можно вызвать следующим образом.

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

    Console.WriteLine(message.Length);
}

После включения ссылочных типов, допускающих значения NULL, необходимо убедиться, что предыдущий код компилируется без вывода предупреждений. Когда метод возвращает значение, параметр value гарантированно не будет равен NULL. Однако можно вызвать ThrowWhenNull с пустой ссылкой. Можно сделать value ссылочным типом, допускающим значение NULL, и добавить постусловие NotNull в объявление параметра.

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

В предыдущем коде показано четкое выражение имеющегося контракта. Вызывающие объекты могут передавать переменную со значением null, но аргумент гарантированно не будет равен NULL, если метод возвращает значение без выдачи исключений.

Для указания безусловных постусловий используются следующие атрибуты.

  • Может быть,Null: возвращаемое значение, не допускающее значение NULL, может иметь значение NULL.
  • NotNull: возвращаемое значение null никогда не будет null.

Условные постусловия: NotNullWhen, MaybeNullWhen, и NotNullIfNotNull

Вам, скорее всего, известен метод String.IsNullOrEmpty(String) string. Этот метод возвращает true, если аргумент имеет значение NULL или является пустой строкой. Это форма проверки null: вызывающие не должны проверять аргумент, если метод возвращается false. Чтобы сделать такой метод методом, допускающим значения NULL, необходимо задать для аргумента ссылочный тип, допускающий значение NULL, и добавить атрибут NotNullWhen.

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

В этом случае компилятор знает, что любой код, в котором возвращаемое значение равно false, не должен проходить проверку значений NULL. Для статического анализа добавление атрибута означает, что IsNullOrEmpty выполняет необходимую проверку значений NULL: когда он возвращает false, аргумент не равен null.

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

Метод String.IsNullOrEmpty(String) будет помечен для .NET Core 3.0, как показано выше. В вашей базе кода могут быть аналогичные методы, которые проверяют состояние объектов на значения NULL. Компилятор не распознает пользовательские методы проверки значений NULL, и вам потребуется добавить заметки самостоятельно. При добавлении атрибута статический анализ компилятора знает, когда тестируемая переменная прошла проверку на наличие значения NULL.

Другим применением этих атрибутов является шаблон Try*. Постусловия для аргументов ref и out сообщаются через возвращаемое значение. Рассмотрим этот метод, показанный ранее (в контексте, не допускающем значение NULL):

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

Предыдущий метод соответствует стандартному подходу .NET: возвращаемое значение указывает, было ли message задано найденное значение или задано значение по умолчанию, если сообщение не было найдено. Если метод возвращает true, значение message не равно NULL. В противном случае метод задает message значение NULL.

В контексте, допускаемом значение NULL, можно сообщить об этом идиоме с помощью атрибута NotNullWhen . При обновлении сигнатуры для аннотации параметров типов, допускающих значение NULL, задайте message значение string? и добавьте атрибут:

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

В предыдущем примере, когда TryGetMessage возвращает true, известно, что значение message не будет равно NULL. Точно так же можно добавить заметки к аналогичным методам в базе кода: аргументы могут иметь значение null и известно, что они не будут равны NULL при возвращении методом значения true.

Кроме того, может потребоваться еще один, последний атрибут. Иногда состояние NULL возвращаемого значения зависит от состояния NULL одного или нескольких аргументов. Эти методы возвращают значение, отличающееся от NULL, если определенные аргументы не равны null. Чтобы правильно добавить заметки к этим методам, используйте атрибут NotNullIfNotNull. Рассмотрим следующий метод.

string GetTopLevelDomainFromFullUrl(string url)

Если аргумент url не равен NULL, выходные данные не равны null. После включения ссылок, допускающих значение NULL, необходимо добавить дополнительные заметки, если API может принимать аргумент NULL. Можно добавить заметку к типу возвращаемого значения, как показано в следующем коде:

string? GetTopLevelDomainFromFullUrl(string? url)

Это работает, но вызывающим объектам придется часто проводить дополнительные проверки null. Контракт заключается в том, что возвращаемое значение будет равно null только тогда, когда аргумент url имеет значение null. Чтобы выразить этот контракт, можно добавить заметку к этому методу, как показано в следующем коде.

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

В предыдущем примере для параметра urlиспользуется nameof оператор. Эта функция доступна в C# 11. Перед C# 11 необходимо ввести имя параметра в виде строки. Возвращаемое значение и аргумент снабжены заметкой ?, указывающей, что каждый из них может быть равен null. Атрибут дополнительно уточняет, что возвращаемое значение не будет равно NULL, если аргумент url не принимает значение null.

Для указания условных постусловий используются следующие атрибуты.

  • MaybeNullWhen. Аргумент, не допускающий значение NULL, может иметь значение NULL, если метод возвращает указанное значение bool.
  • NotNullWhen. Аргумент, допускающий значение NULL, не будет иметь значение NULL, если метод возвращает указанное значение bool.
  • NotNullIfNotNull: возвращаемое значение не равно NULL, если аргумент для указанного параметра не имеет значения NULL.

Вспомогательные методы: MemberNotNull и MemberNotNullWhen

Эти атрибуты определяют ваше намерение при рефакторинге общего кода из конструкторов в вспомогательные методы. Компилятор C# анализирует конструкторы и инициализаторы полей, чтобы убедиться, что все ссылочные поля, не допускающие значения NULL, были инициализированы перед возвратом каждого конструктора. При этом компилятор C# не следит за назначениями полей с помощью всех вспомогательных методов. Компилятор выдает предупреждение CS8618, когда поля инициализируются не непосредственно в конструкторе, а в вспомогательном методе. Вам нужно добавить MemberNotNullAttribute в объявление метода и указать поля, которые инициализируются значением, отличным от NULL, в методе. Рассмотрим следующий пример.

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();
    }
}

В конструкторе MemberNotNull атрибута можно указать несколько имен полей в качестве аргументов.

MemberNotNullWhenAttribute имеет аргумент bool. MemberNotNullWhen используется в ситуациях, когда вспомогательный метод возвращает тип bool, указывающий, инициализированы ли поля вспомогательного метода.

Останавливает анализ типов, допускающих значение NULL, при выдаче исключения вызванным методом

Некоторые методы, как правило, это вспомогательные методы исключений или другие служебные методы, всегда завершают работу, вызывая исключение. Или вспомогательный метод может вызвать исключение на основе значения логического аргумента.

В первом случае можно добавить атрибут DoesNotReturnAttribute в объявление метода. При анализе состояния NULL компилятора не проверяется код в методе, который следует за вызовом метода, для которого добавлена заметка DoesNotReturn. Рассмотрим этот метод:

[DoesNotReturn]
private void FailFast()
{
    throw new InvalidOperationException();
}

public void SetState(object containedField)
{
    if (containedField is null)
    {
        FailFast();
    }

    // containedField can't be null:
    _field = containedField;
}

Компилятор не выдает никаких предупреждений после вызова FailFast.

Во втором случае добавьте атрибут System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute к логическому параметру метода. Предыдущий пример можно изменить следующим образом.

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;
}

Если значение аргумента совпадает со значением конструктора DoesNotReturnIf, компилятор не выполняет анализ состояния NULL после этого метода.

Итоги

Добавление ссылочных типов, допускающих значения NULL, предоставляет исходный словарь для описания ожиданий API для переменных, которые могут иметь значение null. Атрибуты позволяют более подробно описывать состояние NULL для переменных в качестве предусловий и постусловий. Эти атрибуты более четко описывают ожидания и обеспечивают более эффективную работу специалистов, использующих ваши API.

При обновлении библиотек для контекста, допускающего значение NULL, добавьте эти атрибуты, чтобы пользователи могли правильно использовать ваши API. С помощью этих атрибутов вы можете полностью описывать состояние NULL аргументов и возвращаемых значений.

  • AllowNull. Поле, параметр или свойство, не допускающие значения NULL, могут иметь значение NULL.
  • DisallowNull. Поле, параметр или свойство, допускающие значения NULL, не должны иметь значение NULL.
  • MaybeNull. Поле, параметр, свойство или возвращаемое значение, не допускающие значения NULL, могут иметь значение NULL.
  • NotNull. Поле, параметр, свойство или возвращаемое значение, допускающие значения NULL, не будут иметь значение NULL.
  • MaybeNullWhen. Аргумент, не допускающий значение NULL, может иметь значение NULL, если метод возвращает указанное значение bool.
  • NotNullWhen. Аргумент, допускающий значение NULL, не будет иметь значение NULL, если метод возвращает указанное значение bool.
  • NotNullIfNotNull. Параметр, свойство или возвращаемое значение не равно NULL, если аргумент для указанного параметра не равен NULL.
  • DoesNotReturn. Метод или свойство никогда не возвращает значение. Другими словами, он всегда создает исключение.
  • DoesNotReturnIf. Этот метод или свойство никогда не возвращает значение, если связанный параметр bool имеет указанное значение.