Стратегии миграции, допускающие значение NULL

Подсказка

Запуск нового проекта? Новые проекты, созданные из .NET 6 или более поздних шаблонов, уже имеют набор <Nullable>enable</Nullable>. Вам не нужна стратегия миграции: перейдите к разделу "Разрешить предупреждения, допускающие значение NULL".

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

При включении ссылочных типов, допускающих значение NULL, в большом проекте, запущенном до появления ссылочных типов, допускающих значение NULL, компилятор одновременно создает множество предупреждений. Миграция — это поэтапная организация работы: выбор контекста по умолчанию, показ предупреждений по одному файлу или по одному разделу и постепенный переход к <Nullable>enable</Nullable> во всём проекте. Правильная последовательность зависит от того, насколько активна база кода и сколько рисков вы можете принять в одном проходе.

Конечное состояние во всех случаях одинаково: проект задаёт <Nullable>enable</Nullable> и не содержит директив препроцессора #nullable.

Выбор контекста по умолчанию

Контекст, допускающий значение NULL, имеет два независимых флага: заметки (объявляется ли ? ссылочный тип, допускающий значение NULL), и предупреждения (выдает ли компилятор диагностику). Задайте их вместе как одно <Nullable> значение:

Значение по умолчанию Annotations Предупреждения лучше всего подходит для
disable (неявное) off off Стабильные библиотеки, для которых на этом этапе не планируется работа над новыми функциями.
enable on on Активные базы кода с частыми новыми файлами. Новый код включён по умолчанию.
warnings off on Двухэтапная миграция: сначала устраните предупреждения, затем добавьте аннотации.
annotations on off Аннотируйте публичный API перед исправлением внутренних предупреждений.

Выберите стратегию, которая лучше всего соответствует целям миграции проекта:

  • Отключить как значение по умолчанию. Задайте <Nullable>disable</Nullable> и добавьте #nullable enable в верхней части каждого файла при его переносе. Существующие файлы остаются вне контекста nullable, пока вы их не затронете. Этот вариант имеет наименьшее трение для стабильных библиотек, так как новая функция работает редко.
  • Включите в качестве значения по умолчанию. Установите <Nullable>enable</Nullable> и добавьте #nullable disable в начало каждого файла, который вы еще не перенесли. Каждый новый файл изначально создаётся с учётом nullable-аннотаций, поэтому объём незавершённой миграции может только сокращаться. Этот выбор лучше, если разработка активна.
  • Предупреждения по умолчанию. Задайте <Nullable>warnings</Nullable>. Выберите этот параметр по умолчанию для двухэтапной миграции: предупреждения об адресе, в то время как каждый ссылочный тип по-прежнему рассматривается как невнимательный, а затем включите заметки. Разделение между двумя этапами позволяет сосредоточиться на каждом шаге.
  • Аннотации по умолчанию. Задайте <Nullable>annotations</Nullable>. Начните с аннотирования общедоступного API (? для членов, которые допускают null), прежде чем разбираться с предупреждениями. Компилятор пока не выдает предупреждения, поэтому можно урегулировать поверхность API без отвлекающих факторов.

Файл проекта определяет глобальное значение по умолчанию. #nullable Директивы препроцессора переопределяют значение по умолчанию для региона кода:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

В файлах исходного кода директива включает или отключает для области действие параметра проекта, определяющего допустимость значений NULL:

#nullable disable
public static class LegacyHelper
{
    // This file is nullable-oblivious. Reference types use the legacy rules.
    public static string GetGreeting(string name) =>
        name == null ? "hello" : $"hello {name}";
}
#nullable restore

#nullable enable
public static class MigratedHelper
{
    // This file is fully migrated. Reference types are non-nullable by default.
    public static string GetGreeting(string? name) =>
        name is null ? "hello" : $"hello {name}";
}
#nullable restore

Перенос файла по файлу

Самый предсказуемый способ миграции большого проекта — включать предупреждения или аннотации для каждого файла отдельно. Шаблон одинаков независимо от выбранного по умолчанию:

  1. Выберите файл. Начните с наиболее глубоких листовых типов в графе зависимостей, а затем переходите к внешним уровням. Аннотирование типа вызывает новые предупреждения в вызывающем коде, поэтому работа снизу вверх сводит к минимуму объём переделок.
  2. Добавьте директиву #nullable, которая подключает файл к новому поведению. Используйте #nullable enable, если нужны оба флага. Используется #nullable enable warnings только для предупреждения.
  3. Устраните предупреждения в файле с помощью методов, описанных в разделе Устранение предупреждений о допустимости значений NULL.
  4. Повторите для следующего файла.
  5. Если директива задана в каждом файле проекта, удалите эти директивы и установите <Nullable>enable</Nullable> на уровне проекта.

