我完全同意亚历山大的观点。如果您存储惰性集合,那么您通常会做错一些事情,并且重复访问的成本会不断让您感到惊讶。
这些集合已经超出了它们的复杂性要求,这是真的 https://developer.apple.com/documentation/swift/lazydropwhilecollection:
注意:访问 startIndex、first 或任何依赖于 startIndex 的方法的性能取决于有多少元素满足集合开头的谓词,并且可能无法提供 Collection 协议给出的通常性能。因此,请注意,LazyDropWhileCollection 实例上的常规操作可能没有记录的复杂性。
但缓存并不能解决这个问题。第一次访问时它们仍然是 O(n),所以像这样的循环
for i in 0..<xs.count { print(xs[i]) }
仍然是 O(n^2)。还要记住 O(1) 和“快”不是一回事。感觉就像你试图达到“快速”,但这并不能解决复杂性承诺(也就是说,惰性结构已经打破了 Swift 中的复杂性承诺)。
缓存是一个净负面因素,因为它使惰性数据结构的正常(和预期)使用速度变慢。使用惰性数据结构的正常方法是消耗它们零次或一次。如果您要多次使用它们,则应该使用严格的数据结构。缓存你从不使用的东西是浪费时间and space.
当然,在某些可以想象的用例中,您有一个大型数据结构,将被稀疏地多次访问,因此缓存会很有用,但这不是用例lazy
是为了处理而建造的。
尝试使用缓存来优化结构上的惰性集合是不可能的,因为 subscript(_position:) 以及您需要实现以符合 LazyProtocolCollection 的所有其他方法都是不可变的,并且默认情况下结构是不可变的。这意味着我们必须为每次调用属性或方法重新计算所有操作。
这不是真的。结构体可以在内部存储引用类型来保存其缓存,这很常见。字符串正是这样做的。它们包括一个StringBuffer
这是一个引用类型(由于与 Swift 编译器错误相关的原因,StringBuffer
实际上是作为包装类的结构实现的,但从概念上讲它是引用类型)。 Swift 中的许多值类型都以这种方式存储内部缓冲区类,这使得它们在呈现不可变接口的同时可以在内部可变。 (这对于 CoW 以及许多其他性能和内存相关的原因也很重要。)
请注意,今天添加缓存也会破坏现有的用例lazy
:
struct Massive {
let id: Int
// Lots of data, but rarely needed.
}
// We have lots of items that we look at occassionally
let ids = 0..<10_000_000
// `massives` is lazy. When we ask for something it creates it, but when we're
// done with it, it's thrown away. If `lazy` forced caching, then everything
// we accessed would be forever. Also, if the values in `Massive` change over
// time, I certainly may want it to be rebuilt at this point and not cached.
let massives = ids.lazy.map(Massive.init)
let aMassive = massives[10]
这并不是说缓存数据结构在某些情况下没有用处,但它肯定并不总是有利的。它会带来大量成本,并在帮助其他用途的同时破坏一些用途。因此,如果您想要这些其他用例,您应该构建一个提供它们的数据结构。但合理的是lazy
不是那个工具。