Общие сведения о допустимости значений NULL

Завершено

Если вы разработчик .NET, вероятно, вы сталкивались с исключением System.NullReferenceException. Это происходит во время выполнения при null разыменовании; то есть при оценке переменной во время выполнения, но переменная ссылается на null. Это наиболее распространенное исключение в экосистеме .NET. Создатель null, сэр Тони Хоар, называется null "миллиардной ошибкой".

В следующем примере переменной FooBar было присвоено значение null и она сразу же была разыменована, в результате чего обнаружилась проблема:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

С увеличением размера и сложности приложения разработчику становится все сложнее выявить эту проблему. Поиск таких потенциальных ошибок, как эта, является заданием для инструментария, и компилятор C# может в этом помочь.

Определение безопасности использования значения NULL

Термин null safety определяет набор возможностей, относящихся к nullable типам, которые помогают уменьшить количество возможных NullReferenceException случаев.

Учитывая предыдущий FooBar пример, можно избежатьNullReferenceException, проверяя, была fooBar ли null переменная перед разыменовыванием:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

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

Типы, допускающие значения NULL

До C# версии 2.0 значение NULL допускали только ссылочные типы. Типы значений, такие как int или DateTimeне могут быть null. Если эти типы инициализируются без значения, для них возвращается стандартное значение (default). В случае int это 0. Для DateTime это DateTime.MinValue.

Ссылочные типы, экземпляры которых созданы без начальных значений, работают по-другому. Значением default для всех ссылочных типов является null.

Рассмотрим следующий фрагмент C#.

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

В предыдущем примере:

  • first имеет значение null, поскольку ссылочный тип string был объявлен без присваивания.
  • При объявлении second присваивается string.Empty. У объекта никогда не было присваивания null.
  • third несмотря 0 на то, что не назначено. Это переменная struct (тип значения) и ее значение default равно 0.
  • date неинициализирован, но его default значение равно System.DateTime.MinValue.

Начиная с C# 2.0, можно определить типы значений, допускающие значение NULL , с помощью Nullable<T> (или T? для краткой версии). Это позволяет сделать допустимым значение NULL для типов значений. Рассмотрим следующий фрагмент C#.

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

В предыдущем примере:

  • first имеет значение null, поскольку тип значения, допускающий значение NULL, не инициализирован.
  • При объявлении second присваивается null.
  • third имеет значение null, так как значение default для Nullable<int> равно null.
  • fourth имеет значение 0, так как выражение new() вызывает конструктор Nullable<int>, а int по умолчанию имеет значение 0.

В C# 8.0 появились ссылочные типы, допускающие значение NULL, которые позволяют выразить намерение, что ссылочный тип может быть null, либо всегда неnull может быть NULL. Возможно, вы думаете: "Я думал, что все ссылочные типы являются пустыми!" Ты не ошибаешься, и они. Эта функция позволяет выразить намерение, которое компилятор затем пытается применить. Тот же синтаксис T? указывает на то, что ссылочный тип должен допускать значение NULL.

Рассмотрим следующий фрагмент C#.

#nullable enable

string first = string.Empty;
string second;
string? third;

Учитывая предыдущий пример, компилятор определяет намерение следующим образом:

  • first не никогдаnull, так как это определенно присвоено.
  • second никогда не должно быть null, даже если это изначально null. При вычислении переменной second до присвоения значения выдается предупреждение компилятора, так как она не инициализирована.
  • third Может бытьnull. Например, он может указывать на System.String, но может указывать на null. Любой из этих вариантов приемлем. Компилятор помогает, предупреждая о разыменовании переменной third без предварительной проверки того, что она не равна NULL.

Внимание

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

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

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

  • disable. Компилятор ведет себя так, как в C# 7.3 и более ранних версиях.
  • enable. Компилятор включает все средства анализа пустых ссылок и все языковые функции.
  • warnings. Компилятор выполняет весь анализ значений NULL и выдает предупреждения, когда код может разыменовывать переменные со значением null.
  • annotations. Компилятор не выполняет анализ значений NULL и не выдает предупреждения, когда код может разыменовывать переменные со значением null, но можно добавить заметки к коду, используя ссылочные типы ?, допускающие значение NULL, и операторы обеспечения допустимости значений NULL (!).

Этот модуль ограничен контекстами, disable допускаемыми значением enable NULL. Дополнительные сведения см. в ссылочных типах, допускающих значение NULL: контексты, допускающие значение NULL.

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

В файле проекта C# (CSPROJ) добавьте дочерний <Nullable> узел в <Project> элемент (или добавьте к существующему <PropertyGroup>). Это приведет к применению допускающего значение NULL контекста enable ко всему проекту.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

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

#nullable enable

Предыдущая директива компилятора C# функционально эквивалентна конфигурации проекта, но она ограничена файлом, в котором он находится. Дополнительные сведения см. в разделе ссылочных типов, допускающих значение NULL: контексты, допускающие значение NULL (документация)

Внимание

Контекст, допускающий значение NULL, включен в CSPROJ-файл по умолчанию во всех шаблонах проектов C#, начиная с .NET 6.0 и выше.

При включении контекста, допускающего значение NULL, появляются новые предупреждения. Рассмотрим предыдущий FooBar пример, имеющий два предупреждения при анализе в контексте, допускающего значение NULL:

  1. Строка FooBar fooBar = null; содержит предупреждение о null назначении: Предупреждение C# CS8600: преобразование null-литерала или возможного значения null в тип, не допускающий NULL-значения.

    Снимок экрана предупреждения C# CS8600: преобразование значения null или возможного значения null в ненулевой тип.

  2. Строка _ = fooBar.ToString(); также содержит предупреждение. На этот раз компилятор обеспокоен, что fooBar может быть null: предупреждение C# CS8602: разыменование возможной null-ссылки.

    Снимок экрана: предупреждение C# CS8602: разыменовка возможно пустой ссылки.

Внимание

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

Итоги

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