Andrey on .NET | C# 8 – Ссылочные типы, допускающие значение null

C# 8 – Ссылочные типы, допускающие значение null

C# logoИзначально ссылочные типы в C# всегда допускали использование значения null. Однако 8 версия языка в корне меняет этот подход. Теперь переменные ссылочных типов могут явно указывать допустимо ли присваивать им значение null. В этом плане их поведение становится аналогичным значимым типам. Разберемся с этим нововведением подробнее.

Зачем нужны новые типы

Рассмотрим на примере. Пусть есть некий метод GetUser(int id), который в случае успешного завершения своей работы гарантированно возвращает экземпляр объекта типа User.

До 8 версии языка C#, в общем случае, такой контракт мог быть основан только на договоренностях (или с использованием дополнительных инструментов, таких как Code Contracts). Например, все методы, имя которых начинается с Get, гарантированно возвращают экземпляр или выкидывают исключение. А все методы, имя которых начинается с Find, могут вернуть null, если ничего не найдено. Такой "устный контракт" должен поддерживаться разработчиками. Кроме того, далеко не всем методам можно дать осмысленное название, основанное на таком правиле. А расширение списка правил приведет к тому, что их труднее будет соблюдать. На практике только непосредственно вызовы таких методов будут полагаться на подобные устные соглашения. Весь остальной код будет по прежнему изобиловать проверками на null, которые по факту никогда не сработают, а также могут привести к увеличению числа тестов для их покрытия.

Данную проблему решает появившиеся в C# 8 ссылочные типы, допускающие значение null. Теперь приведенный выше в пример контракта можно определить непосредственно в коде. Более того, не только вызывающий, но и остальной код получит информацию о том, что в полученной переменной ссылочного типа не может быть значение null. А за соблюдением контракта будет следить компилятор, который даст знать об ошибках еще на этапах написания кода и компиляции.

Важно понимать, что все проверки происходят на этапе компиляции, а не во время исполнения run-timе.

Синтаксис

Определение типа

Для описания ссылочных типов, допускающих значение null, C# 8 заимствует синтаксис значимых типов и использует оператор ?:

Type?

Для ссылочного типа, который не может иметь значение null, используется просто его имя:

Type

В этом случае компилятор будет следить, чтобы в переменную такого типа нельзя было присвоить значение null или значение из другой переменной, которая допускает null.

Например, следующий код вызовет предупреждение при компиляции:

public User? FindUser(int id) { … } // метод может вернуть null
…
User sourceUser = FindUser(id); // Предупреждение при компиляции

Легко заменить, что такое изменение в правилах языка является критическим (breaking change). Поэтому в 8 версии языка поддержка данной возможности должна быть явно активирована в настройках проекта. Кроме того, вместо ошибок для указания нарушений контракта по-умолчанию используются предупреждения, что не мешает завершению компиляции.

С точки зрения компилятора, тип Type, не допускающий значение null, является подтипом типа Type?, допускающего null. Ведь Type? может содержать все, что есть в Type плюс null. Поэтому при выводе типа в ситуациях где используется как Type? так и Type результатом будет Type?.

Шаблонные методы (generics)

Ссылочные типы, допускающие значение null, нельзя указывать в качестве T – типа шаблонного метода. Однако, нет запрета на их использование в роли параметров или результата. Например:

public void DoSomething<T>(T? value)
    where T : class
{ … }

При этом необходимо указать, что T является классом. Это исключит неопределенность между ссылочным T? и Nullable<T> со значимым типом.

Оператор !

Разработчик может подсказать компилятору, что значение переменной в данном месте никогда не равно null, используя для этого оператор !. Например, в примере ниже компилятор не выдаст предупреждение или ошибку, не смотря на то, что тип переменной userName может принимать null:

// Используем string? т.к. в переменную позже может
// быть присвоено значение null
string? userName = GetUserName(id: 1);
var len = userName!.Lenght; // гарантируем что здесь userName != null

Вывод типов

При использовании ссылочных типов, допускающих значение null, есть интересная особенность: компилятор предварительно проводит анализ возможности использования null в месте вывода типа. Рассмотрим следующий пример:

User? x = FindUser(userId);
if (user == null) throw UserNotFoundException(userId);
var target = user;
…

