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

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

<font color="silver">EDIT: コードの一部を更新しました。 2016.3.29</font>

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

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

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

```.swift
weak var delegates: [MyClassDelegate?] // error
```

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

```.swift
var delegates = NSPointerArray.weakObjectsPointerArray()
```

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

# `ZWeakObjectSet` の設計

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

```.swift
// 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 参照としました。

```.swift
// 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対応としました。<font color="silver">2020.10.25</font>
EDIT: `SequenceType` に適合させてみました。<font color="silver">2016.3.25</font>
EDIT: `removeObject()`, `removeObjects()` を追加。イニシャライザを `init(objects: [T])` から `init(_ objects: [T])` に変更。<font color="silver">2016.3.29</font>
EDIT: gist のコードの更新を反映。<font color="silver">2016.6.11</font>

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

```.swift
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 のバージョンは以下の通りです。

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

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

```.console
swift --version
Apple Swift version 5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)
Target: x86_64-apple-darwin19.6.0
```