Java: использование RuntimeException для выхода из гостевой системы

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

Идея состоит в том, что я хочу прервать рекурсивное исследование поддеревьев посетителем без необходимости проверять флаг "стоп" при каждом вызове метода. В частности, я строю график потока управления, используя посетителя над абстрактным деревом синтаксиса. Заявление A return в АСТ должно прекратить изучение поддерева и отправить посетителя обратно в ближайшее окружение if/then или loop.

Суперкласс класса Visitor (из XTC library) определяет

Object dispatch(Node n)

который обращается через методы отражения формы

Object visitNodeSubtype(Node n)

dispatch не объявляется, чтобы делать какие-либо исключения, поэтому я объявил закрытый класс, который расширяет RuntimeException

private static class ReturnException extends RuntimeException {
}

Теперь метод посетителя для оператора return выглядит как

Object visitReturnStatement(Node n) {
    // handle return value assignment...
    // add flow edge to exit node...
    throw new ReturnException();
}

и каждый составной оператор должен обрабатывать ReturnException

Object visitIfElseStatement(Node n) {
  Node test = n.getChild(0);
  Node ifPart = n.getChild(1);
  Node elsePart = n.getChild(2);

  // add flow edges to if/else... 

  try{ dispatch(ifPart); } catch( ReturnException e ) { }
  try{ dispatch(elsePart); } catch( ReturnException e ) { }
}

Все работает отлично, кроме:

  • Я могу забыть где-то ReturnException, и компилятор не предупредит меня.
  • Я чувствую себя грязным.

Есть ли лучший способ сделать это? Есть ли Java-шаблон, о котором я не знаю, для реализации такого типа нелокального управления потоком?

[UPDATE] Этот конкретный пример оказывается несколько недействительным: суперкласс класса Visitor ловит и обматывает исключения (даже RuntimeException s), поэтому бросание исключений действительно не помогает. Я внедрил предложение вернуть тип enum из visitReturnStatement. К счастью, это нужно только проверить в небольшом количестве мест (например, visitCompoundStatement), так что это на самом деле немного меньше хлопот, чем бросание исключений.

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

Ответ 1

Я думаю, что это разумный подход по нескольким причинам:

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

Кроме того, есть те, которые утверждали, что непроверенные исключения не так уж плохи. Ваше использование напоминает мне Eclipse OperationCanceledException, который используется для выключения длительных фоновых задач.

Это не идеально, но, если хорошо документировано, мне кажется хорошо.

Ответ 2

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

У вас есть несколько вариантов, которые значительно более чисты.

1. Исправление исключений

Хорошая техника использования, когда вы ограничены исключениями, которые вы можете бросить, если вы не можете выбросить проверенное исключение, верните объект, который будет вызывать проверенное исключение. Например, java.util.concurrent.Callable является экземпляром этого функтора.

См. здесь подробное объяснение этой техники.

Например, вместо этого:

public Something visit(Node n) {
  if (n.someting())
     return new Something();
  else
     throw new Error("Remember to catch me!");
}

Сделайте это:

public Callable<Something> visit(final Node n) {
  return new Callable<Something>() {
    public Something call() throws Exception {
      if (n.something())
         return new Something();
      else
         throw new Exception("Unforgettable!");
    }
  };
}

2. Несвязанный союз (a.k.a. Либо бифунтер)

Этот метод позволяет вам вернуть один из двух разных типов из того же метода. Это немного похоже на метод Tuple<A, B>, который большинство людей знакомо для возврата более одного значения из метода. Однако вместо того, чтобы возвращать значения обоих типов A и B, это включает в себя возврат одного значения типа A или B.

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

public Either<Fail, Something> visit(final Node n) {
  if (n.something())
    return Either.<Fail, Something>right(new Something());
  else
    return Either.<Fail, Something>left(Fail.DONE);
}

Выполнение вызова теперь намного более чистое, потому что вам не нужен try/catch:

Either<Fail, Something> x = node.dispatch(visitor);
for (Something s : x.rightProjection()) {
  // Do something with Something
}
for (Fail f : x.leftProjection()) {
  // Handle failure
}

Любой класс не очень сложно писать, но полнофункциональная реализация предоставляется функциональной библиотекой Java.

3. Вариант Monad

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

Теперь у вас есть...

public Option<Something> visit(final Node n) {
  if (n.something())
    return Option.some(new Something());
  else
    return Option.<Something>none();
}    

Вызов приятный и чистый:

Option<Something> s = node.dispatch(visitor));
if (s.isSome()) {
  Something x = s.some();
  // Do something with x.
}
else {
  // Handle None.
}

И тот факт, что он monad позволяет вам переадресовывать вызовы без обработки специального значения None:

public Option<Something> visit(final Node n) {
  return dispatch(getIfPart(n).orElse(dispatch(getElsePart(n)));
}    

Класс Option еще проще писать, чем Либо, но опять-таки полнофункциональная реализация обеспечивается функциональной библиотекой Java.

См. здесь подробное обсуждение опции и любого из них.

Ответ 3

Есть ли причина, по которой вы не просто возвращаете значение? Например, NULL, если вы действительно хотите ничего не вернуть? Это было бы намного проще и не помешало бы исключить неконтролируемое исключение во время выполнения.

Ответ 4

Я вижу следующие варианты для вас:

  • Идем дальше и определяем подкласс RuntimeException. Проверьте наличие серьезных проблем, перехватив свое исключение в самом общем вызове dispatch и сообщив об этом, если он дойдет до этого.
  • Если код обработки node возвращает специальный объект, если он считает, что поиск должен заканчиваться внезапно. Это по-прежнему заставляет вас проверять возвращаемые значения вместо улавливания исключений, но вам может понравиться внешний вид кода.
  • Если древовидная дорожка должна быть остановлена ​​каким-то внешним фактором, сделайте все это внутри подпотока и установите синхронизируемое поле в этом объекте, чтобы остановить поток преждевременно.

Ответ 5

Почему вы возвращаете ценность от своего посетителя? Соответствующий метод посетителя вызывается классами, которые посещаются. Вся выполненная работа инкапсулируется внутри самого класса посетителя, она ничего не должна возвращать и обрабатывать собственные ошибки. Единственным обязательным требованием для вызывающего класса является вызов соответствующего метода visitXXX, не более того. (Предполагается, что вы используете перегруженные методы, как в вашем примере, в отличие от переопределения одного метода посещения() для каждого типа).

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

Шаблон посетителя

Ответ 6

Вам нужно использовать посетителя из XTC? Это довольно тривиальный интерфейс, и вы можете реализовать свой собственный, который может бросить проверено ReturnException, которое вы не забыли поймать там, где это необходимо.

Ответ 7

Я не использовал библиотеку XTC, которую вы упомянули. Как он обеспечивает дополнительную часть шаблона посетителя - метод accept(visitor) на узлах? Даже если это диспетчер, основанный на отражении, все равно должно быть что-то, что обрабатывает рекурсию по дереву синтаксиса?

Если этот структурный итерационный код легко доступен, и вы еще не используете возвращаемое значение из ваших методов visitXxx(node), можете ли вы использовать простое перечисляемое возвращаемое значение или даже логический флаг, сообщая accept(visitor) не рекурсия в дочерние узлы?

Если:

  • accept(visitor) явно не реализуется узлами (там происходит некоторое отражение поля или аксессуаров, или узлы просто реализуют интерфейс для получения дочерних элементов для некоторой стандартной логики потока управления или по любой другой причине...) и

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

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

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