4

在另一个问题中,一位用户建议编写如下代码:

def list = ['a', 'b', 'c', 'd']
def i = 0; 
assert list.collect { [i++] } == [0, 1, 2, 3]

在其他语言中,这样的代码被认为是不好的做法,因为 collect 的内容会改变其上下文的状态(这里它会改变 的值i)。换句话说,闭包有副作用。

这样的高阶函数应该能够并行运行闭包,并再次将其组装到一个新列表中。如果闭包中的处理是长时间的、CPU 密集型操作,则可能值得在单独的线程中执行它们。collect更改为使用 anExecutorCompletionService来实现这一点很容易,但它会破坏上面的代码。

另一个问题的例子是,如果出于某种原因,collect以相反的顺序浏览集合,在这种情况下,结果将是[3, 2, 1, 0]. 请注意,在这种情况下,列表没有被还原,0 实际上是对 'd' 应用闭包的结果!

有趣的是,这些函数记录在Collection 的 JavaDoc中的“迭代此集合”中,这表明迭代是顺序的。

groovy 规范是否明确定义了高阶函数(如collectoreach )中的执行顺序?上面的代码是坏了,还是没问题?

4

2 回答 2

3

由于您上面给出的原因,我不喜欢在我的闭包中依赖显式的外部变量。

事实上,我必须定义的变量越少,我就越快乐;-)

对于可能并行的事情,如果证明单线程处理太多,请始终编写代码以用某种程度的GPars 可爱来包装它。为此,正如您所说,您希望尽可能少的可变性并尝试完全避免副作用(例如上面的外部计数器模式)

至于问题本身,如果我们以collect函数为例,并检查源代码,我们可以看到给定一个Object(Collection并且Map以类似的方式完成,在如何引用 Iterator 方面略有不同) 它沿着迭代InvokerHelper.asIterator(self),添加每个闭包调用结果列表的结果。

InvokerHelper.asIterator(再次来源是here)基本上调用iterator()传入的对象上的方法。

因此对于Lists等,它将按照迭代器定义的顺序向下迭代对象。

因此,可以编写自己的遵循Iterable 接口设计的类(由于鸭子类型,不需要实现Iterable),并定义如何迭代集合。

我认为通过询问 Groovy 规范,这个答案可能不是您想要的,但我认为没有答案。Groovy 从来没有真正有过一个“完整”的规范(事实上,这是一些人不喜欢的关于 groovy 的观点)。

于 2011-11-11T12:12:59.997 回答
1

我认为保持函数通过collectfindAll无副作用通常是一个好主意,不仅可以保持较低的复杂性,而且可以使代码更加并行友好,以防将来需要并行执行。

但是在each保持函数副作用自由的情况下没有多大意义,因为它不会做任何事情(实际上这种方法的唯一目的是替换作为 for-each 循环的行为)。Groovy 的文档有一些使用each(及其变体eachWithIndex和)的示例,这些示例reverseEach需要定义执行顺序。

现在,从务实的角度来看,我认为有时可以在collect. 例如,要[index, value]成对转换列表,可以使用transposea和 range

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] }

collect现在,如果 Groovy 提供了一个带有 index-value-param 函数的类似方法(参见Jira issue ) ,这个例子就没有意义了,但它有点表明,有时实用性胜过纯度 IMO :)

于 2011-11-11T21:08:44.217 回答