在Groovy闭包中模拟“continue”的最佳方式是什么?

70

似乎Groovy不支持在闭包内使用breakcontinue。有什么最好的方法可以模拟出这种行为吗?

revs.eachLine { line -> 
    if (line ==~ /-{28}/) {
         // continue to next line...
    }
}
6个回答

73

您只能支持干净地继续,而不能打破。特别是对于像eachLine和each这样的东西。无法支持break是因为这些方法的评估方式,没有考虑不完成循环可以传达给方法的任何事情。以下是如何支持continue的方法--

最佳方法(假设您不需要结果值)。

revs.eachLine { line -> 
    if (line ==~ /-{28}/) {
        return // returns from the closure
    }
}

如果你的示例确实如此简单,那么这对于可读性来说是很好的。

revs.eachLine { line -> 
    if (!(line ==~ /-{28}/)) {
        // do what you would normally do
    }
}

另一个选项是,在字节码级别上模拟通常会发生的“继续”操作。

revs.eachLine { line -> 
    while (true) {
        if (line ==~ /-{28}/) {
            break
        }
        // rest of normal code
        break
    }

}

通过异常处理是支持break的一种可能方式:

try {
    revs.eachLine { line -> 
        if (line ==~ /-{28}/) {
            throw new Exception("Break")
        }
    }
} catch (Exception e) { } // just drop the exception

如果您的类中可能会抛出实际异常,如NumberFormatException或IOException,为避免掩盖其他真实异常,您可能希望使用自定义异常类型。


34
使用异常来控制程序流程是一个不好的想法。创建异常需要对调用堆栈进行快照,这是很耗费资源的。 - John Flinchbaugh
6
如果你重写生成异常时调用堆栈的方法,让其什么也不做,那么就不会有调用堆栈信息。这就是自定义异常的优势。 - shemnon
10
这也是为了解决一个本来可以用闭包处理但却被看作循环结构的问题而额外增加的步骤。上述示例应更多地关注修复逻辑的整体意图,是要过滤行还是查找行才更为明确。 - Cliff

17

闭包不能使用break或continue语句,因为它们不是循环/迭代结构。相反,它们是处理/解释/处理迭代逻辑的工具。您可以通过仅从闭包返回而不进行处理来忽略给定的迭代,例如:

revs.eachLine { line -> 
    if (line ==~ /-{28}/) {
            return
    }

}

在闭包级别上并没有支持break,它是通过接受闭包的方法调用的语义隐含的。简而言之,这意味着你不应该像处理整个集合一样调用"each",而应该调用find,直到满足某个条件为止。大多数(全部?)情况下,当你需要从一个闭包中退出时,你真正想做的是在迭代过程中找到一个特定的条件,使得find方法不仅符合你的逻辑需求,还符合你的意图。不幸的是,有些API缺少对find方法的支持,例如File类。也许把时间花在争论是否应该包括break/continue上,可能不如把这些被忽视的领域添加find方法来得好。例如,像firstDirMatching(Closure c)或findLineMatching(Closure c)这样的方法可以很好地解决99+%的“为什么我无法从...中断?”的问题,这些问题经常出现在邮件列表中。话虽如此,你可以通过MetaClass或Categories轻松地自行添加这些方法。

class FileSupport {
   public static String findLineMatching(File f, Closure c) {
      f.withInputStream {
         def r = new BufferedReader(new InputStreamReader(it))
         for(def l = r.readLine(); null!=l; l = r.readLine())
             if(c.call(l)) return l
         return null
      }
   }
}

using(FileSupport) { new File("/home/me/some.txt").findLineMatching { line ==~ /-{28}/ }

把异常和其他神奇的东西结合起来可能会起作用,但在某些情况下会引入额外的开销,在其他情况下会使可读性变得复杂。真正的答案是查看您的代码,并询问自己是否真正进行的是迭代或搜索。


11
如果在Java中预先创建一个静态Exception对象,然后从闭包内抛出(静态)异常,运行时成本是最小的。真正的成本在于创建异常,而不是抛出异常。根据Scala发明者Martin Odersky的说法,许多JVM实际上可以将throw指令优化为单个跳转。
这可以用来模拟break:
final static BREAK = new Exception();
//...
try {
  ... { throw BREAK; }
} catch (Exception ex) { /* ignored */ }

2
那真是个非常聪明的想法。结合枚举来构建每个枚举预先创建的异常。甚至可以在枚举中放置一个抛出该异常的方法。Condition.BAD_WEATHER.fire(); - paulmurray
1
实际上,这个想法不是我自己的。Martin Odersky(Scala的发明者)在他的演讲中展示它作为解决同样问题的方案。我很多年前在Rensselaer的数据结构课程中有一位教授展示了一个不使用goto语句的问题解决方案,以及相同的解决方案使用了goto语句。后者要短得多。他的观点是有时实用性胜过优雅。 - Ralph

10

使用 return继续执行,使用任何闭包来 中止执行

示例

文件内容:

1
2
----------------------------
3
4
5

Groovy代码:

new FileReader('myfile.txt').any { line ->
    if (line =~ /-+/)
        return // continue

    println line

    if (line == "3")
        true // break
}

输出:

1
2
3

2
你很聪明。小心使用.any.。如果闭包中的最后一个语句与“true”相关联,它将停止执行。如果你想让逻辑或文件被完全处理,请把“false”作为任何闭包内的最后一个语句。我看到这篇文章之前花了几天时间。 - user2618844
2
如果你用line == "3"替换最后的if块,我认为会更加清晰。 - Ed Norris

4

在这种情况下,你应该考虑使用find()方法。它会在传递给它的闭包函数第一次返回true后停止执行。


2
使用 rx-java,您可以将可迭代对象转换为可观察对象。
然后,您可以使用 filter 替换 continue,并使用 takeWhile 替换 break
以下是一个示例:
import rx.Observable

Observable.from(1..100000000000000000)
          .filter { it % 2 != 1} 
          .takeWhile { it<10 } 
          .forEach {println it}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接