关于字典访问的 Swift 语义 [英] Swift semantics regarding dictionary access

查看:24
本文介绍了关于字典访问的 Swift 语义的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在阅读 objc.io 的优秀Advanced Swift 书,但遇到了一些我不明白的问题.

I'm currently reading the excellent Advanced Swift book from objc.io, and I'm running into something that I don't understand.

如果您在操场上运行以下代码,您会注意到,当修改字典中包含的结构时,下标访问会生成一个副本,但随后似乎字典中的原始值被副本替换了.我不明白为什么.到底发生了什么?

If you run the following code in a playground, you will notice that when modifying a struct contained in a dictionary a copy is made by the subscript access, but then it appears that the original value in the dictionary is replaced by the copy. I don't understand why. What exactly is happening ?

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

Also, is there a way to avoid the copy ? According to the author of the book, there isn't, but I just want to be sure.

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
} 

将值从字典中拉出,解包成一个临时的,经过变异,然后放回字典中.

The value is pulled out of the dictionary, unwrapped into a temporary, mutated, and then placed back into the dictionary.

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

Because there's now two references to the underlying buffer (one from our local temporary value, and one from the COWStruct instance in the dictionary itself) – we're forcing a copy of the underlying Buffer instance, as it's no longer uniquely referenced.

所以,为什么不

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

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

do the same thing? Surely the element should be pulled out of the array, mutated and then stuck back in, replacing the previous value?

不同的是,与Dictionary的下标由getter和setter组成,Array下标由一个 getter 和一个特殊的访问器组成,称为 mutableNcodeOwerativeWith.

The difference is that unlike Dictionary's subscript which comprises of a getter and setter, Array's subscript comprises of a getter and a special accessor called mutableAddressWithPinnedNativeOwner.

这个特殊访问器的作用是返回一个指针到数组底层缓冲区中的元素,以及一个所有者对象,以确保缓冲区不会从调用者下释放.这样的访问器称为 addressor,因为它处理地址.

What this special accessor does is return a pointer to the element in the array's underlying buffer, along with an owner object to ensure that the buffer isn't deallocated from under the caller. Such an accessor is called an addressor, as it deals with addresses.

因此当你说:

array[0].change()

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

you're actually mutating the actual element in the array directly, rather than a temporary.

这样的寻址器不能直接应用于Dictionary 的下标,因为它返回一个Optional,并且底层值不是作为可选值存储的.因此,它目前必须使用临时文件进行解包,因为我们无法返回指向存储中值的指针.

Such an addressor cannot be directly applied to Dictionary's subscript because it returns an Optional, and the underlying value isn't stored as an optional. So it currently has to be unwrapped with a temporary, as we cannot return a pointer to the value in storage.

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

In Swift 3, you can avoid copying your COWStruct's underlying Buffer by removing the value from the dictionary before mutating the temporary:

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

目前只有临时对象可以查看底层Buffer实例.

As now only the temporary has a view onto the underlying Buffer instance.

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

And, as @dfri points out in the comments, this can be reduced down to:

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

节省散列操作.

另外,为方便起见,您可能需要考虑将其变成扩展方法:

Additionally, for convenience, you may want to consider making this into an extension method:

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


In Swift 4, you should be able to use the values property of Dictionary in order to perform a direct mutation of the value:

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

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

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

然而,目前(使用 Xcode 9 beta 5 附带的 Swift 4 版本),这仍然是一个副本.这是因为 DictionaryDictionary.Values 实例都可以查看底层缓冲区——作为 values 计算属性 刚刚使用getter实现和传递对字典缓冲区的引用的 setter.

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.

因此,在调用寻址器时,会触发字典缓冲区的副本,从而导致对 COWStructBuffer 实例的两个视图,从而触发它的副本在 change() 被调用时.

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.

在这里提交了一个错误.(这个现在已经在 master 上修复使用协程的通用访问器的非官方介绍,因此将在 Swift 5 中修复 - 有关更多信息,请参见下文).

I have filed a bug over this here. ( 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).

在 Swift 4.1 中,Dictionarysubscript(_:default:) 现在使用寻址器,因此我们可以有效地改变值,只要我们提供一个用于变异的默认值.

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.

例如:

dict["key", default: COWStruct()].change() // No copy

default: 参数使用 @autoclosure 以便在不需要时不评估默认值(例如在这种情况下,我们知道有一个键的值).

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 中引入了两个新的下划线访问器,_read_modify 使用协程将值返回给调用者.对于 _modify,这可以是任意可变表达式.

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.

协程的使用令人兴奋,因为这意味着 _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 访问器 – 这包括 Dictionarysubscript(_:) 现在可以产生调用者唯一引用的值(使用我上面提到的去初始化技巧).

The standard library has already updated many previously inefficient APIs to make use of the new _modify accessor – this includes Dictionary's subscript(_:) which can now yield a uniquely referenced value to the caller (using the deinitialisation trick I mentioned above).

这些变化的结果意味着:

The upshot of these changes means that:

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

无需在 Swift 5 中复制即可执行值的更改(您甚至可以自己尝试一下 带有主快照).

will be able to perform an mutation of the value without having to make a copy in Swift 5 (you can even try this out for yourself with a master snapshot).

这篇关于关于字典访问的 Swift 语义的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