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


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

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

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

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

Справочные документы на языке C#, выпущенные последней версией языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

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

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей по журналу версий языка C#.

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

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

Укажите безусловные postconditions с помощью следующих атрибутов:

  • Может быть Нулл: возвращаемое значение, не допускающее значение 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 имеет указанное значение.