【问题标题】:Java: using a RuntimeException to escape from a VisitorJava:使用 RuntimeException 从访客中逃脱
【发布时间】:2025-11-25 05:55:01
【问题描述】:

我非常想在 Java 程序中使用未经检查的异常作为短路控制流构造。我希望这里有人可以建议我以更好、更清洁的方式来处理这个问题。

我的想法是我想缩短访问者对子树的递归探索,而不必在每个方法调用中检查“停止”标志。具体来说,我正在使用抽象语法树上的访问者构建控制流图。 AST 中的return 语句应该停止探索子树并将访问者发送回最近的封闭 if/then 或循环块。

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

这一切都很好,除了:

  1. 我可能忘记在某处捕捉ReturnException,编译器不会警告我。
  2. 我觉得很脏。

有没有更好的方法来做到这一点?是否有我不知道的 Java 模式来实现这种非本地控制流?

[更新] 这个具体的例子有些无效:Visitor 超类捕获并包装异常(甚至是RuntimeExceptions),所以抛出异常并没有真正的帮助。我已经实施了从visitReturnStatement 返回enum 类型的建议。幸运的是,这只需要在少数地方进行检查(例如,visitCompoundStatement),因此它实际上比抛出异常要少一些麻烦。

总的来说,我认为这仍然是一个有效的问题。虽然也许,如果您不依赖第三方库,则可以通过明智的设计避免整个问题。

【问题讨论】:

  • 你的第 2 点让我笑了。这家伙的直觉很好......

标签: java exception visitor-pattern callcc


【解决方案1】:

我认为这是一种合理的方法,原因如下:

  • 您使用的是第 3 方,无法添加已检查的异常
  • 在大量访问者中到处检查返回值时只需要在少数人中进行检查是不必要的负担

另外,有些人认为unchecked exceptions aren't all that bad。您的使用让我想起了 Eclipse 的 OperationCanceledException,它用于清除长时间运行的后台任务。

这并不完美,但是,如果有据可查,对我来说似乎没问题。

【讨论】:

  • 让我感觉更干净... ;-)
  • 你还在泥泞中,但可能只到你的腰部 :)
【解决方案2】:

将运行时异常作为控制逻辑抛出绝对是个坏主意。你觉得脏的原因是你绕过了类型系统,即你的方法的返回类型是一个谎言。

您有几个更干净的选项。

1.异常函子

一个很好的技术,当你被限制在你可能抛出的异常中时,如果你不能抛出一个检查的异常,返回一个将抛出一个检查的异常的对象。例如,java.util.concurrent.Callable 就是这个函子的一个实例。

See here for a detailed explanation of this technique.

例如,而不是这个:

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。不相交并集(又名 The Either Bifunctor)

此技术允许您从同一方法返回两种不同类型中的一种。这有点像大多数人熟悉的Tuple&lt;A, B&gt; 技术,用于从方法返回多个值。但是,这不是返回 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
}

Either 类不是很难写,但是a full-featured implementation is provided by the Functional Java library

3.选项单子

有点像类型安全的 null,当您不想为某些输入返回值但不需要异常或错误代码时,这是一种很好的技术。通常,人们会返回所谓的“哨兵值”,但 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.
}

事实上,它是一个monad,让您可以在不处理特殊 None 值的情况下链接调用:

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

Option 类比 Either 更容易编写,但同样,a full-featured implementation is provided by the Functional Java library

See here for a detailed discussion of Option and Either.

【讨论】:

    【解决方案3】:

    您是否有理由不只是返回一个值?比如NULL,如果你真的想什么都不返回?这会简单得多,并且不会冒险引发未经检查的运行时异常。

    【讨论】:

    • 我认为返回值不会解决问题。抛出异常的方法不会返回 NULL。您仍然需要检查每个节点是否提前返回。
    • 哦,等等。也许你的意思更像是保罗布林克利的 #2?
    【解决方案4】:

    我为您看到以下选项:

    1. 继续定义RuntimeException 子类。通过在对dispatch 的最一般调用中捕获您的异常并报告该异常来检查是否存在严重问题。
    2. 如果节点处理代码认为搜索应该突然结束,它会返回一个特殊对象。这仍然会强制您检查返回值而不是捕获异常,但您可能更喜欢这样的代码外观。
    3. 如果树遍历因某些外部因素而停止,请在子线程内执行所有操作,并在该对象中设置同步字段,以告知线程提前停止。

    【讨论】:

      【解决方案5】:

      为什么要从访问者那里返回值?访问者的适当方法由正在访问的类调用。所有完成的工作都封装在访问者类本身中,它不应该返回任何内容并处理它自己的错误。调用类所需的唯一义务是调用适当的 visitXXX 方法,仅此而已。 (这假设您在示例中使用重载方法,而不是为每种类型覆盖相同的 visit() 方法)。

      访问的类不应被访问者更改或必须知道它的作用,除非它允许访问发生。返回值或抛出异常将违反这一点。

      Visitor Pattern

      【讨论】:

      • 如果您要投反对票,请发表评论。它对我们所有人都有帮助。
      • 你没有回答这个问题,你错误地声称访问者永远不应该改变数据结构或返回一个值,这个问题没有提到错误处理但你提到了它。我认为您没有理解问题或提问者的实际担忧。
      【解决方案6】:

      您必须使用来自 XTC 的访客吗?这是一个非常简单的接口,你可以实现你自己的,它可以抛出 checked ReturnException,你不会忘记在需要的地方捕获它。

      【讨论】:

        【解决方案7】:

        我没有使用过您提到的 XTC 库。它如何提供访问者模式的补充部分——节点上的accept(visitor) 方法?即使这是一个基于反射的调度程序,仍然必须有一些东西可以处理语法树的递归?

        如果此结构迭代代码易于访问,并且您尚未使用 visitXxx(node) 方法的返回值,您是否可以利用简单的枚举返回值,甚至是布尔标志,告诉 accept(visitor) 不要递归到子节点?

        如果:

        • accept(visitor) 不是由节点显式实现的(有一些字段或访问器反射正在进行,或者节点只是为某些标准控制流逻辑实现子获取接口,或出于任何其他原因.. .),以及

        • 您不想弄乱库的结构迭代部分,或者它不可用,或者不值得努力......

        那么作为最后的手段,我猜想异常可能是您在使用 vanilla XTC 库时唯一的选择。

        不过是一个有趣的问题,我可以理解为什么基于异常的控制流会让你感到肮脏​​......

        【讨论】: