关于字典访问的Swift语义

dea*_*eef 5 swift

我正在阅读来自objc.io 的优秀高级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 ?
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

Ham*_*ish 13

dict["key"]?.change() // Copy
Run Code Online (Sandbox Code Playgroud)

在语义上等同于:

if var value = dict["key"] {
    value.change() // Copy
    dict["key"] = value
} 
Run Code Online (Sandbox Code Playgroud)

将值从字典中拉出,打开成一个临时的,变异的,然后放回字典中.

因为现在有两个对底层缓冲区的引用(一个来自我们的本地临时缓冲区value,一个来自COWStruct字典本身中的实例) - 我们正在强制执行底层Buffer实例的副本,因为它不再是唯一引用的.

那么,为什么不呢

array[0].change() // No Copy
Run Code Online (Sandbox Code Playgroud)

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

不同之处在于,与Dictionary包含getter和setter Array下标不同,下标包括一个getter和一个名为的特殊访问器mutableAddressWithPinnedNativeOwner.

这个特殊访问器的作用是返回一个指向数组底层缓冲区中元素的指针,以及一个所有者对象,以确保缓冲区不会从调用者下面释放.这样的访问被称为发信人,因为它与地址的交易.

因此当你说:

array[0].change()
Run Code Online (Sandbox Code Playgroud)

你实际上是在直接改变数组中的实际元素,而不是临时的.

这样的寻址器不能直接应用于Dictionary下标,因为它返回一个Optional,而底层值不存储为可选的.所以它当前必须用临时解包,因为我们不能返回指向存储中的值的指针.

在Swift 3中,您可以通过在变更临时变量之前从字典中删除值来避免复制您COWStruct的底层Buffer:

if var value = dict["key"] {
    dict["key"] = nil
    value.change() // No Copy
    dict["key"] = value
}
Run Code Online (Sandbox Code Playgroud)

现在只有临时具有对底层Buffer实例的视图.

而且,正如@dfri在评论中指出的那样,这可以减少到:

if var value = dict.removeValue(forKey: "key") {
    value.change() // No Copy
    dict["key"] = value
}
Run Code Online (Sandbox Code Playgroud)

节省散列操作.

此外,为方便起见,您可能需要考虑将其转换为扩展方法:

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
}
Run Code Online (Sandbox Code Playgroud)

在Swift 4中,您应该能够使用values属性Dictionary来执行值的直接变换:

if let index = dict.index(forKey: "key") {
    dict.values[index].change()
}
Run Code Online (Sandbox Code Playgroud)

由于该values属性现在返回一个特殊的Dictionary.Values可变集合,该集合具有带有地址的下标(有关此更改的更多信息,请参阅SE-0154).

但是,目前(使用Xcode 9 beta 5附带的Swift 4版本),这仍然是一个副本.这是由于这样的事实,无论是DictionaryDictionary.Values实例具有的视图到下面的缓冲-作为values计算的属性只是实现与周围到字典的缓冲器的引用通过的获取和设置.

因此调用发信人时,字典的缓冲区的副本被触发,从而导致两种观点上COWStructBuffer情况下,因此在触发它的副本change()被调用.

在这里提出了一个错误.(编辑:已得到修复在师傅与非官方引进使用协程广义存取,因此将其固定在斯威夫特5 -请参阅下面的更多信息).


在雨燕4.1,Dictionarysubscript(_:default:) 现在使用的发信人,所以我们可以有效地,只要我们提供的突变使用默认值变异值.

例如:

dict["key", default: COWStruct()].change() // No copy
Run Code Online (Sandbox Code Playgroud)

default:参数使用@autoclosure如果不需要则不评估默认值(例如在这种情况下我们知道密钥的值).


斯威夫特5及以后

非官方引入广义存取在夫特5中,两个新下划线存取已被引入,_read并且_modify其中,为了产生一个值返回给调用者使用协程.因为_modify,这可以是任意可变的表达.

协同程序的使用令人兴奋,因为它意味着_modify访问者现在可以在突变之前和之后执行逻辑.这使得它们在写入时复制类型时更加高效,因为它们可以例如对存储中的值进行去初始化,同时产生唯一引用给调用者的值的临时可变副本(然后重新初始化值)在控制中存储时返回被叫者).

标准库已更新,很多以前低效的API来使用新的_modify访问- 这包括Dictionarysubscript(_:)现在可以产生独特的参考价值主叫(使用deinitialisation招我上面提到的).

这些变化的结果意味着:

dict["key"]?.change() // No copy
Run Code Online (Sandbox Code Playgroud)

将能够执行值的变化,而无需在Swift 5中进行复制(您甚至可以使用主快照自行尝试).