Общие сведения о допустимости значений 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:
Строка
FooBar fooBar = null;содержит предупреждение оnullназначении: Предупреждение C# CS8600: преобразование null-литерала или возможного значения null в тип, не допускающий NULL-значения.Строка
_ = fooBar.ToString();также содержит предупреждение. На этот раз компилятор обеспокоен, чтоfooBarможет быть null: предупреждение C# CS8602: разыменование возможной null-ссылки.
Внимание
Нет гарантированной безопасности null, даже если вы реагируете на все предупреждения и избавляетесь от них. Существуют некоторые ограниченные сценарии, которые будут передавать анализ компилятора, но приводят к выполнению NullReferenceException.
Итоги
Из этого урока вы узнали, как включить в C# контекст, допускающий значение NULL, для защиты от NullReferenceException. Из следующего урока вы узнаете больше о явном выражении намерений в контексте, допускающем значение NULL.