Nullable reference types в Unity простыми словами

от автора

С выходом Unity 2021 LTS в полной степени стал доступен Nullable reference types. Коротко расскажу о том, как включить поддержку Nullables в Unity, и с какими проблемами вы можете встретиться.

Коротко о Nullable reference types

Если вы уже знакомы с данной концепцией, то можете сразу перейти к разделу “Как включить Null reference type в проекте Unity”.

Nullable reference types позволяет четко определять какие переменные ссылочного типа могут принимать значения null, а какие — нет. И еще на стадии написания кода находить уязвимые места, которые могу приводить Null reference exception.

public class Person     {     private string _name;     private string? _occupation; }

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

public class Person     {     private string _name;     private string? _occupation;      public Person(string name)     {         _name = name;     } }

Или даже так.

public class Person     {     private readonly string _name;     private string? _occupation;      public Person(string name, string? occupation)     {         _name = name;         _occupation = occupation;     } }

Это особенно полезно при тесной работе в команде. Коллега, создавая объект, сразу из конструктора поймёт поведение класса. Также мы можем более точно определять контракт интерфейса.

public interface IPerson {     string Name { get; }     string? Occupation { get; } }

Реализуя данный интерфейс, получим такой класс

public class Person : IPerson {     public string Name { get; }          public string? Occupation { get; private set; }      public Person(string name, string? occupation)     {         Name = name;         Occupation = occupation;     } }

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

public class People {     private readonly Dictionary<string, IPerson> _persons;      public People(Dictionary<string, IPerson> persons)     {         _persons = persons;     }      public string GetOccupationByName(string name)     {         if (_persons.TryGetValue(name, out var person))         {             return person.Occupation;         }          return null;     } }

Тут мы получим сразу два предупреждения. Мы утверждаем, что метод GetOccupationByName() возвращает string, без возможного null. Но при этом возвращаем null в конце, а так же возвращаем person.Occupation, где уже Occupation может быть null. Задаваемые же нами правила заставляют нас писать код более корректно. Либо указать, что GetOccupationByName() возвращает string?, либо, к примеру, ввести метод TryGetOccupationByName.

public bool TryGetOccupationByName(     string name,     [MaybeNullWhen(false)] out string occupation) {     if (_persons.TryGetValue(name, out var person))     {         occupation = person.Occupation;         return occupation is not null;     }      occupation = null;     return false; }

Думаю, вы обратили внимание на атрибут MaybeNullWhen(bool). Интуитивно понятно, что он позволяет указывать для occupation null, при результате false.

Как включить Null reference type в проекте Unity

Конечно, мы всегда можем управлять доступностью Nullable reference type в конкретном месте через директивы #nullable enable, disable или restore. Но как включить эту опцию для всего проекта Unity по умолчанию? 

На данный момент нет возможности это сделать напрямую через настройки в Assembly Definition в самом Unity или через настройки проекта в IDE.

Чтобы включить Nullable reference types для всего проекта Unity расположите файл Directory.Build.props в корневой папке. Содержимое должно быть таким.

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

Для регулирования отдельных сборок разместите файл csc.rsp с текстом ‘-nullable:enable’ рядом с файлом сборки.

Если вы используете Visual Studio, не должно возникнуть никаких проблем. Для Rider от JetBrains проверьте ваши настройки MSBuild. Откройте настройки File -> Settings. Найдите там раздел “Build, Execution, Deployment” -> “Toolset and Build”, пункт “MSBuild version”. Версия MSBuild должна быть достаточно свежая. У меня с 17.0 все работает.

Сериализуемые поля в MonoBehaviour и ScriptableObject

Для MonoBehaviour и ScriptableObject есть возможность указываться значения некоторых полей в редакторе через инспектор. Получается, что инициализация полей через конструктор не производится. А значит будут предупреждения.

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

public class Card : MonoBehaviour { #nullable disable    [SerializeField] private string _description;    [SerializeField] private Sprite _icon; #nullable restore }

При это для указаных полей предупреждения перестают работать. К примеру следующий код у анализатора уже не будет вызывать никаких вопросов.

private void Awake() {    _icon = null; }

Поэтому советую применять второй вариант: назначать всем полям значение null!.

public class Card : MonoBehaviour {     [SerializeField] private string _description = null!;     [SerializeField] private Sprite _icon = null!;      private void Awake()     {         _icon = null;     } }

Здесь в методе Awake() мы уже получим предупреждение.

Стоит коротко упомянуть, из-за того, что для MonoBehaviour нельзя указывать конструкторы, в некоторых случаях в их роли выступаю обычные методы. Тут мы столкнемся с теми же самыми проблемами. Способы их решение тут точно такие же.

public class Cube : MonoBehaviour {    private ICollidingService _collidingService = null!;       public void Initialize(ICollidingService collidingService)    {        _collidingService = collidingService;    } }

Отсутствие поддержки в библиотеке Unity

На мой взгляд, пока это самая большая проблема, которая может отпугнуть разработчика Unity от использования Nullable reference types. К примеру.

public class Card : MonoBehaviour {     [SerializeField] private Card _parent = null!;          private void Awake()     {         _parent = transform.parent.GetComponent<Card>();     } }

Очевидно, что тут мы должны получить предупреждение, т.к. GetComponent<T> может возвращать null, т.е. T?, но это, к сожалению, пока никак не отображено в библиотеке Unity. Так же печалит факт отсутствия хоть каких либо анонсов исправления этой ситуации.

Заключение

Несмотря на перечисленные проблемы в Unity, я всё же считаю, что стоит начать присматриваться к этому новому функционалу. Постепенно внедрять в свои проекты шаг за шагом. Или пока ограничиваться локальным применением в новых фичах. 

Благодаря Nullable reference types код становится более последовательным и помогает избегать некоторых будущих багов. Эффект особенно заметен, если разработка сильно пересекается между программистами. К примеру, один специалист все еще пишет реализацию интерфейса, где ещё полным полно заглушек, а другой — уже использует этот класс в другом месте. При этом всё ещё не может посмотреть его конечную реализацию.

И конечно же мы ждем от Unity, Rider и Visual Studio каких-то готовых решений упомянутых проблем.


ссылка на оригинал статьи https://habr.com/ru/articles/703518/