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