Ссылочные типы, допускающие значение NULL (справочник по C#)

Примечание

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

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

В контексте, поддерживающем значение NULL:

  • переменная ссылочного типа T должна быть инициализирована со значением, отличным от NULL, и ей невозможно присвоить значение, которое может быть равно null;
  • переменная ссылочного типа T? может быть инициализирована с помощью null, или ей может быть присвоено значение null, но она должна быть проверена на null перед разыменованием;
  • переменная m типа T? считается не равной NULL при применении оператора, опускающего NULL, как в m!.

Различия между ссылочным типом, не допускающим значение NULL, T и ссылочным типом, допускающим значение NULL, T? проявляются при интерпретации предыдущих правил компилятором. Переменная типа T и переменная типа T? представлены одним и тем же типом .NET. В следующем примере объявляется строка, не допускающая значение NULL, и строка, допускающая значение NULL, а затем используется оператор, опускающий NULL, для присваивания значения строке, не допускающей значение NULL:

string notNull = "Hello";
string? nullable = default;
notNull = nullable!; // null forgiveness

Переменные notNull и nullable представлены типом String. Так как типы, допускающие и не допускающие значение NULL, хранятся в виде одного типа, существует несколько мест, где использование ссылочного типа, допускающего значение NULL, не допускается. Как правило, ссылочный тип, допускающий значение NULL, запрещено использовать в качестве базового класса или реализованного интерфейса. Ссылочный тип, допускающий значение NULL, не может использоваться в выражении проверки типа или создания объекта. Ссылочный тип, допускающий значение NULL, не может быть типом выражения доступа к члену. Эти конструкции показаны в следующих примерах:

public MyClass : System.Object? // not allowed
{
}

var nullEmpty = System.String?.Empty; // Not allowed
var maybeObject = new object?(); // Not allowed
try
{
    if (thing is string? nullableString) // not allowed
        Console.WriteLine(nullableString);
} catch (Exception? e) // Not Allowed
{
    Console.WriteLine("error");
}

Ссылки, допускающие значение NULL, и статический анализ

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

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

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

Примечание

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

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

public class ProductDescription
{
    private string shortDescription;
    private string? detailedDescription;

    public ProductDescription() // Warning! short description not initialized.
    {
    }

    public ProductDescription(string productDescription) =>
        this.shortDescription = productDescription;

    public void SetDescriptions(string productDescription, string? details=null)
    {
        shortDescription = productDescription;
        detailedDescription = details;
    }

    public string GetDescription()
    {
        if (detailedDescription.Length == 0) // Warning! dereference possible null
        {
            return shortDescription;
        }
        else
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
    }

    public string FullDescription()
    {
        if (detailedDescription == null)
        {
            return shortDescription;
        }
        else if (detailedDescription.Length > 0) // OK, detailedDescription can't be null.
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
        return shortDescription;
    }
}

В следующем фрагменте кода показано, где компилятор выдает предупреждения при использовании этого класса:

string shortDescription = default; // Warning! non-nullable set to null;
var product = new ProductDescription(shortDescription); // Warning! static analysis knows shortDescription maybe null.

string description = "widget";
var item = new ProductDescription(description);

item.SetDescriptions(description, "These widgets will do everything.");

В предыдущих примерах показан статический анализ компилятора для определения состояния NULL ссылочных переменных. Компилятор применяет правила языка для проверок и присваиваний, чтобы получить сведения для анализа. Компилятор не может делать предположения о семантике методов или свойств. При вызове методов, выполняющих проверки значений NULL, компилятор не может понять, что эти методы влияют на состояние NULL переменной. Существует ряд атрибутов, которые можно добавить в API, чтобы сообщить компилятору о семантике аргументов и возвращаемых значений. Эти атрибуты применялись для многих общих API в библиотеках .NET Core. Например, IsNullOrEmpty был обновлен, и компилятор правильно интерпретирует этот метод как проверку значения NULL. Дополнительные сведения об атрибутах, применяемых для статического анализа состояния NULL, см. в статье Атрибуты, допускающие значение NULL.

Задание контекста, допускающего значение NULL

Существует два способа управления контекстом, допускающим значение NULL. На уровне проекта можно добавить параметр <Nullable>enable</Nullable>. В одном файле исходного кода C# можно добавить директиву pragma #nullable enable, чтобы включить контекст, допускающий значение NULL. См. статью о настройке стратегии, допускающей значение NULL. В версиях, предшествовавших версии .NET 6, в новых проектах используется значение по умолчанию <Nullable>disable</Nullable>. Начиная с .NET 6 все файлы новых проектов содержат элемент <Nullable>enable</Nullable>.

Спецификация языка C#

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

См. также