10

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

这个想法是我想缩短访问者对子树的递归探索,而不必在每个方法调用中检查“停止”标志。具体来说,我正在使用抽象语法树上的访问者构建控制流图。AST 中的一条return语句应该停止对子树的探索,并将访问者发送回最近的封闭 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),所以抛出异常并没有真正的帮助。我已经实施了enumvisitReturnStatement. 幸运的是,这只需要在少数地方进行检查(例如,visitCompoundStatement),因此它实际上比抛出异常要少一些麻烦。

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

4

7 回答 7

4

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

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

此外,有些人认为未经检查的异常并没有那么糟糕。您的使用让我想起了 Eclipse 的OperationCanceledException,它用于清除长时间运行的后台任务。

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

于 2008-12-15T19:56:42.893 回答
4

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

你有几个更干净的选择。

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. Disjoint Union(又名 The Either Bifunctor)

此技术允许您从同一方法返回两种不同类型中的一种。这有点像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
}

Either 类编写起来并不难,但是Functional Java 库提供了一个功能齐全的实现

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 更容易编写,但功能性 Java 库同样提供了一个功能齐全的实现

有关 Option 和 Either 的详细讨论,请参见此处。

于 2008-12-16T05:48:42.853 回答
0

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

于 2008-12-15T19:35:57.880 回答
0

我为您看到以下选项:

  1. 继续定义那个RuntimeException子类。通过在最一般的调用中捕获您的异常dispatch并报告该异常来检查是否存在严重问题。
  2. 如果它认为搜索应该突然结束,让节点处理代码返回一个特殊对象。这仍然迫使您检查返回值而不是捕获异常,但您可能更喜欢这种方式的代码外观。
  3. 如果树遍历因某些外部因素而停止,请在子线程内完成所有操作,并在该对象中设置一个同步字段,以便告诉线程过早停止。
于 2008-12-15T19:47:44.237 回答
0

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

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

访客模式

于 2008-12-15T20:11:13.847 回答
0

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

于 2008-12-15T20:46:49.920 回答
0

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

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

如果:

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

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

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

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

于 2008-12-15T22:04:05.710 回答