Функциональный C#: Ссылочные Типы, Не Допускающие Значения Null

Третья статья из серии «Функциональный C#».



Ненулевые ссылочные типы в C# — современное состояние

Давайте посмотрим на этот пример:
  
  
  
  
  
  
  
  
  
  
  
   

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

Выглядит знакомо, не так ли? Какие проблемы можно найти в этом коде? Проблема здесь в том, что мы не знаем, может ли метод GetById возвращать значение null. Если метод возвращает значение NULL для некоторых идентификаторов, мы рискуем получить исключение NullReferenceException во время выполнения.

Хуже того, может пройти значительное время между присвоением клиенту значения null и использованием этого объекта нами.

Такой код сложно отлаживать, т.к.

выяснить, где именно объекту был присвоен null, будет непросто.

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

Конечно, самую быструю обратную связь мог бы обеспечить компилятор.

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

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

Здесь тип Клиент! означает ненулевой тип, т.е.

тип, объекты которого не могут быть нулевыми ни при каких обстоятельствах.

Или еще лучше:

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

Те.

сделать все ссылочные типы ненулевыми по умолчанию (точно так же, как типы значений сейчас), и если нам нужен нулевой тип, то укажите это явно, вот так:

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

К сожалению, ненулевые ссылочные типы нельзя добавить в C# на уровне языка.

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

В новых версиях C# ненулевые ссылочные типы могут быть добавлено на уровне предупреждения , но пока с этим нововведением не все гладко.

И хотя мы не можем заставить компилятор обнаружить ошибки, связанные с некорректным использованием null, мы можем решить проблему, используя обходной путь.

Давайте посмотрим на код класса 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; } }

Мы вынесли всю проверку, связанную с электронными письмами и клиентами, в отдельные классы, но ничего не могли сделать с наличными чеками.

Как видите, это единственные оставшиеся чеки.



Удаление проверок на ноль

Так как же нам от них избавиться? Использование рерайтера IL. Мы можем использовать пакет NuGet NullGuard.Фоди , который был создан специально для этой цели: он добавляет проверки на null в ваш код, заставляя ваши классы генерировать исключения, если null поступает в качестве входного параметра или возвращается как результат метода.

Чтобы начать его использовать, установите пакет NullGuard.Fody и отметьте свою сборку атрибутом

[assembly: NullGuard(ValidationFlags.All)]

С этого момента все методы и свойства в сборке будут автоматически получать нулевую проверку для всех входящих и исходящих параметров.

Наш класс 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; } }

Вот что мы получаем в результате благодаря IL-рерайтеру:

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; } }

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



Что теперь делать с нулем?

Что, если нам нужен ноль? Мы можем использовать структуру 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. Это сообщает переписчику, что ему не следует добавлять проверки на null для этих конкретных параметров.

Используя Maybe, мы можем написать следующий код:

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

И теперь при чтении кода становится очевидно, что метод GetById может возвращать null. Нет необходимости смотреть код метода, чтобы понять его семантику.

Более того, теперь мы не можем случайно спутать нулевой тип с ненулевым; такой код приведет к ошибке компилятора:

Maybe<Customer> customer = _repository.GetById(id); ProcessCustomer(customer); // Compiler error private void ProcessCustomer(Customer customer) { // Method body }

Конечно, не все сборки имеет смысл менять с помощью рерайтера.

Например, применять подобные правила в сборке с WFP, вероятно, будет не лучшей идеей, поскольку слишком многие системные компоненты в ней обнуляются по своей природе.

В таких условиях проверка на null не имеет смысла, потому что с большинством этих nalов вы все равно ничего не сможете сделать.

Что касается доменных сборок, то их определенно стоит улучшить таким образом.

Более того, именно доменные классы получат наибольшую выгоду от этого подхода.



Заключение

Преимущества описанного подхода:
  • Это помогает уменьшить количество ошибок, обеспечивая быструю обратную связь на случай, если значение null неожиданно втиснется туда, где оно не ожидалось.

  • Значительно улучшает читаемость кода.

    Нет необходимости углубляться в метод, чтобы понять, может ли он вернуть значение null.

  • Проверки на null теперь существуют в коде по умолчанию.

    Методы и свойства классов защищены от нулевых значений; нет необходимости писать шаблонный код для проверки наличных.



Другие статьи из серии

Английская версия статьи: Функциональный C#: ссылочные типы, не допускающие значения NULL Теги: #C++ #функциональное программирование #программирование #.

NET #C++ #ООП #Функциональное программирование

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.