2017-06-19 51 views
1

我目前正在读从objc.io优秀高级斯威夫特的书,我运行到的东西,我不明白斯威夫特语义。关于字典访问

如果运行在操场下面的代码,你会发现,修改包含在字典中的结构时,副本由下标访问制造,但随后便出现在字典中的原始值被替换复制。我不明白为什么。究竟发生了什么?

另外,有没有办法避免复制?根据这本书的作者,没有,但我只是想确定。

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 ? 

enter image description here

回答

4
dict["key"]?.change() // Copy 

在语义上等同于:

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

值被拉出的字典,解开到一个临时的,突变的,然后放回字典。

因为现在有一种潜在的缓冲区(一个从本地临时value在字典本身,以及一个从COWStruct实例)引用 - 我们迫使底层Buffer实例的副本,因为它没有更长的唯一引用。

那么,为什么不

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

做同样的事情?当然,元素应该从数组中拉出来,发生变异,然后重新插入,取代之前的值?

的区别在于Array的下标返回一个非可选值。因此,作为优化,而不是具有由吸气剂和二氧化碳吸收剂构成的subscript和称为mutableAddressWithPinnedNativeOwner的特殊吸收剂。

什么这个特殊的存取器确实是在该阵列的基础缓冲器指针返回元件,与所有者对象一起,以确保缓冲器不从呼叫者下释放。

因此,当你说:

array[0].change() 

你实际上是变异的实际元素的数组直接的,而不是暂时的。

相同的特殊访问器不能直接应用于Dictionary的下标,因为如前所述,它返回Optional,并且底层值不作为可选项存储。所以它必须暂时解开。

在斯威夫特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 
} 

节省了哈希运算。


在斯威夫特4,你应该能够使用的Dictionaryvalues财产,以执行价值的直接突变:

if let index = dict.index(forKey: "key") { 
    dict.values[index].change() 
} 

由于values属性现在返回一个特殊的Dictionary.Values可变集合that has a subscript带有mutableAddressWithNativeOwner访问者(有关此更改的更多信息,请参见SE-0154)。

但是,目前(随着与Xcode 9 beta 5一起发行的Swift 4版本),这仍然是一个副本。这是因为DictionaryDictionary.Values实例都可以查看底层缓冲区 - 因为values计算属性is just implemented带有一个传入字典缓冲区的引用的getter和setter。

所以调用mutableAddressWithNativeOwner访问时,字典的缓冲区的副本被触发,从而导致两种观点在COWStructBuffer实例,因此在change()触发它的副本被调用。我有filed a bug over this here

+0

当在Swift 3中突变字典KV对时,(现今)惯用的remove-mutate-replace操作的替代(以及同样常见的)实现是使用['removeValue(forKey:)'](https:/ /developer.apple.com/documentation/swift/dictionary/1641348-removevalue)方法执行联合存在检查和删除操作,而不是单独执行这些操作。例如。 'if var value = dict.removeValue(forKey:“key”){/ * mutate value并重新插入* /}'。一如既往的好答案,有趣的是你对新的Dictionary.Values集合的调查。 – dfri

+0

@dfri啊,是的,非常好的一点!我已经编辑了我的答案:) – Hamish

+0

我刚刚注意到,似乎目前副本和随后的'nil'赋值替代可能会产生(直接的'removeValue(forKey :))调用相比(最小?)开销,作为先前的选择,在明确地复制该值之后[[貌似也调用'removeValue(forKey :)),但放弃了'nil' assigment的结果](https://github.com/apple/swift/blob/master /stdlib/public/core/HashedCollections.swift.gyb#L1982)。但也许这是在发布版本中优化:) – dfri