Кроме стандартных значений типа чисел, строк, язык C# имеет специальное значение - null, которое фактически указывает на отсутствие значения как такового, отсутствие данных. До сих пор значение null выступает как значение по умолчанию для ссылочных типов.
До версии C# 8.0 всем ссылочным типам спокойно можно было присваивать значение null:
string name = null; Console.WriteLine(name);
Но начиная с версии C# 8.0 в язык была введена концепция ссылочных nullable-типов (nullable reference types) и nullable aware context - nullable-контекст, в котором можно использовать ссылочные nullable-типы.
Чтобы определить переменную/параметр ссылочного типа, как переменную/параметр, которым можно присваивать значение null, после названия типа указывается знак вопроса ?
string? name = null; Console.WriteLine(name); // ничего не выведет
К примеру встроенный метод Console.ReadLine()
. который считывает с консоли строку, возвращает именно значение string?, а не просто string
:
string? name = Console.ReadLine();
Зачем нужно это значение null? В различных ситуациях бывает удобно, чтобы объекты могли принимать значение null, то есть были бы не определены. Стандартный пример - работа с базой данных, которая может содержать значения null. И мы можем заранее не знать, что мы получим из базы данных - какое-то определенное значение или же null.
При этом подобные ссылочные типы, которые допускают присвоение значения null
, доступно только в nullable-контексте. Для nullable-контекста характерны следующие особенности:
Переменную ссылочного типа следует инициализировать конкретным значением, ей не следует присваивать значение null
Переменной ссылочного nullable-типа можно присвоить значение null, но перед использование необходимо проверять ее на значение null.
Начиная с .NET 6 и C# 10 nullable-контекст по умолчанию распространяется на все файлы кода в проекта. Например, если мы наберем в Visual Studio 2022 для проекта .NET 6 предыдущий пример, то мы столкнемся с предупреждением:
Хотя nullable-контекст - это опция, которой мы можем управлять. Так, откроем файл проекта. Для этого либо двойным кликом левой кнопкой мыши нажмем на проект, либо нажмем на проект правой кнопкой мыши и в появившемся меню выберем пункт Edit Project File
После этого Visual Studio откроет нам файл проекта, который будет выглядеть примерно следующим образом:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Здесь строка
<Nullable>enable</Nullable>
точнее элемент <Nullable>
со значением enable указывает,
что эта nullable-контекст будет распространяться на весь проект.
Чем так плох null? Дело в том, что это значение означает, отсутствие данных. Но, допустим, у нас есть ситуация, когда мы получаем извне некоторую строку и
пытаемся обратиться к ее функциональности. Например, в примере ниже у строки вызывается метод ToUpper()
, который переводит все символы строки в верхний регистр:
string name = null; PrintUpper(name); // ! NullReferenceException void PrintUpper(string text) { Console.WriteLine(text.ToUpper()); }
Здесь при выполнении вызова PrintUpper(name)
мы столкнемся с исключением NullReferenceException,
и программа аварийно завершит свою работу. Кто-то может сказать, что ситуация искуственная - мы же явно знаем, что в функцию передается
null. Однако в реальности данные могут приходить извне, например, из базы данных, откуда-то из сети и т.д. И мы можем явно не знать, есть ли в реальности данные или нет.
И использование ссылочных nullable-типов позволяет частично решить эту ситуацию. Частично - поскольку предупреждения все равно не мешают нам скомпилировать и
запустить программу выше. Однако nullable-контекст позволяет воспользоваться возможностями статического анализа, благодаря которому можно увидеть потенциально опасные
куски кода, где мы можем столкнуться с NullReferenceException.
Кроме того, есть вероятность, что Microsoft изменит отношение в отношении null и NullReferenceException, и подобные предупреждения превратятся в будущих версиях в ошибки, поэтому лучше уже сейчас быть к этому готовым
Например, изменим предыдущий пример следующим образом:
string? name = null; PrintUpper(name); // void PrintUpper(string? text) { Console.WriteLine(text.ToUpper()); }
Здесь статический анализ подскажет, что в методе PrintUpper потенциально опасная ситуация, поскольку параметр text
может быть равен null.
Для отключения nullable-контекста в файле конфигурации проекта достаточно изменить значение опции Nullable, например, на "disable":
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>disable</Nullable> </PropertyGroup> </Project>
Отключив nullable-контекст, мы больше не сможем использовать в файлах кода в проекте ссылочные nullable-типы и соответственно воспользоваться встроенным статическим анализом потенциально опасных ситуаций, где можно столкнуться с NullReferenceException.
Мы также можем включить nullable-контекст на урове отдельных участков кода с помощью директивы #nullable enable. Допустим, глобально у нас отключен nullable-контекст:
<Nullable>disable</Nullable>
Определим в файле Program.cs следующий код:
#nullable enable // включаем nullable-контекст на уровне файла string? name = null; PrintUpper(name); void PrintUpper(string? text) { Console.WriteLine(text.ToUpper()); }
Первая строка позволяет включить на уровне всего файла nullable-контекст.
Оператор ! (null-forgiving operator) позволяет указать, что переменная ссылочного типа не равна null:
string? name = null; PrintUpper(name!); void PrintUpper(string text) { if(text == null) Console.WriteLine("null"); else Console.WriteLine(text.ToUpper()); }
Здесь если бы мы не использовали оператор !, а написали бы PrintUpper(name)
, то компилятор высветил бы нам предупреждение.
Но в самом методе мы итак проверяем на null, поэтому даже если в метод передается null, то мы не столкнемся ни с какими проблемами. И чтобы убрать
ненужное предупреждение, применяется данный оператор. То есть данный оператор не оказывает никакого влияния во время выполнения кода и предназначен только
для статического анализа компилятора. Во время выполнения выражение name!
будет аналогично значению name
С помощью специальной директивы #nullable disable можно исключить какой-то определенный кусок кода из nullable-контекста. Например:
#nullable disable string text = null; // здесь nullable-контекст не действует #nullable restore string? name = null; // здесь nullable-контекст снова действует
Любой код между директивами #nullable disable и #nullable restore будет исключен из nullable-контекста и тем самым не будет подлежать статическому анализу.