我目前正在从objc.io 阅读出色的 Advanced Swift 书籍,并且遇到了一些我不理解的问题。
如果在操场上运行以下代码,您会注意到,修改词典中包含的结构时,下标访问权限会创建一个副本,但是看起来词典中的原始值已被副本替换。我不明白为什么。到底是什么情况?
另外,有没有办法避免复制?据这本书的作者说,没有,但我只是想确定一下。
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 }
将该值从字典中拉出,展开为临时变量,进行突变,然后放回字典中。
因为现在有 两个 对基础缓冲区的引用(一个来自我们的本地临时引用value,一个来自COWStruct字典本身的实例)–我们将强制执行一个基础Buffer实例的副本,因为它不再被唯一引用。
value
COWStruct
Buffer
所以,为什么不
array[0].change() // No Copy
做同样的事情?当然应该将元素从数组中拉出,进行突变然后重新插入,以替换先前的值吗?
区别在于,与Dictionary下标包含getter和setter Array的下标不同,下标包含getter和称为的特殊访问器mutableAddressWithPinnedNativeOwner。
Dictionary
Array
mutableAddressWithPinnedNativeOwner
这个特殊的访问器所做的是返回一个 指向 数组基础缓冲区中的元素的 指针 ,以及一个所有者对象,以确保不会从调用者下方释放该缓冲区。这样的访问器称为 地址器 ,因为它处理地址。
因此,当您说:
array[0].change()
您实际上是在 直接 改变数组中的实际元素,而不是临时的。
此类地址不能直接应用于Dictionary的下标,因为它返回Optional,并且基础值未存储为可选值。因此,由于我们无法返回指向存储中值的指针,因此当前必须使用临时包对其进行解包。
Optional
在Swift 3中,可以通过在突变临时变量之前从字典中删除值来避免复制您COWStruct的基础Buffer:
if var value = dict["key"] { dict["key"] = nil value.change() // No Copy dict["key"] = value }
现在, 只有 临时人员可以查看基础Buffer实例。
而且,正如@dfri在评论中指出的那样,可以将其简化为:
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中,您 应该 能够使用values属性Dictionary来执行值的直接突变:
values
if let index = dict.index(forKey: "key") { dict.values[index].change() }
As the values property now returns a special Dictionary.Values mutable collection that has a subscript with an addressor (see SE-0154 for more info on this change).
Dictionary.Values
However, currently (with the version of Swift 4 that ships with Xcode 9 beta 5), this still makes a copy. This is due to the fact that both the Dictionary and Dictionary.Values instances have a view onto the underlying buffer – as the values computed property is just implemented with a getter and setter that passes around a reference to the dictionary’s buffer.
So when calling the addressor, a copy of the dictionary’s buffer is triggered, therefore leading to two views onto COWStruct‘s Buffer instance, therefore triggering a copy of it upon change() being called.
change()
I have filed a bug over this here. ( Edit: This has now been fixed on master with the unofficial introduction of generalised accessors using coroutines, so will be fixed in Swift 5 – see below for more info).
In Swift 4.1, Dictionary‘s subscript(_:default:) now uses an addressor, so we can efficiently mutate values so long as we supply a default value to use in the mutation.
subscript(_:default:)
For example:
dict["key", default: COWStruct()].change() // No copy
The default: parameter uses @autoclosure such that the default value isn’t evaluated if it isn’t needed (such as in this case where we know there’s a value for the key).
default:
@autoclosure
With the unofficial introduction of generalised accessors in Swift 5, two new underscored accessors have been introduced, _read and _modify which use coroutines in order to yield a value back to the caller. For _modify, this can be an arbitrary mutable expression.
_read
_modify
The use of coroutines is exciting because it means that a _modify accessor can now perform logic both before and after the mutation. This allows them to be much more efficient when it comes to copy-on-write types, as they can for example deinitialise the value in storage while yielding a temporary mutable copy of the value that’s uniquely referenced to the caller (and then reinitialising the value in storage upon control returning to the callee).
标准库已更新,很多以前低效的API来使用新的_modify访问- 这包括Dictionary的subscript(_:)现在可以产生独特的参考价值主叫(使用deinitialisation招我上面提到的)。
subscript(_:)
这些变化的结果意味着:
dict["key"]?.change() // No copy
无需在Swift 5中进行复制就可以执行值的突变(您甚至可以使用主快照亲自尝试一下)。