Использование Throwable для других вещей, кроме исключений - PullRequest
6 голосов
/ 01 августа 2011

Я всегда видел Throwable / Exception в контексте ошибок. Но я могу вспомнить ситуации, в которых было бы неплохо расширить Throwable, чтобы вырваться из стека рекурсивных вызовов методов. Скажем, например, вы пытались найти и вернуть какой-то объект в дереве с помощью рекурсивного поиска. Как только вы найдете его, вставьте его в Carrier extends Throwable, бросьте его и поймайте в методе, который вызывает рекурсивный метод.

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

Отрицательный: у вас есть трассировка стека, которая вам не нужна. Также блок try/catch становится нелогичным.

Вот идиотски простое использование:

public class ThrowableDriver {
    public static void main(String[] args) {
        ThrowableTester tt = new ThrowableTester();
        try {
            tt.rec();
        } catch (TestThrowable e) {
            System.out.print("All good\n");
        }
    }
}

public class TestThrowable extends Throwable {

}

public class ThrowableTester {
    int i=0;

    void rec() throws TestThrowable {
        if(i == 10) throw new TestThrowable();
        i++;
        rec();
    }
}

Вопрос в том, есть ли лучший способ достичь того же? Кроме того, есть ли что-то плохое в таких действиях?

Ответы [ 5 ]

7 голосов
/ 01 августа 2011

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

Существует классическое возражение о том, что «исключения не лучше, чем 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 в его нынешнем виде.)

Но я отвлекся.

3 голосов
/ 01 августа 2011

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

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

Существует довольно много связанныхвопросы по SO:

2 голосов
/ 01 августа 2011

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

Обычная мудрость: «Исключения составляют исключительные обстоятельства». Как вы заметили, Throwable в теории звучит более обобщенно, но, за исключением исключений и ошибок, он не предназначен для более широкого использования. Из документов :

Класс Throwable является суперклассом всех ошибок и исключений. на языке Java.

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

1 голос
/ 01 августа 2011

Вопрос в том, есть ли лучший способ достичь того же?Кроме того, есть ли что-то плохое в таких действиях?

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

Кроме того, исключения составляют управляемые gotos, почти эквивалентные прыжкам в длину.Да, да, они могут быть вложенными, и в таких языках, как Java, вы можете иметь свои хорошие блоки finally и все.Тем не менее, это все, что они есть, и как таковые, они не предназначены для замены общего случая для ваших типичных структур управления.Более четырех десятилетий коллективных, отраслевых знаний говорят нам о том, что, в общем, нам следует избегать таких вещей ЕСЛИ у вас есть очень веская причина для этого.

И это относится к сути вашего первого вопроса.Да, есть лучший способ (на примере кода) ... просто используйте ваши типичные структуры управления:

// class and method names remain the same, though using 
// your typical logical control structures

public class ThrowableDriver {
    public static void main(String[] args) {
        ThrowableTester tt = new ThrowableTester();
        tt.rec();
        System.out.print("All good\n");
        }
    }
}

public class ThrowableTester {
    int i=0;

    void rec() {
        if(i == 10) return;
        i++;
        rec();
    }
}

Видите?Simpler.Меньше строк кода.Никаких лишних попыток / вылова или ненужных исключений.Вы достигаете того же самого.

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

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

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

Если вы не беспокоитесь о логике возврата, просто проигнорируйте возврат или определите свой метод как тип void.Упаковка в try / catch просто делает код более сложным, чем необходимо.Если вы не заботитесь о возвращении, я уверен, что вы заботитесь о методе для завершения.Так что все, что вам нужно, это просто вызвать его (как в примере кода, который я предоставил в этом посте).

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

Дешевле получить толчок возврата (в значительной степени ссылка на объект в JVM) в стек до завершения метода, чем вести весь учет, связанный с генерацией исключения (запускать эпилоги и заполнять потенциально большую трассировку стека) и перехватывать ее (обход трассировки стека.) JVM или нет, это базовый материал CS 101.

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

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

0 голосов
/ 01 августа 2011
...