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

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

// 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. Возможно, вы думаете: "Я думал, что все ссылочные типы являются пустыми!" Ты не ошибаешься, и они. Эта функция позволяет выразить намерение, которое компилятор попытается реализовать принудительно. Тот же синтаксис 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 (!).

Этот модуль ограничен контекстами, enable допускаемыми значением disable 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. Разыменование вероятной пустой ссылки.

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

Внимание

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

Итоги

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