Общие сведения о допустимости значений 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.