Swift 为什么过滤器(35;:)&x2019;当惰性地计算谓词时,它会被调用这么多次吗?
我看到了,在它的第一次修订版中,有类似的代码:Swift 为什么过滤器(35;:)&x2019;当惰性地计算谓词时,它会被调用这么多次吗?,swift,Swift,我看到了,在它的第一次修订版中,有类似的代码: let numbers = Array(0 ..< 50) let result = numbers.lazy .filter { // gets called 2-3x per element in the range (0...15)! print("Calling filter for: \($0)") return $0 % 3 == 0 } .prefix(5
let numbers = Array(0 ..< 50)
let result = numbers.lazy
.filter {
// gets called 2-3x per element in the range (0...15)!
print("Calling filter for: \($0)")
return $0 % 3 == 0
}
.prefix(5)
print(Array(result)) // [0, 3, 6, 9, 12]
让数字=数组(0..<50)
让result=numbers.lazy
.过滤器{
//在(0…15)范围内,每个元素被调用2-3倍!
打印(“为:\($0)调用筛选器”)
返回$0%3==0
}
.前缀(5)
打印(数组(结果))/[0,3,6,9,12]
通过使用惰性过滤器集合,它能够过滤满足给定谓词的numbers
的前5个元素(在本例中,可被3整除),而无需计算numbers
数组中的每个元素
然而,答案随后指出,过滤器(:)
的谓词可以对每个元素调用多次(1…15范围内的元素调用3次,0调用两次)
这个过滤器的惰性评估效率低下的原因是什么?有没有办法避免对同一元素多次求值?问题
这里的第一个罪魁祸首是通过使用前缀(:)
对惰性过滤器集合进行切片,在本例中,它返回LazyFilterBidirectionalCollection
的双向切片
通常,集合的切片
需要存储基本集合,以及对切片“查看”有效的索引范围。因此,为了创建LazyFilterBidirectionalCollection的切片以查看前5个元素,存储的索引范围必须是startIndex..
为了在fifthement
之后获得索引,LazyFilterBidirectionalCollection
必须迭代基本集合(numbers
),以找到满足谓词的第六个元素(您可以看到)
因此,上面示例中0…15范围内的所有元素都需要对照谓词进行检查,以便创建惰性过滤器集合的切片
第二个罪魁祸首是数组
的init(:)
,它接受与数组的元素
类型相同的序列
。调用序列上的\u copyToContiguousArray()
,对于大多数序列,该序列:
对于我们的切片,它将在fifthelement
之后遍历索引,直到索引,从而再次重新评估0…15范围内的元素
最后,生成切片的迭代器,并对其进行迭代,将序列中的每个元素添加到新数组的缓冲区中。对于双向片
,这将使用索引迭代器
,该迭代器只需推进索引并输出每个索引的元素即可
此遍历索引不会重新计算结果第一个元素之前的元素(请注意,在问题的示例中,0的计算次数减少了一次)的原因是,它不会直接访问LazyFilterBidirectionalCollection
的startIndex
,这是因为它没有直接访问LazyFilterBidirectionalCollection
。相反,迭代器可以从切片本身的开始索引开始工作
解决方案
简单的解决方案是避免为了获取其前缀而对惰性过滤器集合进行切片,而是惰性地应用前缀
实际上有两种前缀(:)
的实现。一个是,并返回一个子序列
(这是大多数标准库集合的切片形式)
另一种是,它返回一个AnySequence
——它在引擎盖下使用一个基本序列,它只需要一个迭代器并允许通过它进行迭代,直到给定数量的元素被迭代——然后停止返回元素
对于惰性集合而言,前缀(:)
的这种实现非常好,因为它不需要任何索引–它只是惰性地应用前缀
因此,如果你说:
let result : AnySequence = numbers.lazy
.filter {
// gets called 1x per element :)
print("Calling filter for: \($0)")
return $0 % 3 == 0
}
.prefix(5)
当您强制Swift使用序列前缀(:)
的默认实现时,过滤器(:)
的谓词在传递到数组的初始化器时,只会对数字的元素(直到第5次匹配)求值一次
防止对给定的惰性筛选器集合执行所有索引操作的简单方法是只使用惰性筛选器序列,这可以通过将希望对其执行惰性操作的集合包装在AnySequence
中来实现:
let numbers = Array(0 ..< 50)
let result = AnySequence(numbers).lazy
.filter {
// gets called 1x per element :)
print("Calling filter for: \($0)")
return $0 % 3 == 0
}
.dropFirst(5) // neither of these will do indexing,
.prefix(5) // but instead return a lazily evaluated AnySequence.
print(Array(result)) // [15, 18, 21, 24, 27]
让数字=数组(0..<50)
让结果=任意序列(数字)。惰性
.过滤器{
//每个元素调用1x:)
打印(“为:\($0)调用筛选器”)
返回$0%3==0
}
.dropFirst(5)//这两种方法都不能进行索引,
.prefix(5)//但返回一个延迟计算的AnySequence。
打印(数组(结果))/[15,18,21,24,27]
但是请注意,对于双向收集,这可能会对收集结束时的操作产生不利影响,因为此时必须迭代整个序列才能到达结束
对于suffix(:)
和dropLast(:)
等操作,在序列上使用惰性集合(至少对于较小的输入)可能更有效,因为它们可以简单地从集合的末尾进行索引
尽管如此,与所有与性能相关的问题一样,您应该首先检查这是否是一个问题,然后运行您自己的测试,看看哪种方法更适合您的实现
结论(TL;DR)
因此,在所有这些之后——这里要学到的教训是,您应该警惕这样一个事实:切片一个懒惰的过滤器集合可能会重新评估基本集合中的每个元素
let result : AnySequence = numbers.lazy
.filter {
// gets called 1x per element :)
print("Calling filter for: \($0)")
return $0 % 3 == 0
}
.prefix(5)
let numbers = Array(0 ..< 50)
let result = AnySequence(numbers).lazy
.filter {
// gets called 1x per element :)
print("Calling filter for: \($0)")
return $0 % 3 == 0
}
.dropFirst(5) // neither of these will do indexing,
.prefix(5) // but instead return a lazily evaluated AnySequence.
print(Array(result)) // [15, 18, 21, 24, 27]