С выходом 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 каких-то готовых решений упомянутых проблем.
