変種オセロのUI作りは中休み。
今日は、SwiftでSetの型パラメータにプロトコルを指定する方法について説明したいと思う。
事の起こり
事の起こりは、変種オセロのUIでボタンが押された事を通知する仕組みを作ろうとしていたこと。
もちろん、これにはいくつかの実装方法があって、
- フラグを立てておいて、ゲームのメインループの方でチェックする
- クロージャを登録できるようにして、登録されたクロージャを呼び出す
- デリゲートを用意して、デリゲートのメソッドを呼び出す
- オブザーバパターンを使って、登録されたオブザーバに通知をする
といった方法が考えられる。
フラグを立てる方法は、とりあえずパス。
もちろん、通知先のオブジェクト(デリゲート or オブザーバ)でフラグを立てるということは十分に考えられるけど、ボタンの実装から直接フラグを立てるのだと、メインループとべったりになってしまって、切り離すのが難しくなるから。
クロージャを登録する方法も、誰がクロージャを登録するのかといった管理がけっこうややこしい。
デリゲートを用意する方法とオブザーバパターンを使う方法は、登録されたオブジェクト(デリゲート or オブザーバ)のメソッドが呼び出されるという意味で非常に似ているのだけど、登録できるオブジェクトの数に差がある。
デリゲートを用意する場合、デリゲートの数は0個もしくは1個だけど、オブザーバパターンを使う場合、オブザーバの数は何個でもOKになる。
それと、仮に登録されるオブジェクトの数が高々1個だとしても、オブジェクトの登録状況がころころと変わるような場合には、オブザーバパターンを使う方が好ましい。(オブザーバに登録されているかもしれない他のオブジェクト(場合によっては自分自身)について、考える必要がないから)
今回については、登録されるオブジェクトが複数になったり、ころころと変わる可能性が考えられたので、オブザーバパターンを使おうと思ったのだけど、それで見事にハマることに・・・
というのも、オブザーバを管理するためにSetを使おうとしたら、これが思いの外難しかったから。
Setの型パラメータにプロトコルが使えない!?
典型的なオブザーバパターンをSwiftで書こうと思ったら、次のようになると思う。
protocol Observer: class { func notify() } class Some: Observer { func notify() { ... } } class Other: Observer { func notify() { ... } } class Subject { private var observers: Set<Observer> func addObserver(observer: Observer) { self.observers.insert(observer) } func removeObserver(observer: Observer) { self.observers.remove(observer) } func notifyObservers() { for observer in self.observers { observer.notify() } } }
けど、これはエラーになる。
というのも、Setの型はSet<Element: Hashable>となっているので、型パラメータがプロトコルHashableに準拠している必要があるから。
一つの逃げは、Setの代わりにArrayを使う方法。
ただし、その場合、同じオブザーバが複数回登録されてしまうことがないようにする必要がある。
そういったことをしなくて済むようにするためのSetというクラスなのに、それを使えないのは悔しい。。。
ということで、ObserverをHashableに準拠させる必要がある。
protocol Observer: class, Hashable { func notify() } class Some: Observer { func notify() { ... } var hashValue: Int { ... } } class Other: Observer { func notify() { ... } var hashValue: Int { ... } } // 以下略
しかし、これでもまだダメ。
実際にやってみると分かるのだけど、このようなコードを書くと"Protocol 'Observer' can only be used as a generic constraint because it has Self or associated type requirements"っていうエラーが出て、上手くいかない。
なぜじゃ・・・
エラーの原因
エラーの根源的な原因は、プロトコルHashableがプロトコルEquatableを継承していること。
Equatableに準拠するには、二項演算子の==(_: Self, _: Self) -> Bool
を定義しないといけないのだけど、このSelfが曲者。
Selfはプロトコルに準拠したクラス名になるのだけれど、逆にいえば、準拠するクラスが確定しなければ、どのメソッドを呼び出せばいいのか分からないことになる。
Setの場合、オブジェクトの同一性を判定するために、まずはハッシュ値で比較し、それが同じなら==で比較する(はず)。
なので、呼び出す==を確定させるには、型パラメータが具体的なクラスになっていないとダメというわけだ。
・・・って、そうなると、Set<Some>やSet<Other>としないといけないことになるけど、そんなのはハッキリ言って使い物にならない!
ゴミか!
なお、Javaの場合、オブジェクトの同一性を判定するのは、演算子ではなくメソッドのObject#equals(Object)なので、こういった問題は起きない。
回避策
さて、肝心の回避策を。
この問題を回避するには、Dictionaryのキーが重複を許さないという性質と、ObjectIdentifierという構造体を利用する。
具体的には、以下のようにする。
protocol Observer: class { func notify() } class Some: Observer { func notify() { ... } } class Other: Observer { func notify() { ... } } class Subject { private var observers: [ObjectIdentifier:Observer] func addObserver(observer: Observer) { self.observers[ObjectIdentifier(observer)] = observer } func removeObserver(observer: Observer) { self.observers.removeValueForKey(ObjectIdentifier(observer)) } func notifyObservers() { for (_, observer) in self.observers { observer.notify() } } }
ObjectIdentifierという構造体は、いわばオブジェクトのポインタ自身を表すような構造体。
この構造体を作って比較することで、「オブジェクトの内容が等しいか」ではなく「オブジェクトが同じオブジェクトか」を調べることが出来る。
例えば、以下のような感じ。
class Test: Equatable { private var value: Int init(_ value: Int) { self.value = value } } func ==(lhs:Test, rhs: Test) -> Bool { return lhs.value == rhs.value } var a = Test(10) var b = Test(10) var c = a a == b // true b == c // true c == a // true ObjectIdentifier(a) == ObjectIdentifier(b) // false ObjectIdentifier(b) == ObjectIdentifier(c) // false ObjectIdentifier(c) == ObjectIdentifier(a) // true
上のコードで、aもbもcも内容は同じなので、==で比較するとtrueが返ってくる。
けど、ObjectIdentifierを使って比較すると、同じオブジェクトを参照しているaとcだけがtrueになり、他はfalseになる。
(※なお、同じことをやりたいだけだったら、演算子===を使うことも出来る)
そして、このObjectIdentifierはプロトコルHashableに準拠しているので、Dictionaryのキーにすることが出来る。
Dictionaryのキーには==で比較して同じと判断されるオブジェクトは複数登録できないので、ObjectIdentifierをキーとすると「同じ内容と判断されるオブジェクト」ではなく「同じオブジェクト」を複数登録できなくなる。
あとは、Dictionaryの値にオブジェクト自身を入れておけば、コレクション内にオブジェクトの重複のない集合ーーすなわちSetが擬似的に実現されることになる、というカラクリだ。
Setの型パラメータにプロトコルを指定するなんて、よくありそうなことなのに、こんなトリッキーなことをやらないといけないなんて、ちょっと呆れてしまうけどね。
今日はここまで!