Throw выражения в C# 7

habr.png

Всем привет. Продолжаем исследовать новые возможности C# 7. Уже были рассмотрены такие темы как: сопоставление с образцом, локальные функции, кортежи. Сегодня поговорим про Throw.

В C# throw всегда был оператором. Поскольку throw — это оператор, а не выражение, существуют конструкции в C#, в которых нельзя использовать его.

  • в операторе Null-Coalescing (?)
  • в лямбда выражении
  • в условном операторе (?:)
  • в теле выражений (expression-bodied)


Чтобы исправить данные проблемы, C# 7 вводит выражения throws. Синтаксис остался таким же, как всегда использовался для операторов throw. Единственное различие заключается в том, что теперь их можно использовать в большом количестве случаев.
Давайте рассмотрим, в каких местах throw выражения будет лучше использовать. Поехали!

Тернарные операторы


До 7 версии языка C#, использование throw в тернарном операторе запрещалось, так как он был оператором. В новой версии С#, throw используется как выражение, следовательно мы можем добавлять его в тернарный оператор.

var customerInfo = HasPermission()
? ReadCustomer()
: throw new SecurityException("permission denied");


Вывод сообщения об ошибке при проверке на null


«Ссылка на объект не указывает на экземпляр объекта» и «Объект Nullable должен иметь значение», являются двумя наиболее распространенными ошибками в приложениях C#. С помощью выражений throw легче дать более подробное сообщение об ошибке:


var age = user.Age ?? throw new InvalidOperationException("user age must be initialized");


Вывод сообщения об ошибке в методе Single ()


В процессе борьбы с ошибками проверок на null, в логах можно видеть наиболее распространенное и бесполезное сообщение об ошибке: «Последовательность не содержит элементов». С появлением LINQ, программисты C# часто используют методы Single () и First (), чтобы найти элемент в списке или запросе. Несмотря на то, что эти методы являются краткими, при возникновении ошибки не дают детальной информации о том, какое утверждение было нарушено.

Throw выражения обеспечивают простой шаблон для добавления полной информации об ошибках без ущерба для краткости:

var customer = dbContext.Orders.Where(o => o.Address == address)
                               .Select(o => o.Customer)
                               .Distinct()
                               .SingleOrDefault()
                               ?? throw new InvalidDataException($"Could not find an order for address '{address}'");


Вывод сообщения об ошибке при конвертации


В C# 7 шаблоны типа предлагают новые способы приведения типов. С помощью выражений throw, можно предоставить конкретные сообщения об ошибках:

var sequence = arg as IEnumerable
?? throw new ArgumentException("Must be a sequence type", nameof(arg));

var invariantString = arg is IConvertible c
    ? c.ToString(CultureInfo.InvariantCulture)
    : throw new ArgumentException($"Must be a {nameof(IConvertible)} type", nameof(arg));


Выражения в теле методов


Throw выражения предлагают наиболее сжатый способ реализовать метод с выбросом ошибки:

class ReadStream : Stream
{
  ...
  override void Write(byte[] buffer, int offset, int count) =>
  throw new NotSupportedException("read only");
  ...
}


Проверка на Dispose


Хорошо управляемые классы IDisposable бросают ObjectDisposedException на большинство операций после их удаления. Throw выражения могут сделать эти проверки более удобными и менее громоздкими:

class DatabaseContext : IDisposable
{
  private SqlConnection connection;

  private SqlConnection Connection => this.connection
          ?? throw new ObjectDisposedException(nameof(DatabaseContext));

  public T ReadById(int id)
  {
    this.Connection.Open();
    ...
  }

  public void Dispose()
  {
    this.connection?.Dispose();
    this.connection = null;
  }
}


LINQ


LINQ обеспечивает идеальную настройку, чтобы сочетать многие из вышеупомянутых способов использования. С тех пор, как он был выпущен в третьей версии C#, LINQ изменил стиль программирования на C# в сторону ориентированного на выражения, а не на операторы. Исторически LINQ часто заставлял разработчиков делать компромиссы между добавлением значимых утверждений и исключений их из кода, оставаясь в синтаксисе сжатого выражения, который лучше всего работает с лямбда выражениями. Throw выражения решают эту проблему!

var awardRecipients = customers.Where(c => c.ShouldReceiveAward)
                       // concise inline LINQ assertion with .Select!
                       .Select(c => c.Status == Status.None
                       ? throw new InvalidDataException($"Customer {c.Id} has no status and should not be an award recipient")
                       : c)
                       .ToList();


Unit тестирование


Также, throw выражения хорошо подходят при написании неработающих методов и свойств (заглушек), которые планируются покрыть с помощью тестов. Поскольку эти члены обычно бросают NotImplementedException, можно сэкономить некоторое место и время.


public class Customer
{
  // ...

  public string FullName => throw new NotImplementedException();

  public Order GetLatestOrder() => throw new NotImplementedException();
  public void ConfirmOrder(Order o) => throw new NotImplementedException();
  public void DeactivateAccount() => throw new NotImplementedException();
}


Типичная проверка в конструкторе



public ClientService(
IClientsRepository clientsRepository,
IClientsNotifications clientsNotificator)
{
  if (clientsRepository == null)
  {
    throw new ArgumentNullException(nameof(clientsRepository));
  }
  if (clientsNotificator == null)
  {
    throw new ArgumentNullException(nameof(clientsNotificator));
  }

  this.clientsRepository = clientsRepository;
  this.clientsNotificator = clientsNotificator;
}


Всем лень писать столько строчек кода для проверки, теперь, если использовать возможности C# 7, можно написать выражения. Это позволит вам переписать такой код.

public ClientService(
IClientsRepository clientsRepository,
IClientsNotifications clientsNotificator)
{
  this.clientsRepository = clientsRepository ?? throw new ArgumentNullException(nameof(clientsRepository));
  this.clientsNotificator = clientsNotificator ?? throw new ArgumentNullException(nameof(clientsNotificator));
}


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

Сеттеры свойств


Throw выражения также позволяют сделать свойства объектов более короткими.


public string FirstName
{
  set
  {
    if (value == null)
      throw new ArgumentNullException(nameof(value));
    _firstName = value;
  }
}


Можно сделать еще короче, используя оператор Null-Coalescing (?).

public string FirstName
{
  set
  {
    _firstName = value ?? throw new ArgumentNullException(nameof(value));
  }
}


или даже использовать тело выражения для методов доступа (геттер, сеттер)

public string FirstName
{
  set => _firstName = value ?? throw new ArgumentNullException(nameof(value));
}


Давайте посмотрим, во что разворачивается данный код компилятором:

private string _firstName;
public string FirstName
{
   get
   {
     return this._firstName;
   }
   set
   {
     string str = value;
     if (str == null)
        throw new ArgumentNullException();
     this._firstName = str;
   }
}


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

Заключение.


Throw выражения помогают писать меньший код и использовать исключения в выражениях-членах (expression-bodied). Это всего лишь языковая функция, а не что-то основное в языковой среде исполнения. Хотя throw выражения помогают писать более короткий код, это не серебряная пуля или лекарство от всех болезней. Используйте throw выражения только тогда, когда они могут вам помочь.

© Habrahabr.ru