Когда нужны исключения

Предисловие


Тема Исключений (за и против) не нова, и уже не раз обсуждалась. Но всё же, я надеюсь, что каждый из прочитавших данную статью почерпнёт что-то новое и полезное для себя.

Причиной появления этой публикации стало нежелание больше молчать и просто смотреть, как интернет заполняют «всезнающие» программисты, которые учат новичков в нашей сфере забивать гвозди микроскопом, находя десятки аргументов в защиту своих методов! Потому, эта публикация направлена, скорее, на новичка постигающего программирование и задающего вопросы. Она является моим «правильным ответом»

Итак, что же такое Исключение (Exception)?


imageИсключение — это то, вероятность (возможность) чего исключается системой… это то что в условиях программы произойти не может.

Посмотрите в свой код. Можете ли вы к каждому исключению дописать «но ведь это невозможно» или «но это же исключено»? Думаю, мало кто сможет честно ответить «да». Если ваш ответ «нет» — значит часть исключений на самом деле не являются таковыми, просто вы использовали этот механизм, потому как вам показалось это более удобным. То самое «удобство» такого подхода будет рассмотрено далее.

Кто же должен определять, какие ситуации программа исключает, а какие нет?


Все исключения могут быть продиктованы только заданием или здравым смыслом, и ничем или никем иным. Исключение не определяется ни типом данных, ни их источником, хотя нередко можно услышать или прочитать в «умных» статьях: «Неправильный клиентский ввод не может быть исключением»… не верьте! Вот, просто, не надо в такое верить.

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

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

Или предусмотрите, или исключите.

Почему любое лишнее Исключение является вредным для кода?


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

  • Не передавать же по всей цепочке вызовов флаг завершения
  • Мне надо передать данные, собранные до получения отрицательного ответа
  • Функция может вернуть отрицательный результат по трём причинам. Так проще передать эту причину
  • множество других доводов...

Почему же так не надо делать? Да потому, что:
image
  • Вы не сможете смело использовать уже написанные методы в других местах, ведь они могут порождать неожиданные исключения;

  • GOTO зло — потому что путает код, передавая управление в произвольную точку. Не следует думать, что передача управления, перепрыгивая произвольное количество вызовов в стеке, меньше путает ваш код, чем GOTO;

  • Кому-то, а возможно и вам самим, потом надо будет что-то исправить или дописать в написанном коде. Не следует считать, что через 2 года вы влёт вспомните, что этот модуль системы для отрицательных ответов использует исключения. То есть, вам придётся вникать… и вникать не только в код, но и во все «необычности» подхода.

Не стал включать в список выше такие аргументы как «это правило хорошего тона» и «давайте всё использовать по назначению», итак написано уже достаточно.

Заключение


Старался не вдаваться лишние подробности и в каждое предложение вкладывать максимум смысла. Надеюсь у меня это получилось, и читая эти строки, вы думаете: «пять минут, а столько нового!» Ну что же, надеюсь это так.

Напоследок хотелось бы сказать, что не следует изворачиваться под давлением факторов, сроков и «так же проще», и писать неправильный код, зато быстро.

