简体   繁体   中英

Associated objects and Swift arrays

I have an associated object on a view extension that contains an array (a mutable array in this case).

var queue: NSMutableArray {
    get {
        if let queue = objc_getAssociatedObject(self, &Key.queue) as? NSMutableArray {
            return queue
        } else {
            let queue = NSMutableArray()
            objc_setAssociatedObject(self, &Key.queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return queue
        }
    }
}

I would like to transform NSMutableArray into a Swift array but I cannot figure out how to do. This will prevent me to perform quite ugly casting. Any suggestion?

The NSMutableArray here is a reference type. The consequence of this is that the moment you hand somebody a reference to your view object's queue , your view object loses all control of it. The recipient of the reference can rearrange items, delete all items, etc., and your view wouldn't know about it until next time it tried reading it.

Generally speaking, this sort of implicit data sharing has been found to be a bad idea, because it breaks encapsulation, and complicates systems, because you can no longer reason locally about code, because there's always the threat that another thread has a reference to your aliased object, and is changing it under your feet.

This model isn't compatible with Swift's value-types, such as Array . If queue were an Array<T> , every person who accessed it would get back their own value. Semantically, these copies are all completely isolated from each other, and there's no way in which a mutation done through one reference can cause an effect that's observable via the other reference.

If you wanted to faithfully preserve the reference semantics of your current code, you would need a mechanism to listen for changes to the NSMutableArray , and update every individual Array<T> that was derived from it. This isn't really practical, nor is it a good idea.

Here's what I would do:

  1. Make the interface more explicitly transactional. More than likely, you can hide the queue entirely. Make it private, and have your view expose public methods like push and pop .

     import Foundation class C: NSObject { private enum AssociatedObjectKeys { static var queue: Int8 = 0 } private var queue: Array<Int> { get { guard let existingValue = objc_getAssociatedObject(self, &AssociatedObjectKeys.queue) else { self.queue = [] return [] } guard let existingArray = existingValue as? Array<Int> else { fatalError("Found an associated object that had the wrong type!") } return existingArray } set { objc_setAssociatedObject(self, &AssociatedObjectKeys.queue, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } public func printQueueForDebugging() { print(self.queue) } public func push(_ newValue: Int) { self.queue.append(newValue) } public func pushAll<S: Sequence>(_ newValues: S) where S.Element == Int { self.queue.append(contentsOf: newValues) } public func pop() -> Int? { if self.queue.isEmpty { return nil } return self.queue.removeFirst() } } let c = C() c.printQueueForDebugging() c.pushAll(1...3) c.printQueueForDebugging() c.push(4) c.printQueueForDebugging() print(c.pop() as Any) print(c.pop() as Any) print(c.pop() as Any) print(c.pop() as Any) print(c.pop() as Any)
  2. I would use a type-safe Swift wrapper to tuck away associated object sets/gets, which can do type checking and casting automatically behind the scenes. The new property wrapper feature is perfect for this.

  3. Extract out a separate Queue<T> data structure. Array<T> by itself doesn't make for a good queue, because removing at the start has O(n) time complexity. There are lots of data structure libraries out there, I would use one of their queues instead.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM