Swift
WeakReference

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

More than 1 year has passed since last update.


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 に対応させます。

private class ZWeakObject<T: AnyObject>: Equatable, Hashable {

weak var object: T?

init(_ object: T) {
self.object = object
}

var hashValue: Int {
if let object = self.object { return unsafeAddressOf(object).hashValue }
else { return 0 }
}
}

private func == <T> (lhs: ZWeakObject<T>, rhs: ZWeakObject<T>) -> Bool {
return lhs.object === rhs.object
}

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

public class ZWeakObjectSet<T: AnyObject>: SequenceType {

private var _objects: Set<ZWeakObject<T>>

var objects: [T] {
return _objects.flatMap { $0.object }
}

public init() {
self._objects = Set<ZWeakObject<T>>([])
}

public init(_ objects: [T]) {
self._objects = Set<ZWeakObject<T>>(objects.map { ZWeakObject($0) })
}

public var allObjects: [T] {
return _objects.flatMap { $0.object }
}

public func contains(object: T) -> Bool {
return self._objects.contains(ZWeakObject(object))
}

public func addObject(object: T) {
self._objects.unionInPlace([ZWeakObject(object)])
}

public func addObjects(objects: [T]) {
self._objects.unionInPlace(objects.map { ZWeakObject($0) })
}

public func removeObject(object: T) {
self._objects.remove(ZWeakObject<T>(object))
}

public func removeObjects(objects: [T]) {
for object in objects {
self._objects.remove(ZWeakObject<T>(object))
}
}

public func generate() -> AnyGenerator<T> {
let objects = self.allObjects
var index = 0
return AnyGenerator {
defer { index += 1 }
return index < objects.count ? objects[index] : nil
}
}
}

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