В данном случае переменная target будет типа User, а не User?, как можно было бы предположить. Все дело в предварительно сделанной проверке на null. Поскольку в момент вывода типа переменная user не может иметь значение null, то в качестве типа target будет использован User.

Также после такой проверки переменную user можно передавать в методы, которые принимают тип User. При этом компилятор не будет выдавать предупреждения, поскольку, несмотря на тип переменной User?, её значение после проверки не может быть null.

Поддержка ссылочных типов, допускающих значение null

Компилятор C# 8 содержит два контекста для анализа ссылочных типов на возможность присваивания значения null

  • контекст аннотаций – определяет как трактуется ссылочный тип без указания ?, как допускающий или не допускающий null.
  • контекст предупреждений – определяет необходимо ли выводить предупреждения о нарушении контракта использования ссылочных типов.

В C# 8.0 по умолчанию оба контекста отключены для совместимости с уже существующим кодом, который в этом случае будет компилироваться без предупреждений.

Изменить это поведение можно добавив в файл проекта ".csproj" параметр <nullable>…</nullable>, который может принимать следующие значения:

Значение <nullable/> Ссылочные типы без "?" Предупреждения о нарушениях контракта
enable Не допускают null Выводятся
warnings Допускают null Выводятся
annotations Не допускают null Не выводятся
disable Допускают null Не выводятся

Отсутствие <nullable>…</nullable> в файле проекта аналогично <nullable>disable</nullable>.

Как видно из таблицы, самый строгий вариант это enable.

Режим warnings подразумевает что типы без ? по прежнему могут содержать null. Это может помочь при первичной адаптации существующего кода или использоваться на этапе статического анализа кода. Компилятор будет выдавать предупреждения в местах, где возможно появление NullReferenceException. Например в ситуации, когда разработчик сначала использовал свойство класса, предварительно проверив что экземпляр класса не равен null, а затем повторно обратился к свойству без такой проверки. Компилятор предупредит, что во втором случае возможно исключение.

Также настройку контекстов можно изменить в любом месте кода при помощи директивы:

#nullable (enable|disable|restore) [annotations|warnings]

Опция restore возвращает поведение к варианту, заданному в файле проекта при помощи <nullable />.

Опции annotations и warnings определяют конкекст для изменения. Значение не является обязательным и без него операция будет изменять оба контекста сразу. Например:

  • #nullable enable – включает контексты аннотаций и предупреждений.
  • #nullable restore warnings – возвращает настройку контекста предупреждений к значению, заданному в файле проекта.

Область действия директивы – до следующей директивы или до конца файла. Это позволяет постепенно переводить существующий код на использование новых ссылочных типов.

Совместимость с существующим кодом

В рамках одного проекта и решения (solution) может сосуществовать код с "классическими" и новыми ссылочными типами. Не говоря уже о сочетаниях различных проектов и библиотек. Как следствие возникает вопрос: по каким правилам происходит взаимодействие классических и новых ссылочных типов?

Существуют следующие правила:

  1. При пересечении границы между "классическим" и новым кодом ссылочные переменные будет трактоваться согласно текущим правилам. При этом, в случае передачи из нового кода в "классический", это будет сделано независимо от того, допускает ли в новом коде тип значение null или нет.
  2. Контроль со стороны компилятора будет только в той части кода, где явно включена поддержка ссылочных типов, допускающих значение null.

Здесь стоит обратить внимание на то, что значения, передаваемые из "классического" кода в новый будут трактоваться как не допускающие значение null. Это означает что перед дальнейшим использованием лучше всего присваивать такие значения в переменные с явным указанием типа, который допукает null. и избегать использования var. Например:

// GetUserName объявлена и реализована в "классическом" коде
// и возращает "string", но присваиваем в "string?"
string? userName = GetUserName(id: 1);

Еще одно правило, которое позволит уменьшить число проблем, при совместном использованнии "классического" и нового кода: если объявление интерфейса использует ссылочные типы, допускающие значение null, то и все реализации должны его использовать. Это актуально при рефакторинге существующего кода.

Вывод ошибок при нарушении контракта

При нарушении контракта ссылочных типов C# 8.0 выводит предупреждение. Это не мешает успешной сборке проекта. Но Visual Studio позволяет перевести предупреждения в разряд ошибки при помощи любого из следующих вариантов:

