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 を宣言します。コレクションとして扱えるように、Equatable
、 Hashable
に対応させます。
// 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
では、テストの為のコードを書いてみましょう。String
は AnyObject
ではないので、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