Если у вашей базы кода уже есть <Nullable>enable</Nullable>, вы движетесь в противоположном направлении. Отключайте предупреждения в неигрированных файлах до тех пор, пока не будете готовы. Используйте #nullable disable, чтобы исключить файлы, затем удаляйте подавления по одному.

Миграция на двух этапах

Двухэтапная миграция отделяет два типа работы, к которым относятся ссылочные типы, допускающие значение NULL. Вы можете расположить фазы в любом порядке, в зависимости от того, какая форма стабильности для вас важнее.

Предупреждения сначала, а затем заметки

Начинайте с предупреждений, если приоритетом является исправление скрытых System.NullReferenceException ошибок:

  1. Этап 1: Устранение предупреждений. Задайте для проекта значение по умолчанию warnings. Ссылочные типы по-прежнему игнорируют допускаемость NULL, поэтому система типов пока не меняется. Компилятор выдает предупреждения везде, где существующий код уже может вызвать System.NullReferenceException. Добавьте проверки на null, перестройте логику выполнения или примените атрибуты, пока в проекте не останется предупреждений. Каждое исправление делает рабочий код более устойчивым даже до появления заметок.
  2. Этап 2. Добавление заметок. Переключить значение проекта по умолчанию на enable. Ссылочные типы теперь по умолчанию не допускают значения null, а локальные переменные var становятся допускающими значение null. Новые предупреждения отражают объявления, которые не соответствуют использованию переменных. Добавьте ? в типы, которые должны разрешать null. Ужесточьте API, которые должны требовать аргументы, отличные от null.

Сначала заметки, а затем предупреждения

Используйте аннотации, если приоритетом является стабилизация публичного API. Эта последовательность хорошо подходит для библиотек: вы можете поставлять аннотированные сигнатуры, чтобы пользователи видели корректные контракты, а затем устранить внутренние предупреждения в удобные для вас сроки.

  1. Этап 1. Добавление заметок. Задайте для проекта значение по умолчанию annotations. Ссылочные типы по умолчанию считаются не допускающими значения null, но компилятор не выводит предупреждения, поэтому лишний шум не мешает вам. Просмотрите общедоступный API и добавьте ? к каждому элементу, который может возвращать или принимать null. Ужесточите требования к подписям там, где этого делать не следует. Поскольку предупреждения отключены, вы можете зафиксировать структуру API в отдельных целевых коммитах, не пытаясь одновременно разбираться с реализацией.
  2. Этап 2: Устраните предупреждения. Переключить значение проекта по умолчанию на enable. Аннотации, которые вы добавили на этапе 1, теперь используются при анализе состояний допустимости NULL, поэтому предупреждения, выдаваемые компилятором, с самого начала становятся точнее: каждое из них указывает на участок кода, поведение которого не соответствует контракту, который вы уже опубликовали. Устраните их с помощью методов, описанных в предупреждениях, допускающих значение NULL.

Выбор между упорядочениями

Каждое упорядочивание разделяет этапы на более мелкие, более проверяемые диффы. Один этап изменяет только поведение, а другие изменения изменяют только типы. Недостатком является то, что вы посещаете каждый файл дважды. Для зрелого, стабильного кода, где каждое изменение связано с риском, два прохода обычно оправданы. Сначала выберите предупреждения , когда требуется ужесточить выполнение кода. Выбирайте аннотации в первую очередь, если вам важнее опубликовать стабильный контракт.

Созданный код исключен

Компилятор обрабатывает файлы, помеченные как созданные, как если бы контекст, допускающий значение NULL, был отключен независимо от параметра проекта. Файл считается сгенерированным, если выполняется любое из следующих условий:

  • Правило .editorconfig задаёт generated_code = true для файла.
  • Первый комментарий в файле содержит <auto-generated> или <auto-generated/>.
  • Имя файла начинается с TemporaryGeneratedFile_.
  • Имя файла заканчивается на .designer.cs, .generated.cs, .g.cs или .g.i.cs.

Генераторы, создающие вывод с учетом nullable-аннотаций, могут снова включить эту возможность, добавив #nullable enable в начало сгенерированного файла.

Когда вы закончите

После того как каждый файл участвует в проекте по умолчанию, и <Nullable>enable</Nullable> элемент задается:

  • Удалите каждую #nullable директиву в источнике.
  • Удалите инициализаторы null! и default!, которые вы добавили только для того, чтобы подавить предупреждения при миграции. Замените их корректной инициализацией или сделайте тип ссылочным типом, допускающим значение NULL.
  • Выборочно проверьте общедоступный API. Каждый член, который возвращает или принимает null, должен быть аннотирован с помощью ?. Заметки являются частью контракта после отправки пакета.

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