На самом деле, это отличная идея - использовать исключения в некоторых случаях, когда «нормальные» программисты не думают об их использовании. Например, в синтаксическом анализаторе, который запускает «правило» и обнаруживает, что оно не работает, исключение является довольно хорошим способом возврата к правильной точке восстановления. (Это похоже на то, как вы предлагаете выйти из рекурсии.)
Существует классическое возражение о том, что «исключения не лучше, чем goto», что явно ложно. В Java и большинстве других достаточно современных языков у вас могут быть вложенные обработчики исключений и finally
обработчики, и поэтому, когда управление передается через исключение, хорошо разработанная программа может выполнять очистку и т. Д. Фактически, таким образом, исключения находятся в нескольких Способы предпочтительны для возврата кодов, так как с кодом возврата вы должны добавить логику в КАЖДУЮ точку возврата, чтобы проверить код возврата и найти и выполнить правильную логику finally
(возможно, несколько вложенных частей) перед выходом из подпрограммы. Для обработчиков исключений это достаточно автоматически, с помощью вложенных обработчиков исключений.
Исключения идут с некоторым «багажом» - трассировка стека в Java, например. Но исключения Java на самом деле довольно эффективны (по крайней мере, по сравнению с реализациями в некоторых других языках), поэтому производительность не должна быть большой проблемой, если вы не используете исключения слишком сильно.
[Я добавлю, что у меня 40-летний опыт программирования, и я использую исключения с конца 70-х годов. Самостоятельно «изобрел» try / catch / finally (назвал это BEGIN / ABEXIT / EXIT) около 1980 года.]
«незаконное» отступление:
Я думаю, что в этих обсуждениях часто упускается то, что проблема № 1 в вычислительной технике заключается не в стоимости или сложности, не в стандартах или производительности, а в контроле.
Под «контролем» я не подразумеваю «поток управления», «язык управления», «управление оператором» или любой другой контекст, в котором часто используется термин «управление». Я имею в виду «контроль сложности», но это больше, чем «концептуальный контроль».
Мы все сделали это (по крайней мере, те из нас, кто занимался программированием дольше, чем около 6 недель) - начали писать «простую небольшую программу» без реальной структуры или стандартов (кроме тех, которые мы могли бы обычно использовать) использовать), не беспокоясь о его сложности, потому что он «простой» и «одноразовый». Но затем, в одном случае из 10 или в одном случае из 100, в зависимости от контекста, «простая маленькая программа» превращается в чудовище.
Мы теряем «концептуальный контроль» над ним. Исправление одной ошибки вводит еще две. Контроль и поток данных программы становится непрозрачным. Он ведет себя так, что мы не можем его понять.
И все же, по большинству стандартов, эта "простая маленькая программа" не так уж сложна. Это не так много строк кода. Очень вероятно (поскольку мы - опытные программисты), оно разбито на «подходящее» количество подпрограмм. Запустите его с помощью алгоритма измерения сложности и, скорее всего (поскольку он все еще относительно мал и «подпрограммирован»), он получит оценку как не очень сложную.
В конечном счете, поддержание концептуального управления является движущей силой практически всех программных инструментов и языков. Да, такие вещи, как ассемблеры и компиляторы, делают нас более продуктивными, а производительность является заявленной движущей силой, но большая часть этого повышения производительности заключается в том, что нам не нужно заниматься «не относящимися к делу» деталями и вместо этого мы можем сосредоточиться на концепциях, которые мы хотим реализовать.
Основные достижения в концептуальном управлении произошли на ранних этапах истории вычислений, когда появились «внешние подпрограммы» и стали все более и более независимыми от их среды, что позволило «разделить проблемы», когда разработчику подпрограммы не нужно было много знать о среда подпрограммы, и пользователь подпрограммы не должен был знать много о внутренних функциях подпрограммы.
Простая разработка BEGIN / END и "{...}" привела к аналогичным улучшениям, поскольку даже "встроенный" код мог бы выиграть от некоторой изоляции между "там" и "здесь".
Многие инструменты и языковые функции, которые мы считаем само собой разумеющимися, существуют и полезны, поскольку они помогают поддерживать интеллектуальный контроль над все более сложными структурами программного обеспечения. И можно довольно точно оценить полезность нового инструмента или функции по тому, как она помогает в этом интеллектуальном управлении.
Одним из самых больших оставшихся проблем является управление ресурсами. Здесь под «ресурсом» я подразумеваю любую сущность - объект, открытый файл, выделенную кучу и т. Д. - которые могут быть «созданы» или «выделены» в ходе выполнения программы и впоследствии нуждаются в некоторой форме освобождения. Изобретение «автоматического стека» стало первым шагом - переменные могли быть размещены «в стеке», а затем автоматически удалены, когда подпрограмма, «распределившая» их, завершилась. (В свое время это была очень противоречивая концепция, и многие «авторитеты» рекомендовали не использовать эту функцию, поскольку она влияла на производительность.)
Но в большинстве (всех?) Языках эта проблема все еще существует в той или иной форме. Языки, которые используют явную кучу, должны «удалить» все, что вы «новый», например. Открытые файлы должны быть как-то закрыты. Замки должны быть освобождены. Некоторые из этих проблем могут быть изощренными (например, с помощью кучи GC) или скрыты (счетчик ссылок или «родительские функции»), но нет способа устранить или скрыть их все. И хотя решение этой проблемы в простом случае является довольно простым (например, new
объект, вызов подпрограммы, которая его использует, а затем delete
это), реальная жизнь редко бывает такой простой. Весьма распространено иметь метод, который делает дюжину или около того различных вызовов, несколько случайным образом распределяя ресурсы между вызовами, с различным «временем жизни» для этих ресурсов. И некоторые из вызовов могут возвращать результаты, которые изменяют поток управления, в некоторых случаях вызывая выход из подпрограммы, или могут вызывать цикл вокруг некоторого подмножества тела подпрограммы. Знание того, как освободить ресурсы в таком сценарии (освобождение всех правильных и ни одного неправильного), является проблемой, и это становится еще более сложным, поскольку подпрограмма изменяется со временем (как и весь код любой сложности).
Базовая концепция механизма try/finally
(на мгновение игнорируя аспект catch
) решает вышеуказанную проблему довольно хорошо (хотя я и не совсем признаю). С каждым новым ресурсом или группой ресурсов, которыми нужно управлять, программист вводит блок try/finally
, помещая логику освобождения в предложение finally. В дополнение к практическому аспекту обеспечения того, что ресурсы будут освобождены, этот подход имеет то преимущество, что четко очерчивает «объем» задействованных ресурсов, предоставляя своего рода документацию, которая «принудительно поддерживается».
Тот факт, что этот механизм связан с механизмом catch
, является немного случайным, так как тот же механизм, который используется для управления ресурсами в обычном случае, используется для управления ими в случае «исключения». Поскольку «исключения» (якобы) редки, всегда разумно минимизировать количество логики в этом редком пути, поскольку он никогда не будет так хорошо проверен, как основная линия, и поскольку «концептуализация» случаев ошибок особенно трудна для среднего программист.
Конечно, try/finally
имеет некоторые проблемы. Одним из первых среди них является то, что блоки могут стать настолько глубокими, что структура программы становится неясной, а затемненной. Но эта проблема является общей для do
циклов и if
операторов, и она ожидает некоторого вдохновенного понимания от дизайнера языка. Большая проблема заключается в том, что try/finally
имеет багаж catch
(и, что еще хуже, исключение), что означает, что он неизбежно должен быть гражданином второго сорта. (Например, finally
даже не существует как концепция в байт-кодах Java, кроме устаревшего ныне механизма JSB / RET.)
Есть и другие подходы.В IBM iSeries (или «System i», или «IBM i», или как они их сейчас называют) есть концепция присоединения обработчика очистки к заданному уровню вызова в стеке вызовов, который будет выполняться, когда связанная программа завершит работу (или выйдет ненормально).).Хотя это в его нынешнем виде неуклюже и не совсем подходит для тонкого уровня управления, необходимого в Java-программе, например, он указывает на потенциальное направление.
И, конечно, в C ++В языковом семействе (но не в Java) существует возможность создания экземпляра класса, представляющего ресурс, в качестве автоматической переменной, и деструктор объекта обеспечивает «очистку» при выходе из области видимости переменной.(Обратите внимание, что эта схема, по сути, использует try / finally.) Это отличный подход во многих отношениях, но он требует либо набора общих классов «очистки», либо определения нового класса для каждого отдельного типа.ресурса, создавая потенциальное «облако» текстуально громоздких, но относительно бессмысленных определений классов.(И, как я уже сказал, это не вариант для Java в его нынешнем виде.)
Но я отвлекся.