不调用 PropertyWrapper 下标。为什么?

PropertyWrapper subscript is not called. WHY?

我正在实现自己的 AtomicDictionary 属性 包装器,如下所示:

@propertyWrapper
public class AtomicDictionary<Key: Hashable, Value>: CustomDebugStringConvertible {
  public var wrappedValue = [Key: Value]()

  private let queue = DispatchQueue(label: "atomicDictionary.\(UUID().uuidString)",
                                    attributes: .concurrent)

  public init() {}

  public subscript(key: Key) -> Value? {
    get {
      queue.sync {
        wrappedValue[key]
      }
    }

    set {
      queue.async(flags: .barrier) { [weak self] in
        self?.wrappedValue[key] = newValue
      }
    }
  }

  public var debugDescription: String {
    return wrappedValue.debugDescription
  }
}

现在,当我使用它时如下:

class ViewController: UIViewController {
  @AtomicDictionary var a: [String: Int]

  override func viewDidLoad() {
    super.viewDidLoad()
    self.a["key"] = 5
  }
}

AtomicDicationary的下标函数没有被调用!!

有人能解释这是为什么吗?

Property wrappers 只是为基本访问器方法提供一个接口,仅此而已。它不会拦截下标或其他方法。

最初的 属性 包装提案 SE-0258 向我们展示了幕后发生的事情。它考虑了一个假设的 属性 包装器 Lazy,其中:

The property declaration

@Lazy var foo = 1738

translates to:

private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
    get { return _foo.wrappedValue }
    set { _foo.wrappedValue = newValue }
}

请注意 foo 只是一个 Int 计算出来的 属性。 _fooLazy<Int>.

因此,在您的 a["key"] = 5 示例中,它不会使用您的 属性 包装器的下标运算符。它将 geta 关联的值,使用字典自己的下标运算符来更新该值(不是 属性 包装器的下标运算符),然后它将 seta.

关联的值

这就是 属性 包装器所做的全部工作,提供 getset 访问器。例如,声明:

@AtomicDictionary var a: [String: Int]

转换为:

private var _a: AtomicDictionary<String, Int> = AtomicDictionary<String, Int>(wrappedValue: [:])
var a: [String: Int] {
    get { return _a.wrappedValue }
    set { _a.wrappedValue = newValue }
}

在此示例中,您定义的任何其他方法只能通过 _a 访问,而不是 a(这只是一个计算的 属性,用于获取和设置 wrappedValue _a).


所以,你最好只为你的“原子字典”定义一个合适的类型:

public class AtomicDictionary<Key: Hashable, Value> {
    private var wrappedValue: [Key: Value]
    
    private let queue = DispatchQueue(label: "atomicDictionary.\(UUID().uuidString)", attributes: .concurrent)
    
    init(_ wrappedValue: [Key: Value] = [:]) {
        self.wrappedValue = wrappedValue
    }
    
    public subscript(key: Key) -> Value? {
        get {
            queue.sync {
                wrappedValue[key]
            }
        }
        
        set {
            queue.async(flags: .barrier) {
                self.wrappedValue[key] = newValue
            }
        }
    }
}

let a = AtomicDictionary<String, Int>()

这给了你想要的行为。


如果您要提供 CustomDebugStringConvertible 一致性,请确保也在那里使用您的同步机制:

extension AtomicDictionary: CustomDebugStringConvertible {
    public var debugDescription: String {
        queue.sync { wrappedValue.debugDescription }
    }
}

所有与包装值的交互都必须同步。


显然,您可以将此通用模式与您想要的任何同步机制一起使用,例如,上面的 reader-writer 模式、GCD 串行队列、锁、参与者等。(reader-writer 模式有一个自然的吸引力,但在实践中,通常有更好的机制。)


不用说了,以上假设subscript-level原子性就足够了。人们应该始终警惕通用 thread-safe 集合,因为我们代码的正确性通常依赖于 higher-level 同步。