在另一个问题中,用户建议编写类似的代码:
def list = ['a', 'b', 'c', 'd']
def i = 0;
assert list.collect { [i++] } == [0, 1, 2, 3]
在其他语言中,这样的代码被认为是不好的做法,因为collect的内容改变了它的上下文的状态(这里它改变了i
的值)。换句话说,封闭有副作用。
这样的高阶函数应该能够并行运行闭包,并再次将它组装在一个新的列表中。如果闭包中的处理很长,CPU密集型操作,则可能值得在单独的线程中执行它们。更改collect
以使用ExecutorCompletionService
来实现这一点很容易,但它会破坏上面的代码。
问题的另一个例子是,由于某种原因,collect
以相反的顺序浏览集合,在这种情况下,结果将是[3, 2, 1, 0]
。请注意,在这种情况下,列表尚未恢复,0实际上是将闭包应用于' d'!
有趣的是,这些功能记录在" 通过此集合迭代"在Collection's JavaDoc中,这表明迭代是连续的。
groovy规范是否明确定义了更高阶函数(如collect
或each
)的执行顺序?以上代码是否已损坏,或者可以吗?
答案 0 :(得分:3)
由于上面提到的原因,我不喜欢在我的闭包中依赖明确的外部变量。
事实上,我必须定义的变量越少,我就越快乐; - )
对于可能并行的东西,如果单个线程要处理得太多,那么代码总是用一些GPars loveliness级别来包装它。为此,正如您所说,您希望尽可能少的可变性并尝试完全避免副作用(例如上面的外部计数器模式)
至于问题本身,如果我们将collect
作为示例函数并检查the source code,我们可以看到给定Object
(Collection
和{{ 3}}以类似的方式完成,与Iterator的引用方式略有不同)它沿着InvokerHelper.asIterator(self)
进行迭代,将每个闭包调用的结果添加到结果列表中。
InvokerHelper.asIterator
(再次Map
)基本上调用传入的对象上的iterator()
方法。
因此,对于source is here等,它将按迭代器定义的顺序迭代对象。
因此可以编写自己的类,该类遵循Lists设计(不需要实现Iterable
,感谢duck-typing),并定义如何迭代集合
我想通过询问Groovy规范,这个答案可能不是你想要的,但我不认为有答案。 Groovy从来没有真正拥有“完整”的规范(事实上,这是关于Iterable interface的常规问题。
答案 1 :(得分:1)
我认为保持通过collect
或findAll
副作用的函数是一般的好主意,不仅是为了保持低复杂度,而且在并行执行时使代码更加并行友好将来需要。
但是在each
的情况下,保持函数副作用没有多大意义,因为它不会做任何事情(事实上,这种方法的唯一目的是取代act作为for - 每个循环)。 Groovy's documentation有一些使用each
(及其变体,eachWithIndex
和reverseEach
)的示例,这些示例需要定义执行顺序。
现在,从实用的角度来看,我认为在collect
等方法中使用带有一些副作用的函数有时可以。例如,要转换[index, value]
对中的列表transpose
和范围can be used
def list = ['a', 'b', 'c']
def enumerated = [0..<list.size(), list].transpose()
assert enumerated == [[0,'a'], [1,'b'], [2,'c']]
甚至是inject
def enumerated = list.inject([]) { acc, val -> acc << [acc.size(), val] }
但collect
和一个计数器也可以解决问题,我认为结果最具可读性:
def n = 0, enumerated = list.collect{ [n++, it] }
现在,如果Groovy使用index-value-param函数提供collect
和类似的方法(参见Jira issue),这个例子就没有意义了,但它有点表明有时候实用性比纯度高IMO:)