Null и ссылочные типы

Последнее обновление: 15.11.2021

Кроме стандартных значений типа чисел, строк, язык 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 reference types in C# и .NET

Хотя nullable-контекст - это опция, которой мы можем управлять. Так, откроем файл проекта. Для этого либо двойным кликом левой кнопкой мыши нажмем на проект, либо нажмем на проект правой кнопкой мыши и в появившемся меню выберем пункт Edit Project File

nullable enable в C# в Visual Studio

После этого 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 static analyse в C# в Visual Studio

Отключение nullable-контекста

Для отключения 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-контекст на урове отдельных участков кода с помощью директивы #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-контекст.

#nullable enable в C# и .NET

Оператор ! (null-forgiving operator)

Оператор ! (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-контекста

С помощью специальной директивы #nullable disable можно исключить какой-то определенный кусок кода из nullable-контекста. Например:

#nullable disable
	string text = null;	// здесь nullable-контекст не действует
#nullable restore

string? name = null;   // здесь nullable-контекст снова действует

Любой код между директивами #nullable disable и #nullable restore будет исключен из nullable-контекста и тем самым не будет подлежать статическому анализу.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850