swiftsubscriptproperty-wrapper

PropertyWrapper subscript is not called. WHY?


I am implementing my own AtomicDictionary property wrapper as follows:

@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
  }
}

now, when I use it as follows:

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

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

The subscript function of the AtomicDicationary is not called!!

Does anybody have any explanation as to why that is?


Solution

  • Property wrappers merely provide an interface for the basic accessor methods, but that’s it. It’s not going to intercept subscripts or other methods.

    The original property wrapper proposal SE-0258 shows us what is going on behind the scenes. It contemplates a hypothetical property wrapper, Lazy, in which:

    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 }
    }
    

    Note that foo is just an Int computed property. The _foo is the Lazy<Int>.

    So, in your a["key"] = 5 example, it will not use your property wrapper’s subscript operator. It will get the value associated with a, use the dictionary’s own subscript operator to update that value (not the property wrapper’s subscript operator), and then it will set the value associated with a.

    That’s all the property wrapper is doing, providing the get and set accessors. E.g., the declaration:

    @AtomicDictionary var a: [String: Int]
    

    translates to:

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

    Any other methods you define are only accessible through _a in this example, not a (which is just a computed property that gets and sets the wrappedValue of _a).


    So, you’re better off just defining a proper type for your “atomic dictionary”:

    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
                }
            }
        }
    }
    

    And

    let a = AtomicDictionary<String, Int>()
    

    That gives you the behavior you want.


    And if you are going to supply CustomDebugStringConvertible conformance, make sure to use your synchronization mechanism there, too:

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

    All interaction with the wrapped value must be synchronized.


    Obviously you can use this general pattern with whatever synchronization mechanism you want, e.g., the above reader-writer pattern, GCD serial queue, locks, actors, etc. (The reader-writer pattern has a natural appeal, but, in practice, there are generally better mechanisms.)


    Needless to say, the above presumes that subscript-level atomicity is sufficient. One should always be wary about general purpose thread-safe collections as often the correctness of our code relies on a higher-level of synchronization.