Настройка в файле проекте

В файле проекта ".csproj" необходимо перечислить коды предупреждений, которые необходимо трактовать ��ак ошибки. Например:

<WarningsAsErrors>CS8600;CS8625</WarningsAsErrors>

Файл .editorconfig

Visual Studio 2019 поддерживает файл ".editorconfig", в котором можно указать какие предупреждения должны обработываться как ошибки. Например:

[*.cs]

# CS8600: Converting null literal or possible null value to non-nullable type.
dotnet_diagnostic.CS8600.severity = error

Добавлять коды можно как вручную, так и через контекстное меню редактора:

editorconfig context menu

StyleCop и правила кода

Если в проекте используется StyleCop, то необходимо сделать изменения в файла стилей кода ".ruleset". Открыв его для редактирования необходимо отметить требуемые предупреждения как ошибки. Для упрощения можно воспользоваться окном поиска, введя в него код конкретной ошибки или слово "null".

Code style editor

Список кодов предупреждений, связанных с ссылочными типами, допускающими значение null

Код Описание
CS8073 The result of the expression is always the same since a value of this type is never equal to ‘null’
CS8597 Thrown value may be null.
CS8600 Converting null literal or possible null value to non-nullable type.
CS8601 Possible null reference assignment.
CS8602 Dereference of a possibly null reference.
CS8603 Possible null reference return.
CS8604 Possible null reference argument.
CS8605 Unboxing a possibly null value.
CS8606 Possible null reference assignment to iteration variable
CS8607 A possible null value may not be passed to a target marked with the [DisallowNull] attribute
CS8608 Nullability of reference types in type doesn’t match overridden member.
CS8609 Nullability of reference types in return type doesn’t match overridden member.
CS8610 Nullability of reference types in type of parameter doesn’t match overridden member.
CS8611 Nullability of reference types in type of parameter doesn’t match partial method declaration.
CS8612 Nullability of reference types in type doesn’t match implicitly implemented member.
CS8613 Nullability of reference types in return type doesn’t match implicitly implemented member.
CS8614 Nullability of reference types in type of parameter doesn’t match implicitly implemented member.
CS8615 Nullability of reference types in type doesn’t match implemented member.
CS8616 Nullability of reference types in return type doesn’t match implemented member.
CS8617 Nullability of reference types in type of parameter doesn’t match implemented member.
CS8618 Non-nullable field is uninitialized. Consider declaring as nullable.
CS8619 Nullability of reference types in value doesn’t match target type.
CS8620 Argument cannot be used for parameter due to differences in the nullability of reference types.
CS8621 Nullability of reference types in return type doesn’t match the target delegate.
CS8622 Nullability of reference types in type of parameter doesn’t match the target delegate.
CS8624 Argument cannot be used as an output for parameter due to differences in the nullability of reference types.
CS8625 Cannot convert null literal to non-nullable reference type.
CS8626 The ‘as’ operator may produce a null value for a type parameter.
CS8629 Nullable value type may be null.
CS8631 The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn’t match constraint type.
CS8632 The annotation for nullable reference types should only be used in code within a ‘#nullable’ annotations context.
CS8633 Nullability in constraints for type parameter doesn’t match the constraints for type parameter in implicitly implemented interface method’.
CS8634 The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn’t match ‘class’ constraint.
CS8638 Conditional access may produce a null value for a type parameter.
CS8643 Nullability of reference types in explicit interface specifier doesn’t match interface implemented by the type.
CS8644 Type does not implement interface member. Nullability of reference types in interface implemented by the base type doesn’t match.
CS8645 Interface is already listed in the interface list with different nullability of reference types.
CS8653 A default expression introduces a null value for a type parameter.
CS8654 A null literal introduces a null value for a type parameter.
CS8655 The switch expression does not handle some null inputs.
CS8667 Partial method declarations have inconsistent nullability in constraints for type parameter
CS8714 The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn’t match ‘notnull’ constraint.

Комментарии (3) -

Вероятно, код
<code>
User? x = FindUser(userId);
if (user != null) throw UserNotFoundException(userId);
var target = user;
</code>
надо заменить кодом
<code>
User? user = FindUser(userId);
if (user == null) throw UserNotFoundException(userId);
var target = user;

Alex Спасибо что заметили опечатку.

Добавить комментарий