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

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

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

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

    Чтобы исправить данные проблемы, C# 7 вводит выражения throw. Синтаксис остался таким же, как всегда использовался для операторов 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 выражения только тогда, когда они могут вам помочь.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 21
    • +1
      set => _firstName = value ?? throw new ArgumentNullException(nameof(value));
      Лично мне больше нравилось, когда по обеим частям ?? или в тернарном операторе были совместимые типы. А теперь можно писать справа что-то без типа, чтобы просто было удобно. Это спорно. C# и так хорош синтаксисом, особой надобности в спорных фичах нет.
      • +2

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


        В F#, где всё является выражением есть похожие функции:


        failwith: string -> 'a
        raise: exn -> 'a

        принимаем строку с описанием ошибки или объект Exception и валим выполнение программы. компилятору подсовываем любой тип какой он хочет, т.к. это уже неважно, функция нормально не вернёт.

      • +3
        Большинство вещей выглядят более-менее интуитивно понятными.
        А вот когда выпадет исключение в linq, я честно не уверен:
                               .Select(c => c.Status == Status.None
                               ? throw new InvalidDataException($"Customer {c.Id} has no status and should not be an award recipient")
                               : c)

        Если ToList в конце — то видимо на его материализации.
        А если foreach — может упасть на вычислении конкретного элемента?
        • +2
          Не понял вопроса, на всякий случай уточню: а вы в курсе, что
          в ToList внутри foreach?
                      public List<TResult> ToList()
                      {
                          var list = new List<TResult>();
          
                          foreach (TSource item in _source)
                          {
                              list.Add(_selector(item));
                          }
          
                          return list;
                      }

          • +1
            Был не в курсе, но и не важно это.

            Меня смущает другое — когда я делаю linq операцию, она обычно отложена. Выброс исключения видимо тоже откладывается до перечисления элемента?

            А что будет со всеми траснляторами в sql? Они справятся с трансляцией исключения? Смысла в этом мало =_=
            • +1
              А что будет со всеми траснляторами в sql? Они справятся с трансляцией исключения?

              Фреймворки вида LINQ to SQL, в отличие от LINQ методов для коллекций, используют не делегаты, а деревья выражений (expression trees), по которым строят запрос к базе. В деревьях выражений нельзя использовать throw-выражения.

              Такой код не скомпилируется:
              void A(Expression<Func<int, bool>> expr) {}
              A(x => throw new Exception()); // [CS8188] An expression tree may not contain a throw-expression.
              

              Также в expression'ах нет поддержки других конструкций C# 7: кортежей (tuple literals), объявлений переменных при вызове метода с out-параметром (out var)
              • 0
                Этой информации мне как раз не хватило, стало понятнее, спасибо.
                • 0
                  И pattern matching там тоже нет — не скомпилируется даже x is null.
          • 0
            Исключения в свойствах — зло.
            Остальное — приятный сахар.
            • +2
              Исключения в свойствах — зло.
              Почему это? ExceptionValidationRule в WPF специально для этого и существует. Чем getter/setter cвойств принципиально отличаются от методов, в которых, я надеюсь, исключения не зло?
              • 0
                Почему это? ExceptionValidationRule в WPF специально для этого и существует.

                ExceptionValidationRule отлавливает исключения, которые не обязательно сгенерированы внутри свойства. Они могут появляться и до попытки присвоения значения. При той же конвертации.
                Да и вообще, WPF и его Data Binding много чего странного привносит в архитектуру решения.

                Чем getter/setter cвойств принципиально отличаются от методов, в которых, я надеюсь, исключения не зло?

                Тут уже личное мнение. Технически — почти ничем не отличаются. Коцептуально как раз тем, что свойства не должны содержать никакой логики. В том числе такой, которая приводит к исключениям. В отличие от методов. Поэтому, если для изменения состояния объекта, необходимо провести какие-то дополнительные проверки, то нужно делать это методом.
                Иначе мы придём к чему-то такому:
                var foo = new Bar();
                try
                {
                  foo.Name = "This is my Name";
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
                • 0
                  > Коцептуально как раз тем, что свойства не должны содержать никакой логики.

                  Это неожиданно. А вы это где прочитали?
                  • 0
                    WPF и его Data Binding много чего странного привносит в архитектуру решения
                    В точку.
                    свойства не должны содержать никакой логики.
                    Судя по всему, вы путаете назначение полей и свойств. Свойства как раз для того и нужны, чтобы добавить некую «простую» логику, чего нельзя сделать для поля. Интуитивно я понимаю, что вы имели в виду. Если эта логика переиспользуется или слишком сложна (много строк или различающихся по смыслу действий?) для свойства, то ее имеет смысл вынести в отдельный (переиспользуемый) метод или даже в несколько.

                    В вашем примере, если Name — это свойство, при установке которого должны установится другие, то вполне ожидаемо получить исключение при ошибке парсинга или конвертации. Например, FullName может ожидать пробел и/или более одного слова, чтобы установить FirstName и LastName.
              • +2
                Излишнее стремление к лаконичности — порочно. По мне, вот этот код:

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


                чище и понятнее вот этого:

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


                Всем известно, что в C и C++ можно писать очень кратко и очень непонятно. Только надо ли?
                • 0
                  По-моему второй первый вариант кажется приятней, только если редко сталкиваетесь с оператором ??.. Я часто использую/сталкиваюсь, поэтому мне второй вариант кажется лаконичнее.
                  • 0

                    Он лаконичнее, с этим никто не спорит. Но в то же время, он дальше от обычного человеческого языка, чем первый.

                    • 0

                      Попробуйте перевести код на человеческий язык:


                      1. Если аргументы не валидны, то бросить исключение.
                        Сохранить аргументы
                      2. Сохранить аргументы или бросить исключение, если они не валидны.

                      Второй вариант содержит меньше дублирования и читается легче (после пятиминутного привыкания)

                    • 0
                      Второй вариант семантически отличается от первого. В первом варианте — в случае возникновения исключений внутреннее состояние экземпляра не меняется. Тогда как во втором случае — изменяется и становится неконсистентным.

                      Это конечно же надо иметь в виду в случае исполнения вне конструктора.
                      • 0
                        Второй вариант семантически отличается от первого. В первом варианте — в случае возникновения исключений внутреннее состояние экземпляра не меняется. Тогда как во втором случае — изменяется и становится неконсистентным.

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

                        Это конечно же надо иметь в виду в случае исполнения вне конструктора.
                        Да и в конструкторе тоже — что, если в нем выполняются некие «тяжелые» действия и/или с сайд эффектами, и вдруг окажется, что ничего делать не надо было, т.к. в параметрах пришел null?
                        (Что не отменяет того, что такие тяжелые конструкторы не следует делать.)
                    • 0
                      Вариант вида this.clientsRepository = clientsRepository ?? throw new ArgumentNullException(nameof(clientsRepository)); вполне чист и понятен.

                      Другое дело, что выбор примера неудачен, т.к. подобные вещи лучше делать через Code Contracts — жаль, что в System.Diagnostics.Contracts так и не стали в .NET стандартом (хотя бы де факто) и полноценной частью платформы.

                      К тому же, если совсем по хорошему, то тут и не Code Contracts нужны, а (Not)Nullability.
                      • 0
                        Я за Maybe и Either.
                        Они попросту принуждают невалидные ветки обрабатывать. можно, конечно, исхитриться и забить, но на это больше усилий требуется, чем на Nullable.

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

                    Самое читаемое