Совершенство и отказоустойчивость нашего кода и определяет наш профессионализм.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 58

    +8
    А я думаю «пять минут потраченные впустую», потому что в статье нет ни одного конктетного совета, а есть только общий и весьма очевидный посыл — «исключения это не серебряная пуля, использовать их или нет надо смотреть по ситуации».
      0
      а есть только общий и весьма очевидный посыл

      Да ну? По-моему посыл как раз использовать исключения только для нештатных ситуаций, а не «смотреть по ситуации».
        0
        да, так и есть. Пытался донести мысль, что только нештатные ситуации следует рассматривать как исключения.
      –2
      Бизнес определяет эволюцию языков. Часто получается так: бизнес-требования противоречат инженерным требованиям. Значит, с точки зрения инженера, языки и экосистемы развиваются по абсурдной траектории, инженер видит отрицательный отбор. Но бизнесу можно всё, что приводит к увеличению нормы прибыли, а значит, можно и исключения, и GOTO, и плевать на идеалы инженеров тоже можно. «MVP» и всё, сиди пиши говнокод молча, а то вспомним про hire & fire.
        +5
        Исключение — это то, вероятность (возможность) чего исключается системой… это то что в условиях программы произойти не может.

        Извините, но это чушь какая-то. Если это не может, то нет смысла и обрабатывать это. Может быть, лучше написать «произойти не должно»?
          0
          к сожалению от этого потеряется категоричность утверждения. Ведь пользователь тоже «не должен» пытаться вводить имя там, где у него просят ввести возраст. Но это не повод для порождения исключения, потому как здравый смысл подсказывает, что он может это сделать (ведь ему никто не запретил)
          +5
          Вы не сможете смело использовать уже написанные методы в других местах, ведь они могут порождать неожиданные исключения

          Fail fast? Не, не слышал. Лучше молча глотать и утаивать, что что-то пошло не так.
            +2
            Исключение — это то, вероятность (возможность) чего исключается системой… это то что в условиях программы произойти не может.

            А откуда вы взяли это определение?


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

            Вы так говорите, как будто тип и источник данных не определяются задачей.


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

            Такая ситуация, очевидно, возможна. А вот теперь вопрос — зачем обрабатывать ее на каждом уровне кода (ведь "код не должен ни о чем умалчивать")?


            Не раз сталкивался с ситуацией, когда отрицательный результат выполнения функции возвращали в виде исключения, хотя такой результат был предусмотрен самой задачей

            Что такое "отрицательный результат выполнения функции"?


            Почему любое лишнее Исключение является вредным для кода?

            Как определить, является ли исключение лишним?

              0
              А откуда вы взяли это определение?

              По моему мнению такое определение лучше всего описывает саму суть исключений. Источника нет, как и у статьи в целом. Статья основана на опыте и ранее прочитанных и переваренных материалах

              Вы так говорите, как будто тип и источник данных не определяются задачей.

              Разумеется именно задачей и определяется. Но процитированная фраза была вырвана из контекста. А контекст был такой: источник получения данных не может сам по себе быть аргументом за или против порождения исключения

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

              Такая ситуация, очевидно, возможна. А вот теперь вопрос — зачем обрабатывать ее на каждом уровне кода (ведь «код не должен ни о чем умалчивать»)?

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

              Что такое «отрицательный результат выполнения функции»?

              Это, например, когда функция сохраняя данные в базу проводит их валидацию. Эта функция может вернуть объект, если тот был удачно сохранён или ошибку валидации. Ошибка валидации — отрицательный результат выполнения (объект не сохранён). Встречал случаи, когда эту ошибку передавали в виде исключения, хотя пользователь просто опечатался при вводе e-mail адреса или телефона :)

              Как определить, является ли исключение лишним?

              В статье есть ответ на этот вопрос
              Можете ли вы к каждому исключению дописать «но ведь это невозможно» или «но это же исключено»?
                0
                По моему мнению такое определение лучше всего описывает саму суть исключений.

                Ну то есть вы сам придумали определение, а теперь почему-то хотите, чтобы все остальные использовали механизм согласно этому определению. Найс.


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

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


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

                Это противоречит — вашей же — позиции "ваш код не должен ни о чем умалчивать". Отсутствие явной обработки исключения — это умолчание.


                Это, например, когда функция сохраняя данные в базу проводит их валидацию.

                И… почему нельзя ошибку валидации представить в виде исключения?


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

                Или ошибку сохранения в БД. Или ошибку логирования. Или ошибку нехватки памяти. Вы себе представляете контракт такой функции?


                Можете ли вы к каждому исключению дописать «но ведь это невозможно» или «но это же исключено»?

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

                  0
                  Ну то есть вы сам придумали определение, а теперь почему-то хотите, чтобы все остальные использовали механизм согласно этому определению. Найс.

                  Я рекомендую, а не пытаюсь кого-то заставить… описанный метод (и соответственно определения) я применяю в своей работе и он работает. Потому да, я предлагаю работающую схему (механизм) по которому у меня не возникает вопросов «вернуть ли код ошибки, false или бросить исключение». Задавая себе один простой вопрос я сразу принимаю решение о реакции на провал операции.

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

                  Если Вас поместить в стабильное окружение, где постоянно есть еда, вода и воздух в достаточных количествах, можете ли вы исключить ситуацию, что находясь в этом окружении задохнётесь от нехватки воздуха, умрёте от голода или жажды? Я вот исключил бы. То есть, я бы бросил исключение «наверх», если бы в какой-то момент почувствовал, что умираю от голода!

                  Это противоречит — вашей же — позиции «ваш код не должен ни о чем умалчивать». Отсутствие явной обработки исключения — это умолчание.

                  Он не умалчивает. Он всё ещё исключает возможность возникновения такого случая, так же, как это исключал код уровнем ниже. Позиция разработчика при написании такого кода приблизительно такая: «тот, кто вызовет этот код должен позаботиться о доступности папки на запись, её наличии и о работоспособности накопителя». То есть в этом коде всё ещё исключена невозможность записать файл! И если файл не записался, то исключение, которое бросил код уровнем ниже следует пропустить дальше по цепочке!

                  И… почему нельзя ошибку валидации представить в виде исключения?

                  Я не говорил что нельзя. Я говорил что в указанном контексте это плохо. Именно по причине того, что Вы написали далее
                  Эта функция может вернуть объект, если тот был удачно сохранён или ошибку валидации.

                  Или ошибку сохранения в БД. Или ошибку логирования. Или ошибку нехватки памяти. Вы себе представляете контракт такой функции?

                  У этой функции по факту есть 3 результата выполнения:
                  1. Объект удачно сохранён
                  2. Поля объекта не прошли валидацию (заполнены неверно)
                  3. Валидация или сохранение объекта невозможны (завершились ошибкой)

                  Таким образом у функции есть 2 штатных результата: сохранён/не сохранён. И нештатный результат: невозможно сохранить/проверить. Так зачем ШТАТНУЮ ситуацию представлять в виде исключения? Для меня вполне ожидаемо, что пользователь при регистрации допустит опечатку или не обратит внимания на заявленный шаблон заполнения поля.
                  В то же время я не ожидаю ситуации, что в момент записи в базу оборвётся соединение. А потому да, обрыв соединения — это исключение! Это значит, что произошло что-то такое, чего произойти в условиях моего кода не может! В момент работы было недопустимо изменено окружение, в котором код выполнялся.

                  Можете ли вы к каждому исключению дописать «но ведь это невозможно» или «но это же исключено»?

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

                  Не любое… но многие из них! В моём коде иногда исключения присутствуют… очень часто я пропускаю исключения на самый верх и отдаю их пользователю в виде ошибки… вроде «недостаточно прав для сохранения файла, свяжитесь с администратором для решения проблемы»
                  Но на то они и исключения, что бы их было мало, а не весь код был ими напичкан по 3 штуки на функцию!

                  Я понимаю, что Вы можете быть со мной не согласны. Но как я писал ранее, это работающая схема, которая имеет полное право на жизнь. Я не пытался кого-то переубедить или что-то доказать. Как я и писал, эта статья — совет новичкам по позиционированию исключений как таковых… чем они являются и когда нужны. А учитывая, что это работает и работает неплохо, значит в согласии других не очень нуждается (грустно конечно, что многие оказались несогласны со мной, но всё же… суть статьи не в переубеждении)
                    0
                    Потому да, я предлагаю работающую схему (механизм) по которому у меня не возникает вопросов «вернуть ли код ошибки, false или бросить исключение».

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


                    Если Вас поместить в стабильное окружение, где постоянно есть еда, вода и воздух в достаточных количествах, можете ли вы исключить ситуацию, что находясь в этом окружении задохнётесь от нехватки воздуха, умрёте от голода или жажды

                    Нет, не могу.


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


                    Он не умалчивает. Он всё ещё исключает возможность возникновения такого случая, так же, как это исключал код уровнем ниже.

                    И где сказано, что он исключает такую возможность?


                    Позиция разработчика при написании такого кода приблизительно такая: «тот, кто вызовет этот код должен позаботиться о доступности папки на запись, её наличии и о работоспособности накопителя».

                    Теперь посмотрим на код:


                    void Log(Event msg)
                    {
                      foreach(var sink in _logSinks)
                      {
                         sink.Log(msg);
                      }
                    }

                    О чем должен позаботиться тот, кто вызывает этот код?


                    Таким образом у функции есть 2 штатных результата: сохранён/не сохранён. И нештатный результат: невозможно сохранить/проверить.

                    Ээээ… "невозможно сохранить" — это "не сохранен". Это один и тот же результат.


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

                    … такие вещи проверяют не на уровне сохранения в БД.


                    Не любое… но многие из них!

                    Любое. Потому что, как сказано выше, ни к какому исключению нельзя дописать "это невозможно".


                    Но на то они и исключения, что бы их было мало, а не весь код был ими напичкан по 3 штуки на функцию!

                    В любом коде, написанном в стиле defensive programming исключений будет как минимум не меньше чем аргументов, у которых есть недопустимые значения, чаще — больше. И это, в общем-то, нормально, на то guards и существует.


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

                    Под "работает неплохо" вы подразумеваете "работает у вас лично"? Так это еще не повод рассказывать это "новичкам" под соусом "как правильно".

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

                      Что, простите? Почему нельзя оценивать работу функцию по коду ошибки и при этом обрабатывать исключения? Или я не так понял.
                        0
                        Почему нельзя оценивать работу функцию по коду ошибки и при этом обрабатывать исключения?

                        Потому что возникает вопрос, какие ошибки возвращать кодом, а какие — исключениями. Нарушается униформность.


                        (понятное дело, что есть случаи, когда на униформность можно положить, но как основной гайдлайн — или везде одно, или везде другое)

                          0
                          Мы, наверно, по-разному понимаем.Например, мы работаем со списком, и просим вернуть номер элемента. Он может быть специальным, 0 или отрицательный, как признак ошибки — это фактически код возврата. При этом тот же объект может выкинуть исключение, скажем, ошибки доступа к памяти, которое нужно ловить по-любому, потому что надо освободить ресурсы. Или исключение при обращении к несуществующему элементу. Тут как ни крути, поддерживаешь обе парадигмы — только для «мягких» и «серезных» ошибко отдельно.
                            0
                            Например, мы работаем со списком, и просим вернуть номер элемента. Он может быть специальным, 0 или отрицательный, как признак ошибки — это фактически код возврата.

                            Какая у этого отрицательного номера семантика? "Не нашли" (при поиске)? Option[int] справится лучше. "Не смогли вставить, потому что не хватило памяти"? Лучше бросить исключение.


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

                            Это если быть в рамках языка/платформы, где освобождение ресурсов ручное. А в .net, скажем, намного правильнее сделать IDisposable, который завернуть в using, и не ловить исключение — тогда как только выполнение покинет скоуп using, ресурсы будут освобождены сами.


                            Тут как ни крути, поддерживаешь обе парадигмы — только для «мягких» и «серезных» ошибко отдельно.

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

                              0
                              Разумеется, это зависит от платформы.

                              Какая у этого отрицательного номера семантика? «Не нашли» (при поиске)?

                              Да, это удобно.

                              «Не смогли вставить, потому что не хватило памяти»? Лучше бросить исключение.

                              Ну вот и есть уже обе парадигмы сразу. «не найдено» — частая ситуация, лучше вернуть леговесным кодом. «Нет памяти» — редкая, можно и тяжеловесное исключение прокинуть.
                                0
                                Ну вот и есть уже обе парадигмы сразу. «не найдено» — частая ситуация, лучше вернуть леговесным кодом.

                                Не-а, "не найдено" — это не ошибка. И да, следствие этого подхода — это Option[T] TryFind и T Find (или, соответственно, Find и Get, если другая нотация) в одном интерфейсе, когда, если отсутствие ожидаемого объекта — это норма, то мы это явно проверяем, а если нет — то мы читаем, и не тратим лишний код в текущем скоупе на проверку.


                                И это вопрос не частоты/производительности (хотя, чего уж там, на них в реальности тоже приходится оглядываться), а удобства восприятия.

                                  0
                                  Не-а, «не найдено» — это не ошибка.

                                  Спорно. Если я прошу дать мне номер элемента с ключом Х, то «не найдено» — это не номер элемента, это по сути ошибка.

                                  И да, следствие этого подхода — это Option[T] TryFind и T Find (или, соответственно, Find и Get, если другая нотация) в одном интерфейсе, когда, если отсутствие ожидаемого объекта — это норма, то мы это явно проверяем, а если нет — то мы читаем, и не тратим лишний код в текущем скоупе на проверку.

                                  От API зависит; я про конкретный случай написал с доступом по индексу. Так, просто для примера.
                                    0
                                    Если я прошу дать мне номер элемента с ключом Х, то «не найдено» — это не номер элемента, это по сути ошибка.

                                    Ну вот в это и упирается обсуждение.


                                    От API зависит

                                    Именно. И поэтому в одном случае будет ошибка, а в другом — нет.


                                    Но в общем, повторюсь, моя позиция сводится к следующему:


                                    • поддержка одной парадигмы легче, чем двух
                                    • когда мы что-то возвращаем как "ошибочное значение" (например, индекс ненайденного элемента), надо приложить все усилия к тому, чтобы это нельзя было, не подумав, запихнуть дальше в обработку (т.е., получили индекс, и сразу запихнули в "разрежь массив по индексу") — например, за счет типизации
                      0

                      PS


                      эта статья — совет новичкам по позиционированию исключений как таковых…

                      Причиной появления этой публикации стало нежелание больше молчать и просто смотреть, как интернет заполняют «всезнающие» программисты, которые учат новичков в нашей сфере забивать гвозди микроскопом, находя десятки аргументов в защиту своих методов!

                      В чем отличие вас от "всезнающих программистов, которые учат новичков"?

                0
                Исключения это интересная тема. Если где-то в недрах кода (возможно даже в сторонней библиотеке) возникла недопустимая ситуация — например невозможность выделить память — то нужно предпринять какие-то действия. По идее, это может быть и код ошибки, и исключение, и даже аварийное завершение работы всей программы. Но что именно?

                В какой-то момент мне пришла в голову мысль, что в языке программирования должна существовать некоторая конструкция, определяющая «ошибочную ситуацию». Нечто среднее между «return кода_ошибки» и «throw исключения». То есть библиотека говорит «у меня произошла ошибка», а как именно она должна быть обработана — это вопрос вызывающего кода. Ведь даже обработка исключений может быть разной (SEH, DWARF, SJLJ). В конце концов, код возврата — это тоже способ организации исключений, особенно если компилятор возьмет на себя часть этой работы (очевидно что для этого потребуется сказать компилятору какие коды означают ошибки и обеспечить тип возврата всех функций, участвующих в этой системе).

                То что любая функция может внезапно выкинуть любое исключение (как в С++) — это плохо и по сути является аналогом goto. Реализация в Java (с явным указанием исключений которые могут быть выброшены) лучше (именно тем что там исключения указываются явно). Но можно пойти еще дальше.

                В некотором роде исключения — это модуль языковой функциональности (как RTTI в C++, отключаемую опцией компиляции). В определенных случаях (например программирование под микроконтроллеры с малым объемом памяти) исключения целесообразно отключать вообще, переводя весь код на самую простую реализацию — кодами возврата. При этом хотелось бы, чтобы библиотеки были общие (т.е. не держать отдельные наборы библиотек для каждой реализации исключений).
                  0
                  То что любая функция может внезапно выкинуть любое исключение

                  Почему бы не ловить все исключения?
                    0
                    Реализация в Java (с явным указанием исключений которые могут быть выброшены) лучше (именно тем что там исключения указываются явно).

                    Не все с этим согласны
                      0
                      В какой-то момент мне пришла в голову мысль, что в языке программирования должна существовать некоторая конструкция, определяющая «ошибочную ситуацию». Нечто среднее между «return кода_ошибки» и «throw исключения».

                      в с++ есть std::error_code.
                        0

                        Это уже давно реализовано в LISP :-) http://schemer.in/aeh.html

                        +2
                        «То, чего никогда не может быть» — это только необработанное исключение. Было бы странно вводить в большинство современных языков довольно сложный механизм try\catch\throw для случаев, которые вообще никогда не должны выполняться, не находите?

                        В более широком смысле исключение описывает ситуацию, в результате которой локальный кусок кода не может дальше выполняться. В таком случае оно вполне логично «всплывает» до того уровня, где может быть осмысленно обработано. Это позволяет разделять ответственность, структурирует код и отлично сочетается со стековой архитектурой.

                        В статье ни одного аргументированного довода против исключений не нашел. Всё сводится к тому, что это «сложно» или «не нравится». Сложность вижу только в том случае, если нет порядка и половина функций возвращает значения-ошибки, а половина кидает исключения. Если принять за правило, что все функции работают унифицированно — не запутаешься, и через два года не нужно будет ничего вспоминать.

                        А дальше вообще начинаются «Вредные советы»:
                        Пускай на верхнем уровне висит обработчик исключений и превращает все необработанные исключения в ошибки, сохраняет данные и деликатно завершает выполнение

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

                        Вы не сможете смело использовать уже написанные методы в других местах, ведь они могут порождать неожиданные исключения;

                        Если вы вернете из функции неизвестный код ошибки, ситуация будет еще опаснее. В лучшем случае вы всё равно получите исключение, только не осмысленное, а что-то типа «обращение к нулевому указателю» или «выход за пределы массива» при работе с некорректными данными. В худшем — ошибка будет проглочена и программа молча выполнит неожиданный код, вероятно повреждая при этом данные.

                        Возможно, автор не до конца разобрался с механизмом исключений, или сознательно решил этого не делать из этических соображений?
                          0
                          В худшем — ошибка будет проглочена и программа молча выполнит неожиданный код, вероятно повреждая при этом данные.

                          С исключениями тоже так может быть — пустой catch/except блок.
                          0

                          У исключений есть большое преимущество перед возвратом функции. Просто огромнейшее: стектрейс. Неожиданно вывалившееся исключение можно проверить, отдебажить, воспроизвести. Если функции начинают возвращать -1 — найти источник ошибки гораздо сложнее.


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


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

                            +2
                            ИМХО главная проблема исключений — нечитаемость кода. Очень сложно найти место, где исключение будет обработано. А ещё бывает, что где-то забыли catch, или написали там не тот конкретный тип исключения. И всё, вроде, работает, и даже сообщение об ошибке выдаётся, но ошибка обрабатывается не так, как было задумано. ИМХО программировать с использованием кодов возврата проще. Как минимум, до какого-то уровня сложности системы.
                              0
                              Не могу сказать про ситуацию в целом, но, допустим, в PHP — если всё делать «по фен-шую» то код должен бросать не стандартное исключение, а свой собственный класс исключения, который наследуется от встроенного. И потом не сложно найти где ловится именно наш тип исключения, а не исключение впринципе. Как-то так.
                                +1
                                В Java и С++ не запрещено ловить базовый тип, даже если бросается наследник (что логично). Скажем так: можно сделать нормально, но никто не мешает сделать коряво (что, впрочем, вряд ли является отличительной чертой исключений).
                                +2
                                ИМХО программировать с использованием кодов возврата проще.

                                Вот только проигнорировать код возврата (во многих языках) проще, чем исключение.

                                  0
                                  исключения как инструмент совершенно не уникальны в том, что при корявом их применении код становится сложным. Например, как только начинаешь использовать коды возврата, все функции по умолчанию начинают возвращать не bool/string/..., а код ошибки. И в конечном итоге оказывается, что при возникновении какой-то ошибки её код возврата тянется через всю программу, а обрабатываются они точно так же в ограниченном количестве мест. Архитектура получается точно такая же, как и с исключениями, но с исключениями проще писать как позитивный, так и негативный сценарий выполнения.

                                  Плюс, исключния более приятны когда пытаешься разбить большие функции
                                  +1
                                  У исключений есть большое преимущество перед возвратом функции. Просто огромнейшее: стектрейс.

                                  Это если оно unhandled и на самом деле исключение.
                                  0
                                  Извините чукча не читатель. Плохой учитель губит хороших учеников.
                                  «Все исключения могут быть продиктованы только заданием или здравым смыслом», о боже откуда это? А как же формализация?
                                  Исключения должны выбрасываться в том случае, если нарушаются одно из трех условий при вызове члена типа.
                                  Предусловие, инвариант, пост условие. Как это разумно и правильно применить в контекте проекта это уже другой вопрос.

                                  Предусловие — проверяет входные параметры и инвариант типа. Потом происходит выполнение каких-то операций, которые могут нарушать инвариант. Перед возвращением значения происходит проверка постусловий и инварианта.
                                  Это если в кратце.
                                    0
                                    Не успел отредактировать.
                                    Предусловия это условия при которых метод не сможет выполнить возложенную на него обязанность, обычно в предусловиях проверяют валидность входных данных.
                                    Пост условия это проверка условий, что метод выполнил корректно свою обязанность. Инвариант это условия которые должны выполняться всегда, исключение составляют методы в которых этот инвариант на время выполнения может быть нарушен.
                                    Например есть тип Employee, c методами IncreaseBonus и DecreaseBonus.
                                    Пусть будет объект employee с с бонусом равным 10. И кто-то например вызывает employee.DecreaseBonus(20), методы не имеют возвращаемого значения. Предусловие проверяет что 20 > 0, инваринат проверяет что текущее значение бонуса больше 0, 10>0.
                                    Потом происходит вычисление и бонус становится -10. Проверки на пост условие нет, хотя теоретически это может быть успешность записи еще куда либо, а инвариант проверят что бонус отрицательный и выбрасывает исключение. И объект находится в неконсистетном состоянии, при вызове любого другого метода инвариант должен выбрасывать исключение.
                                    Данный пример с точки зрения дизайна не является лучшим, но наглядно демонстрирует условия.
                                    В реальности примером инварианта в C# является правильная реализация интерфейса IDisposable, где перед каждым методом проверяет флаг isDisposed, другим примером может быть проверка isConnected при вызове в типах которые оперируют связью. Примеры предусловий это проверка аргументов на валидность значений.
                                    По поводу проверки и бросания исключений от пользовательского ввода, тут все определяется контрактом. Если модель не может работать с пустой строкой, то если значение передаваемого параметра пустая строка — исключение однозначно, чтобы исключения не возникало, об этом должен позаботиться UI — не давать применить такое значение.
                                    Вы также не затронули тему о иерархии исключений и их обработке, но этого вообще хватит на отдельный пост.
                                    Учите мат часть она давно уже написана.
                                    +3
                                    Вы не сможете смело использовать уже написанные методы в других местах, ведь они могут порождать неожиданные исключения;

                                    А что такое «неожиданное исключение» и почему оно является препятствием? По сути, это неожиданный код возврата. Функция возвращает вам, скажем, длину буфера, или -1 в случае неудачи. А вы не знали, что значение может быть отрицательным — это неожиданный для вас результат. Значит ли это, что код возврата нельзя использовать? Нет. С исключениями то же самое — выбрасываемые в виде кода ошибки исключения должны быть документированы, так же как и возвращаемые функцией значения.

                                    GOTO зло — потому что путает код, передавая управление в произвольную точку. Не следует думать, что передача управления, перепрыгивая произвольное количество вызовов в стеке, меньше путает ваш код, чем GOTO;

                                    Ничего подобного. Если так рассуждать, то мы должны отказаться от конструкций:
                                    for ()...{
                                    break;
                                    }

                                    if (){

                                    } else {

                                    }

                                    В первом случае у нас есть неявный GOTO вовне цикла, во втором — неявный GOTO к блоку ELSE.

                                    GOTO — не must die. Must die неправильное его использование. Самая большая проблема с goto — это прыжок назад по коду с созданием петли. Именно это тяжело отслежить и понять, именно из-за этого получается пресловутое спагетти. Исключение не бросает нас вверх по ходу исполнения, оно бросает нас вниз, дальше.

                                    Не следует считать, что через 2 года вы влёт вспомните, что этот модуль системы для отрицательных ответов использует исключения

                                    Такой же странный аргумент, как и первый. И тот же самый контр-аргумент. Вот вы надеетесь на то, что код ошибки — это -1. А потом добавили еще -2 и -3. Через поименованные константы, разумеется. А результат функции по-прежнему проверяете на == -1. И? Какая разница? Кто мешает вам ловить все коды ошибки меньше нуля или все исключения (определенные + «остальное»)?

                                    Использовать нужно то, что сделает ваш код читаемым и надежным. Лучше коды ошибок? Пожалуйста. Лучше исключения? На здоровье. Лучше goto с переходом в блок деинициализации в конце функции? Да рали бога.
                                      +1
                                      В разработке руководствуюсь правилом: «исключения для программистов, а не для пользователей.»
                                      То есть если пользователь забыл ввести значение в поле формы — это просто ошибка, не исключение.
                                      А вот если где-то «внутри» очень важная переменная вдруг оказалась не заданной — нужно кидать исключение, причем вывести и имя функции, и сообщение, и значения других параметров, которые помогут найти эту ошибку.
                                        0
                                        >GOTO зло — потому что путает код, передавая управление в произвольную точку.

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

                                        >Вот теперь заказчик сам вам сказал, где следует породить исключение в написанной для него программе

                                        Не в этих ли случаях их использовали, давая указанные вами пояснения? :)
                                          0
                                          Удивительно, но в статье ничего не сказано о том, что исключения ломают referential transparency. И это тот самый пункт номер 1 с которого надо начинать, когда мы говорим вообще что-либо об исключениях. А ещё они также не composable + не работают в multi-threaded окружении.
                                            0

                                            Referential transparency — это хорошая такая штука, особенно в функциональном программировании, но вот только не всегда достижимая. Один раз у вас память выделилась, другой раз — нет.


                                            А ещё они [...] не работают в multi-threaded окружении.

                                            Работают. Просто надо уметь их готовить.

                                              0
                                              А есть подходы, которые не надо уметь как-то специально готовить и которые просто работают, при этом всегда и корректно… А в моменты когда они не работают корректно, программа просто не компилируется ;-)
                                                0

                                                Что же это за волшебные подходы?

                                                  0
                                                  Вот здесь, если я правильно помню, было довольно хорошо про это рассказано: Railway Oriented Programming
                                                    0

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


                                                    (а) монада Try не решает проблему referential transparency. Если внутри функции, пусть даже она возвращает Try[T], есть побочный эффект (например, работа с бд), функция не будет чистой, и, как следствие, referential transparency будет нарушена. Да фиг с ней, с БД, банальное выделение памяти — и ваша функция уже недетерминирована.


                                                    (б) монада Try не решает проблему обработки ошибок в многопоточности: если вы положите функцию, возвращающую Try[T], в другой поток, и проигнорируете ее результат — как вы и делаете в другом комментарии, — вы ничего не узнаете о случившихся ошибках.


                                                    (ц) в обоих этих случаях компилятор вам никак не поможет. Более того, прямо скажем, есть еще сильно больше одного способа "сломать" Try (впрочем, как и любой case class) даже при полной поддержке компилятором — и это еще не упоминая того факта, что описанная вами поддержка есть далеко не везде (скажем, в F# на необработанный кейс будет предупреждение, но код скомпилируется, в Scala, афаик, так же).

                                              0
                                              >не работают в multi-threaded окружении

                                              1. PHP, Node.JS — однопоточные. :) Нам можно :)
                                              2. А если войти в критическую секцию, то, кмк, все должно работать. Но сложновато. Как и любой другой многопоточный код. :)
                                                0
                                                не работают в multi-threaded окружении.

                                                Это с какой стати? Вас не затруднит пояснить развернуто? Без каких-либо проблем используем исключения в многопоточных системах.
                                                  0
                                                  try {
                                                    Task(some_work).run() // (1)
                                                  catch {
                                                    // (2) never reach this point
                                                  }
                                                  


                                                  В примере выше, если Task запускает some_work в другом треде и в этом треде some_work породит исключение мы по дефолту в точке (2) его не поймаем никогда, если только не напишем специальную обвязку вручную, для проброса исключений из слейв-треда в мастер-тред.

                                                    0

                                                    Ну так вы же проигнорировали результат Task.Run — так что неудивительно, что вы не получили информации о ходе выполнения. В таком коде, прямо скажем, some_work может никогда и не выполниться.

                                                      0
                                                      Это просто схемотичный пример, а не кусок кода, который должен компилироваться. В интернете погуглите пожулуйста «Referential transparency» и найдёте много информации почему исключения её ломают и, самое главное, как этого можно избежать с примерами кода и т. п.
                                                        0

                                                        Я знаю, почему исключения ломают referential transparency, но мы-то здесь не о referential transparency, а о многопоточности.


                                                        Так вот, в вашем коде полностью проигнорирован результат some_work — поэтому никакая обработка ошибок, включая монаду Try, про которую вы пишете рядом, вам не поможет. Более того, как я уже писал, поскольку результат проигнорирован, ваш код может завершиться раньше, чем some_work начнется. Ну да, такая вот многопоточность, только конкретно исключения тут ни при чем.

                                                          0
                                                          Я имел ввиду следующее. Сейчас попробую расписать подробнее. Допустим у вас есть поток B в котором происходит какая-то полезная работа. В какой-то момент в этом потоке B происходит ошибка и выбрасывается исключение. Это исключение может быть перехвачено и обработано только внутри потока B. Как поток A, который расчитывает, что от потока B рано или поздно придёт какой-то результат, узнает, что в B вычисление завершилось неудачей? По умолчанию никак. Но можно написать обвязку, которая будет прокидывать ошибку из потока B в поток A (тем или иным способом) и уже дальше в потоке А, например, кидать исключение. Это будет работать, но для этого придётся написать какой-то код. Ещё раз, исключение брошенное в одном потоке можно поймать и обработать только из этого же потока. По умолчанию другие треды про него ничего не узнают, если не предпринять специальных телодвижений.
                                                            0
                                                            Как поток A, который расчитывает, что от потока B рано или поздно придёт какой-то результат, узнает, что в B вычисление завершилось неудачей?

                                                            Давайте начнем с простого вопроса: как поток A узнает, что от потока B пришел какой-то результат? По умолчанию это так не работает.


                                                            (вау, я только что нашел фундаментальную проблему в операторе return)

                                                              0
                                                              Как поток A, который расчитывает, что от потока B рано или поздно придёт какой-то результат, узнает, что в B вычисление завершилось неудачей? По умолчанию никак

                                                              Воу-воу-воу, секундочку. Если мы ожидаем результата от потока Б, то мы ожидаем какого-то события, сигнализирующего достижения результата, или окончания потока Б. Т.е. в любом случае у нас будет код, который является прослойкой между двумя этими потоками. Так или иначе. Кто мешает этой прослойке проверять исключения? Например, что-то вроде
                                                              WaitForSingleObject(hThreadBEvent)
                                                              if lThreadB.Outcome == oFailure 
                                                                CheckException(lThreadB)....
                                                              ...
                                                              ...
                                                              Thread.HandleException(Exception){
                                                              Terminate;
                                                              Outcome = oFailure;
                                                              SetEvent(hmyEvent)
                                                              }
                                                              

                                                                0

                                                                Прямо скажем, .net-овский Task, который местная реализация монады Future, как-то так и делает (не по реализации, а по результирующему поведению):


                                                                var task = Task.Run(somework);
                                                                //...
                                                                await task; // завершится только тогда, когда somework закончен, и если в somework был эксепшн, он будет выкинут здесь
                                                        +1
                                                        Так, простите, где здесь пример того, что исключения не работают в многопоточном окружении?
                                                        Если исключение будет порождено методом run, оно поймается. Исключения, порождаемые some_work'ом, должны ловиться исключением обертки Task, запускающей отдельный поток.
                                                      0
                                                      функция, которая может то выполниться, то зафейлиться, в принципе не может быть чистой. Просто при использовании исключений она выглядит чистой, а ошибки и результат выполнения позитивного сценария обрабатываются в разных местах

                                                    Only users with full accounts can post comments. Log in, please.