如果在游乐场中运行以下代码,您会注意到在修改字典中包含的结构时,副本访问会生成副本,但随后看起来字典中的原始值将被副本替换.我不明白为什么.究竟发生了什么?
还有,有办法避免复制吗?根据这本书的作者,没有,但我只是想确定.
import Foundation class Buffer { let id = UUID() var value = 0 func copy() -> Buffer { let new = Buffer() new.value = self.value return new } } struct COWStruct { var buffer = Buffer() init() { print("Creating \(buffer.id)") } mutating func change() -> String { if isKnownUniquelyReferenced(&buffer) { buffer.value += 1 return "No copy \(buffer.id)" } else { let newBuffer = buffer.copy() newBuffer.value += 1 buffer = newBuffer return "Copy \(buffer.id)" } } } var array = [COWStruct()] array[0].buffer.value array[0].buffer.id array[0].change() array[0].buffer.value array[0].buffer.id var dict = ["key": COWStruct()] dict["key"]?.buffer.value dict["key"]?.buffer.id dict["key"]?.change() dict["key"]?.buffer.value dict["key"]?.buffer.id // If the above `change()` was made on a copy, why has the original value changed ? // Did the copied & modified struct replace the original struct in the dictionary ?
dict["key"]?.change() // Copy
在语义上等同于:
if var value = dict["key"] { value.change() // Copy dict["key"] = value }
将值从字典中拉出,打开成一个临时的,变异的,然后放回字典中.
因为现在有两个对底层缓冲区的引用(一个来自我们的本地临时值,一个来自字典本身的COWStruct实例) – 我们正在强制使用底层Buffer实例的副本,因为它不再是唯一引用的.
那么,为什么不呢
array[0].change() // No Copy
做同样的事?当然应该将元素从数组中拉出来,进行变异然后重新插入,替换之前的值?
不同之处在于,与包含getter和setter的Dictionary的下标不同,Array的subscript comprises of a getter and a special accessor名为mutableAddressWithPinnedNativeOwner.
这个特殊访问器的作用是返回一个指向数组底层缓冲区中元素的指针,以及一个所有者对象,以确保缓冲区不会从调用者下面释放.这种访问器称为寻址器,因为它处理地址.
因此当你说:
array[0].change()
你实际上是在直接改变数组中的实际元素,而不是临时的.
这样的寻址器不能直接应用于Dictionary的下标,因为它返回一个Optional,而底层值不存储为可选的.所以它目前必须用临时解包,因为我们不能返回指向存储中的值的指针.
在Swift 3中,您可以通过在更改临时变量之前从字典中删除值来避免复制COWStruct的底层Buffer:
if var value = dict["key"] { dict["key"] = nil value.change() // No Copy dict["key"] = value }
现在只有临时具有对底层Buffer实例的视图.
并且,在评论中作为@dfri points out,这可以减少到:
if var value = dict.removeValue(forKey: "key") { value.change() // No Copy dict["key"] = value }
节省散列操作.
此外,为方便起见,您可能需要考虑将其转换为扩展方法:
extension Dictionary { mutating func withValue<R>( forKey key: Key, mutations: (inout Value) throws -> R ) rethrows -> R? { guard var value = removeValue(forKey: key) else { return nil } defer { updateValue(value, forKey: key) } return try mutations(&value) } } // ... dict.withValue(forKey: "key") { $0.change() // No copy }
在Swift 4中,您应该能够使用Dictionary的values
属性来执行值的直接变换:
if let index = dict.index(forKey: "key") { dict.values[index].change() }
由于values属性现在返回一个带有地址的特殊Dictionary.Values
可变集合that has a subscript(有关此更改的更多信息,请参阅SE-0154).
但是,目前(使用Xcode 9 beta 5附带的Swift 4版本),这仍然是一个副本.这是因为Dictionary和Dictionary.Values实例都有一个到底层缓冲区的视图 – 作为值计算属性is just implemented,带有一个getter和setter,它绕过对字典缓冲区的引用.
因此,在调用地址符时,会触发字典缓冲区的副本,从而导致在COWStruct的Buffer实例上有两个视图,因此在调用change()时会触发它的副本.
我有filed a bug over this here.(编辑:这个has now been fixed在master上使用协同程序非正式介绍了通用访问器,因此将在Swift 5中修复 – 有关详细信息,请参见下文).
在Swift 4.1,Dictionary的下标(_:default