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

Комментарии (5)


  1. datacompboy
    05.12.2022 13:01
    +1

    Интересно, а почему вводят новый модификатор типа для nullable, а не для non-null типа?

    Ведь разумнее же, что были типы без модификатора по сути nullable -- ну так и оставить их. Ну или хотя бы оставить семантику на время миграции:

    string implicit_nullable;
    string? explicit_nullable;
    string! explicit_nonnull;

    А уже спустя 2-3 версии (и года) сменить семантику для implicit объектов...


    1. vabka
      05.12.2022 13:19

      В решарпере можно было выбирать, как воспринимать нуллабельные объекты - оптимистично (то что без аннотаций - скорее всего не null) или пессимистично (то что без аннотации - скорее всего null).

      Вроде для NRT есть что-то похожее при взаимодействии аннотированного и не-аннотированного кода, но я не уверен.


    1. IL_Agent
      06.12.2022 10:35
      +2

      В подавляющем большинстве случаев требуется именно nonnull. Пришлось бы везде ! писать - этот некрасиво и неудобно.


  1. Tr0sT
    05.12.2022 14:56

    По-прежнему актуален и очень полезен прошлогодний доклад про nullable в unity c девгамма.


    1. denis_kondratev Автор
      07.12.2022 10:18

      Отличное видео! Можно добавить что, Александр там разбирается с тем, как можно использовать NotNull в своём проекте, т.к. от есть только в Standart 2.1. Начиная с Unity 2021 этот стандарт поддерживается