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