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


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

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

  • not-null: статический анализ определяет, что переменной присвоено значение, отличное от 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.
DisallowNull Предусловие Параметр, поле или свойство, допускающие значения NULL, не должны иметь значение NULL.
MaybeNull Постусловие Ненулевой параметр, поле, свойство или возвращаемое значение может иметь значение NULL.
NotNull Постусловие Параметр, допускающий значение NULL, поле, свойство или возвращаемое значение, никогда не имеет значения NULL.
MaybeNullWhen Условное постусловие Аргумент, не допускающий значения NULL, может иметь значение NULL, если метод возвращает указанное bool значение.
NotNullWhen Условное постусловие Аргумент, допускающий значение NULL, не имеет значения NULL, если метод возвращает указанное bool значение.
NotNullIfNotNull Условное постусловие Возвращаемое значение, свойство или аргумент не равны NULL, если аргумент для указанного параметра не равен NULL.
MemberNotNull Вспомогательные методы для методов и свойств Указанный элемент не имеет значения NULL при возврате метода.
MemberNotNullWhen. Вспомогательные методы для методов и свойств Указанный элемент не имеет значения NULL, если метод возвращает указанное bool значение.
DoesNotReturn Недостижимый код Метод или свойство никогда не возвращает значение. Другими словами, он всегда создает исключение.
DoesNotReturnIf Недостижимый код Этот метод или свойство никогда не возвращает значение, если связанный параметр bool имеет указанное значение.

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

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

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

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

При компиляции предыдущего кода в контексте, игнорирующем допустимость значения NULL, не возникает никаких проблем. После включения ссылочных типов, допускающих значения NULL, свойство ScreenName преобразуется в ссылку, не допускающую значение 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 в качестве аргумента, хотя это не самый распространенный способ использования.

Чаще всего этот атрибут требуется для свойств или inoutаргументов.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, метод доступа ReviewCommentget может возвращать значение по умолчанию, равное null. Компилятор предупреждает, что перед доступом необходимо проверить это значение. Более того, он предупреждает вызывающие объекты о том, что это может быть null, им не следует явно задавать его равным null. Атрибут DisallowNull также задает предварительное условие. Он не влияет на метод доступа get. Атрибут DisallowNull используется при отслеживании следующих моментов.

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

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

DisallowNull Атрибуты AllowNull позволяют указать, что предварительные условия для переменных могут не совпадать с заметками, допускающими значение 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, с помощью атрибута null можно указать, что он никогда не возвращает NotNull.

Можно также указать, что возвращаемое значение или аргумент не равны 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.
  • NotNull: возвращаемое значение null никогда не равно NULL.

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

Вам, скорее всего, известен метод stringString.IsNullOrEmpty(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) показан в предыдущем примере. В базе кода могут быть аналогичные методы, которые проверяют состояние объектов для значений 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;
}

В предыдущем примере, когда message возвращает TryGetMessage, известно, что значение true не будет равно 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)

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

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

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

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

Эти атрибуты указывают намерение при рефакторинге общего кода из конструкторов в вспомогательные методы. Компилятор C# анализирует конструкторы и инициализаторы полей, чтобы убедиться, что все ненулевое ссылочные поля инициализированы перед возвратом каждого конструктора. При этом компилятор 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.
  • DisallowNull. Поле, параметр или свойство, допускающие значения NULL, не должны иметь значение NULL.
  • MaybeNull: поле, не допускающее значения NULL, параметра, свойства или возвращаемого значения, может иметь значение NULL.
  • NotNull: поле с значением NULL, параметром, свойством или возвращаемым значением никогда не равно NULL.
  • Может быть,NullWhen: аргумент, не допускающий значения NULL, может иметь значение NULL, если метод возвращает указанное bool значение.
  • NotNullWhen: аргумент, допускающий значение NULL, не является null, если метод возвращает указанное bool значение.
  • NotNullIfNotNull. Параметр, свойство или возвращаемое значение не равно NULL, если аргумент для указанного параметра не равен NULL.
  • DoesNotReturn. Метод или свойство никогда не возвращает значение. Другими словами, он всегда создает исключение.
  • DoesNotReturnIf. Этот метод или свойство никогда не возвращает значение, если связанный параметр bool имеет указанное значение.