Java: использование RuntimeException для выхода из Visitor - PullRequest
10 голосов
/ 15 декабря 2008

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

Идея состоит в том, что я хочу прервать рекурсивное исследование поддеревьев посетителем без необходимости проверять флаг «стоп» при каждом вызове метода. В частности, я строю граф потока управления с помощью посетителя над абстрактным синтаксическим деревом. Оператор return в AST должен остановить исследование поддерева и отправить посетителя обратно в ближайший включающий if / then или блок цикла.

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

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 ) { }
}

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

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

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

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

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

Ответы [ 7 ]

4 голосов
/ 15 декабря 2008

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

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

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

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

4 голосов
/ 16 декабря 2008

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

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

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.

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

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

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

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

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

3. Опциональная монада

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

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

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

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

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

Класс Option написать даже легче, чем Either, но, опять же, полнофункциональная реализация предоставляется библиотекой Functional Java .

См. Подробное обсуждение Option и Either.

0 голосов
/ 16 декабря 2008

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

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

Если:

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

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

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

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

0 голосов
/ 15 декабря 2008

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

0 голосов
/ 15 декабря 2008

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

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

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

0 голосов
/ 15 декабря 2008

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

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

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

...