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

У меня сильное искушение использовать непроверенное исключение в качестве короткого замыкания конструкции потока управления в программе на 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 перехватывает и упаковывает исключения (даже RuntimeExceptions), поэтому генерация исключений на самом деле не помогает. Я реализовал предложение вернуть тип enum из visitReturnStatement. К счастью, это нужно проверять только в небольшом количестве мест (например, visitCompoundStatement), поэтому на самом деле это немного проще, чем создание исключений.

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


person Chris Conway    schedule 15.12.2008    source источник
comment
Ваш пункт № 2 заставил меня хихикнуть. Хорошие инстинкты у этого парня...   -  person Paul Brinkley    schedule 15.12.2008


Ответы (7)


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

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

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

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

person Dave Ray    schedule 15.12.2008
comment
Чувствую себя немного чище... ;-) - person Chris Conway; 15.12.2008
comment
Ты все еще в грязи, но только по пояс, может быть :) - person Dave Ray; 16.12.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. Непересекающийся союз (также известный как бифунктор либо)

Этот метод позволяет вам возвращать один из двух разных типов из одного и того же метода. Это немного похоже на технику 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);
}

Выполнение вызова теперь намного чище, потому что вам не нужен 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. Опционная монада

Это немного похоже на нулевое значение, безопасное для типов, это хорошая техника, которую можно использовать, когда вы не хотите возвращать значение для некоторых входных данных, но вам не нужны исключения или коды ошибок. Обычно люди возвращают то, что называется «дозорным значением», но 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 написать еще проще, чем Choose, но опять же, полнофункциональная реализация предоставляется функциональной библиотекой Java.

Подробное обсуждение опций и иных вариантов см. здесь.

person Apocalisp    schedule 16.12.2008

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

person Elie    schedule 15.12.2008
comment
Я не думаю, что возврат значения решит проблему. Метод, вызывающий исключение, не вернет NULL. У вас все еще есть возможность проверить ранний возврат на каждом узле. - person Chris Conway; 15.12.2008
comment
О, подожди. Может быть, вы имеете в виду что-то более похожее на номер 2 Пола Бринкли? - person Chris Conway; 15.12.2008

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

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

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

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

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

person Robin    schedule 15.12.2008
comment
Если вы собираетесь проголосовать против, оставьте комментарий, пожалуйста. Это помогает нам всем. - person Robin; 15.12.2008
comment
Вы не ответили на вопрос, вы ошибочно утверждали, что посетители никогда не должны изменять структуру данных или возвращать значение, в вопросе не упоминалась обработка ошибок, но вы упомянули об этом. Я не думаю, что вы поняли вопрос или настоящие опасения спрашивающего. - person Steven Huwig; 15.12.2008

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

person Yoni Roit    schedule 15.12.2008

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

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

If:

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

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

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

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

person Dan Vinton    schedule 15.12.2008