LoginSignup
22
20

More than 3 years have passed since last update.

Swift で weak object を コレクションで扱う方法

Last updated at Posted at 2016-03-23

weak 参照オブジェクトのコレクション

Swift でもやっぱり循環参照などを回避するなどの理由で weak なオブジェクトを扱う必要がある場合があります。その代表格が delegate パターンでしょう。

EDIT: コードの一部を更新しました。 2016.3.29

var delegate: MyClassDelegate? // 循環参照の可能性有
weak var delegate: MyClassDelegate? // OK

例えば、この delegate が複数存在しえる可能性を考えてみましょう。または、何らかの理由で weak な object を複数管理する場合を想定してみましょう。まれにですが、このような事をしたい場合があったりします。

当然ながら以下のような定義はできません。

weak var delegates: [MyClassDelegate?] // error

NSPointerArray.weakObjectsPointerArray を使う方法もありますが、Swift の肝とも言える「型」が欠落してしまい、キャストしながら使うのは残念な感じです。

var delegates = NSPointerArray.weakObjectsPointerArray()

そこで、weak な object まとめて管理できる方法を紹介いたします。

ZWeakObjectSet の設計

まずは、object を weak で扱える、wrapper を宣言します。コレクションとして扱えるように、EquatableHashable に対応させます。

// updated: 2020.10.25 - swift 5
fileprivate class ZWeakObject<T: AnyObject>: Equatable, Hashable {
    weak var object: T?
    private let hashKey: Int
    init(_ object: T) {
        self.object = object
        self.hashKey = ObjectIdentifier(object).hashValue
    }
    static func == (lhs: ZWeakObject<T>, rhs: ZWeakObject<T>) -> Bool {
        if lhs.object == nil || rhs.object == nil { return false }
        return lhs.object === rhs.object
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(hashKey)
    }
}

そして、その wrap したオブジェクトをコレクションとして扱う ZWeakObjectSet を宣言します。オブジェクトの順番はあまり重要ではないのではないかと思い、Array ではなく Set ベースにしてみました。このクラスの内部ではオブジェクトは weak 参照で扱われますが、出し入れは strong 参照としました。

// updated: 2020.10.25 - swift 5
public class ZWeakObjectSet<T: AnyObject> {
    private var _objects: Set<ZWeakObject<T>>
    init() {
        self._objects = Set<ZWeakObject<T>>([])
    }
    init(_ objects: [T]) {
        self._objects = Set<ZWeakObject<T>>(objects.map { ZWeakObject($0) })
    }
    var objects: [T] {
        return self._objects.compactMap { $0.object }
    }
    func contains(_ object: T) -> Bool {
        return self._objects.contains(ZWeakObject(object))
    }
    func addObject(_ object: T) {
        self._objects.insert(ZWeakObject(object))
    }
    func addingObject(_ object: T) -> ZWeakObjectSet<T> {
        return ZWeakObjectSet( self.objects + [object])
    }
    func addObjects(_ objects: [T]) {
        self._objects.formUnion(objects.map { ZWeakObject($0) })
    }
    func addingObjects(_ objects: [T]) -> ZWeakObjectSet<T> {
        return ZWeakObjectSet( self.objects + objects)
    }
    func removeObject(_ object: T) {
        self._objects.remove(ZWeakObject(object))
    }
    func removingObject(_ object: T) -> ZWeakObjectSet<T> {
        var temporaryObjects = self._objects
        temporaryObjects.remove(ZWeakObject(object))
        return ZWeakObjectSet(temporaryObjects.compactMap { $0.object })
    }
    func removeObjects(_ objects: [T]) {
        self._objects.subtract(objects.map { ZWeakObject($0) })
    }
    func removingObjects(_ objects: [T]) -> ZWeakObjectSet<T> {
        let temporaryObjects = self._objects.subtracting(objects.map { ZWeakObject($0) })
        return ZWeakObjectSet(temporaryObjects.compactMap { $0.object })
    }
}

EDIT: ZWeakObjectの hash 値は対象のオブジェクトがリリースされているか否かで値が変わってしまうので、初期化時に計算して保持するように変更しました。また、Swift 5対応としました。2020.10.25
EDIT: SequenceType に適合させてみました。2016.3.25
EDIT: removeObject(), removeObjects() を追加。イニシャライザを init(objects: [T]) から init(_ objects: [T]) に変更。2016.3.29
EDIT: gist のコードの更新を反映。2016.6.11

では、テストの為のコードを書いてみましょう。StringAnyObject ではないので、NSString を扱っています。

var alice: NSString? = "Alice"
var bob: NSString? = "Bob"
var cathline: NSString? = "Cathline"

var persons = ZWeakObjectSet<NSString>()
persons.addObject(bob!)
print(persons.allObjects) // [Bob]

persons.addObject(bob!)
print(persons.allObjects) // [Bob]

persons.addObjects([alice!, cathline!])
print(persons.allObjects) // [Alice, Cathline, Bob]

alice = nil
print(persons.allObjects) // [Cathline, Bob]

bob = nil
print(persons.allObjects) // [Cathline]

参照元が nil になった時 ZWeakObjectSet で管理されているオブジェクトも消えている事が確認できます。ちなみに Playground では、期待通りに動作してくれませんのでご注意ください。

Gist

ZWeakObjectSet
https://gist.github.com/codelynx/30d3c42a833321f17d39

まとめ

この記事を書いている最中に NSPointerArray.weakObjectsPointerArray の存在を思い出しました。この weakObjectsPointerArray を Swift の Generics で wrap するのもありかと思いましたが、まぁこれでいい事とします。

執筆時の Swift のバージョンは以下の通りです。

swift --version
Apple Swift version 2.2 (swiftlang-703.0.18.1 clang-703.0.29)
Target: x86_64-apple-macosx10.9

更新時の環境は、次の通りです。

swift --version
Apple Swift version 5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)
Target: x86_64-apple-darwin19.6.0
22
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
20