Функциональный C#: ненулевые ссылочные типы | OTUS

Функциональный C#: ненулевые ссылочные типы

Чем быстрее мы получаем обратную связь, тем меньше времени у нас для исправления ошибки. Естественно, максимально быстрый ответ мы сможем получить лишь от компилятора. Разве было бы не здорово — написать код, и пусть компилятор выполнит все нужные проверки?

Давайте рассмотрим следующий пример:

Customer customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Проблема здесь в том, что нам неизвестно наверняка, вернёт ли метод GetById ненулевой экземпляр. Ведь, несмотря ни на что, существует шанс, что мы получим null и исключение NullReferenceException. Ситуация может ухудшиться, если перед использованием переменной customer она ещё не будет инициализирована с помощью метода GetById — отловить такую ошибку будет очень сложно.

Customer! customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Здесь Customer! означает ненулевой тип, то есть экземпляры данного класса в принципе не могут принимать значение null. Естественно, было бы круто, быть уверенным в том, что компилятор укажет нам, что какой-нибудь кусок кода может вернуть null. Или ещё лучше:

Customer customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

То есть, речь идёт о том, чтобы сделать по умолчанию все ссылочные типы ненулевыми (как и типы значений). А если мы захотим применить нулевой тип, то воспользуемся следующим кодом:

Customer? customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Можно ли вообще представить мир без раздражающих NullReferenceException? К сожалению, нет и необнуляемые ссылочные типы не могут быть введены в Сишарп как признак языка. Но не стоит волноваться. Хотя мы и не можем заставить наш компилятор помочь нам и применять мощности ненулевых ссылочных типов, существуют обходные пути, о которых сейчас и поговорим.

Посмотрим на класс Customer в коде ниже:

public class Customer
{
    public CustomerName Name { get; private set; }
    public Email Email { get; private set; }

    public Customer(CustomerName name, Email email)
    {
        if (name == null)
            throw new ArgumentNullException("name");
        if (email == null)
            throw new ArgumentNullException("email");

        Name = name;
        Email = email;
    }

    public void ChangeName(CustomerName name)
    {
        if (name == null)
            throw new ArgumentNullException("name");

        Name = name;
    }

    public void ChangeEmail(Email email)
    {
        if (email == null)
            throw new ArgumentNullException("email");

        Email = email;
    }
}

Как нам избавиться от проверок на null?

Каким же образом мы можем избавиться от таких проверок? Естественно, посредством IL-рерайтинга (Intermediate Language Rewrite). Существует хороший NuGet-пакет, который называют NullGuard.Fody. Давайте для начала скачаем и установим его. А потом пометим сборку следующим атрибутом:

[assembly: NullGuard(ValidationFlags.All)]

Что мы сделали? Теперь любой метод и свойство в сборке будет проверяться на null автоматически. Это значит, что можно переписать класс Customer, и выглядеть он будет изящно и просто:

public class Customer
{
    public CustomerName Name { get; private set; }
    public Email Email { get; private set; }

    public Customer(CustomerName name, Email email)
    {
        Name = name;
        Email = email;
    }

    public void ChangeName(CustomerName name)
    {
        Name = name;
    }

    public void ChangeEmail(Email email)
    {
        Email = email;
    }
}

Можно ещё проще:

public class Customer
{
    public CustomerName Name { get; set; }
    public Email Email { get; set; }

    public Customer(CustomerName name, Email email)
    {
        Name = name;
        Email = email;
    }
}

Но, несмотря на его визуальную простоту, на деле класс выглядит следующим образом:

public class Customer
{
    private CustomerName _name;
    public CustomerName Name
    {
        get
        {
            CustomerName customerName = _name;

            if (customerName == null)
                throw new InvalidOperationException();

            return customerName;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();

            _name = value;
        }
    }

    private Email _email;
    public Email Email
    {
        get
        {
            Email email = _email;

            if (email == null)
                throw new InvalidOperationException();

            return email;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();

            _email = value;
        }
    }

    public Customer(CustomerName name, Email email)
    {
        if (name == null)
            throw new ArgumentNullException(name, [NullGuard] name is null.);
        if (email == null)
            throw new ArgumentNullException(email, [NullGuard] email is null.);

        Name = name;
        Email = email;
    }
}

Как же быть со значением null?

Как же нам определить, что значение некоторого типа может быть null? Воспользуемся командой Maybe.

public struct Maybe<T>
{
    private readonly T _value;

    public T Value
    {
        get
        {
            Contracts.Require(HasValue);

            return _value;
        }
    }

    public bool HasValue
    {
        get { return _value != null; }
    }

    public bool HasNoValue
    {
        get { return !HasValue; }
    }

    private Maybe([AllowNull] T value)
    {
        _value = value;
    }

    public static implicit operator Maybe<T>([AllowNull] T value)
    {
        return new Maybe<T>(value);
    }
}

Заметьте, что входные значения для класса Maybe имеют атрибут AllowNull. Теперь можно написать следующий код, используя Maybe:

Maybe<Customer> customer = _repository.GetById(id);

В итоге стало очевидно, что метод GetById способен вернуть нулевое значение. Также в результате вы не сможете больше случайно перепутать значение, которое может принять null, с необнуляемым значением, что стало бы причиной ошибки компилятора.

Maybe<Customer> customer = _repository.GetById(id);
ProcessCustomer(customer); // Тут ошибка компилятора

private void ProcessCustomer(Customer customer)
{
    //Тело нашего метода
}

Теперь следует решить, какие сборки обработать пакетом NullGuard.Fody. Скорее всего, использование этого пакета в WPF — не самая хорошая идея, так как там есть много системных компонентов, по сути являющихся обнуляемыми. Это значит, что добавление проверок на null особого преимущества вам не даст. Но для остальных сборок метод будет актуальным.

А что насчёт статических проверок?

Быстрая обратная связь во время выполнения, конечно, неплохо, однако всё же это обратная связь во время выполнения. И было бы неплохо анализировать код быстрее, например, на этапе компиляции. Как это сделать?

Ответ заключается в атрибуте NotNull дополнения ReSharper. Применив этот атрибут к какому-либо методу и вернув нулевое значение в нём, вы получите от плагина ReSharper предупреждение. Такой способ способен неплохо облегчить жизнь, но тут есть ряд проблем: 1. ReSharper работает по методу «от обратного». Вам надо отмечать атрибутом NotNull значения, которые не могут быть нулевыми. Но гораздо удобнее было бы помечать данным атрибутом значения, которые, напротив, могли бы быть нулевыми. Таким образом по умолчанию остальные считались бы необнуляемыми. 2. Предупреждения — всего лишь предупреждения. Можно просто не обратить на них внимание, и, следовательно, пропустить их. Конечно, можно установить настройки Visual Studio таким образом, что он станет воспринимать предупреждения в качестве ошибок. Но с монадой Maybe шансов ошибиться намного меньше.

Итоги

Вышеописанный подход действительно эффективен: 1. Вы быстро найдёте баг с нулевым значением. И вам не будут постоянно надоедать NullReferenceException. 2. Вы повысите читаемость кода. Это значит, что регулярные проверки значения на null перед применением объекта мелькать везде не будут. 3. Все ваши методы будут защищены от исключения NullReferenceException по умолчанию. И не нужно будет каждый новый метод помечать атрибутом NotNull.

Источник — «Functional C#: Non-nullable reference types».

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто