一尘不染

关于字典访问的Swift语义

swift

我目前正在从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 ?

阅读 231

收藏
2020-07-07

共1个答案

一尘不染

dict[“key”]?.change() // Copy

在语义上等效于:

if var value = dict["key"] {
    value.change() // Copy
    dict["key"] = value
}

将该值从字典中拉出,展开为临时变量,进行突变,然后放回字典中。

因为现在有 两个
对基础缓冲区的引用(一个来自我们的本地临时引用value,一个来自COWStruct字典本身的实例)–我们将强制执行一个基础Buffer实例的副本,因为它不再被唯一引用。

所以,为什么不

array[0].change() // No Copy

做同样的事情?当然应该将元素从数组中拉出,进行突变然后重新插入,以替换先前的值吗?

区别在于,与Dictionary下标包含getter和setter
Array下标不同下标包含getter和称为的特殊访问器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在评论中指出的那样,可以将其简化为:

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来执行值的直接突变:

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).

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.

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.

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).


Swift 5 and beyond

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.

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访问-
这包括Dictionarysubscript(_:)现在可以产生独特的参考价值主叫(使用deinitialisation招我上面提到的)。

这些变化的结果意味着:

dict["key"]?.change() // No copy

无需在Swift
5中进行复制就可以执行值的突变(您甚至可以使用主快照亲自尝试一下)。

2020-07-07