В C# throw всегда был оператором. Поскольку throw — это оператор, а не выражение, существуют конструкции в C#, в которых нельзя использовать его.
-
- в операторе Null-Coalescing (??)
- в лямбда выражении
- в условном операторе (?:)
- в теле выражений (expression-bodied)
Чтобы исправить данную проблему, C# 7 вводит выражения throws. Синтаксис остался таким же, как всегда использовался для операторов throw. Единственное различие заключается в том, что теперь их можно разместить в новых местах:
Давайте рассмотрим, в каких местах throw выражения будет лучше использовать. Поехали!
Тернарные операторы
До 7 версии языка C#, использование throw в тернарном операторе запрещалось, так как он был оператором. В новой версии С#, throw используется как выражение, следовательно мы можем добавлять его в тернарный оператор.
1 2 3 | var customerInfo = HasPermission() ? ReadCustomer() : throw new SecurityException("permission denied"); |
Вывод сообщения об ошибке при проверке на null
«Ссылка на объект не указывает на экземпляр объекта» и «Объект Nullable должен иметь значение», являются двумя наиболее распространенными ошибками в приложениях C#. С помощью выражений throw легче дать более подробное сообщение об ошибке:
1 | var age = user.Age ?? throw new InvalidOperationException("user age must be initialized"); |
Вывод сообщения об ошибке в методе Single()
В процессе борьбы с ошибками проверок на null, в логах можно видеть наиболее распространенное и бесполезное сообщение об ошибке: «Последовательность не содержит элементов». С появлением LINQ, программисты C# часто используют методы Single() и First(), чтобы найти количество элементов в списке или запросе. Несмотря на то, что эти методы являются краткими, при возникновении ошибки не дают детальной информации о том, какое утверждение было нарушено. Throw выражения обеспечивают простой шаблон для добавления полной информации об ошибках без ущерба для краткости:
1 2 3 4 5 | 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, можно предоставить конкретные сообщения об ошибках:
1 2 3 4 5 6 | 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 выражения предлагают наиболее сжатый способ реализовать метод с выбросом ошибки:
1 2 3 4 5 6 7 | class ReadStream : Stream { ... override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException("read only"); ... } |
Проверка на Dispose
Хорошо управляемые классы IDisposable бросают ObjectDisposedException на большинство операций после их удаления. Throw выражения могут сделать эти проверки более удобными и менее громоздкими:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 выражения решают эту проблему!
1 2 3 4 5 6 | 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, можно сэкономить некоторое место и время.
1 2 3 4 5 6 7 8 9 10 | 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(); } |
Типичная проверка в конструкторе
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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, можно написать выражения. Это позволит вам переписать такой код.
1 2 3 4 5 6 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 выражения также позволяют сделать свойства объектов более короткими.
1 2 3 4 5 6 7 8 9 | public string FirstName { set { if (value == null) throw new ArgumentNullException(nameof(value)); _firstName = value; } } |
Можно сделать еще короче, используя оператор Null-Coalescing (??).
1 2 3 4 5 6 7 | public string FirstName { set { _firstName = value ?? throw new ArgumentNullException(nameof(value)); } } |
или даже использовать тело выражения для методов доступа (геттер, сеттер)
1 2 3 4 | public string FirstName { set => _firstName = value ?? throw new ArgumentNullException(nameof(value)); } |
Давайте посмотрим, во что разворачивается данный код компилятором:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 выражения только тогда, когда они могут вам помочь.