Throw выражения в C# 7
Всем привет. Продолжаем исследовать новые возможности 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 выражения только тогда, когда они могут вам помочь.