先日、Unityを触っていて次のようなことがしたくなりました。
interfaceIItemInfo{...}classItemInfo:IItemInfo{...}classTestClass{privateReactiveCollection<ItemInfo>m_rxCollection=newReactiveCollection<ItemInfo>();publicIReadOnlyReactiveCollection<IItemInfo>ObservableCollection=>m_rxCollection;}
あるクラスのインスタンスが格納されたReactiveCollectionをReadOnlyにして公開したいのですが、さらに中身もクラスを直接渡すのではなく、インターフェイスとして公開したいというわけです。
でも、普通にやると怒られます。
しかし、例えばListをIReadOnlyListにして公開する場合は怒られません。
これ、なぜかというとIReadOnlyListインターフェイスにはジェネリクスの共変性が認められているからです。
共変性については、他のサイトで詳しく解説されていますので割愛しますが、要は型引数にもポリモーフィズムを適用させられる機能で、型引数にout修飾子をつければ共変性が使えます。
out修飾子は、ジェネリクス引数で指定した型のオブジェクトが変更されないことを保証するので、不変であれば型引数も安全に型変換が可能であるということですね。
詳しい解説は下記サイトが非常に分かりやすくおすすめです。
https://ufcpp.net/study/csharp/sp4_variance.html
で、IReadOnlyListインターフェイスの定義を見てみると、型引数にout修飾子がついています。
対して、IReadOnlyReactiveCollectionインターフェイスの定義を見ると、型引数にout修飾子がついていません。
と、いうわけで、IReadOnlyReactiveCollectionインターフェイスにも共変性を持たせれば解決するということになります。
IReadOnlyReactiveCollectionに共変性を持たせる
IReadOnlyReactiveCollectionのインターフェイスの型引数にout修飾子をつけ、共変性を持たせましょう。
しかし、何も考えずout修飾子をつけると怒られます。
なぜかというと、IReadOnlyReactiveCollectionインターフェイスのメンバが、Tが不変であると保証していないからです。
例えば、CollectionAddEventは、TのSetterをもつので、値が書き換えられる可能性があります。
したがって、Tは不変ではないので、エラーが発生しています。
Tが不変なIReadOnlyCollectionAddEventインターフェイスを定義する
となれば、Tが不変であることを保証するCollectionAddEventのインターフェイスIReadOnlyCollectionAddEventを定義しましょう。
publicinterfaceIReadOnlyCollectionAddEvent<outT>{intIndex{get;}TValue{get;}}
TのSetterを削除しました。もともとprivate setであったので問題ないでしょう。
こうすることで、IReadOnlyCollectionAddEventインターフェイスを通じて行った操作ではTが不変であることが保証されるので、晴れて共変性が使えるようになります。
IReadOnlyReactiveCollectionOutインターフェイスを定義する
IReadOnlyReactiveCollectionインターフェイスを、先ほど定義したIReadOnlyCollectionAddEventインターフェイスを使って直接書き換えてはいけません。
インターフェイスのメンバを変更すると、そのインターフェイスを実装したすべてのクラスに影響があるからです。
したがって、共変性が使える特別なIReadOnlyReactiveCollectionOutインターフェイスを新しく定義しましょう。
publicinterfaceIReadOnlyReactiveCollectionOut<outT>:IEnumerable<T>{IObservable<IReadOnlyCollectionAddEvent<T>>ObserveAdd();IObservable<IReadOnlyCollectionRemoveEvent<T>>ObserveRemove();}
今回は、ObserveAdd()とObserveRemove()しか使用予定がなかったので、とりあえずこの2つを定義しました。
他のメンバも使用する場合は適宜追加してください。
で、この新しいIReadOnlyReactiveCollectionOutインターフェイスを、ReactiveCollectionクラスに実装します。
僕は次のように実装しました。普通のObserveAdd()の戻り値をインターフェイスに変換しているだけです。
IObservable<IReadOnlyCollectionAddEvent<T>>IReadOnlyReactiveCollectionOut<T>.ObserveAdd()=>ObserveAdd().Select(d=>(IReadOnlyCollectionAddEvent<T>)d);IObservable<IReadOnlyCollectionRemoveEvent<T>>IReadOnlyReactiveCollectionOut<T>.ObserveRemove()=>ObserveRemove().Select(d=>(IReadOnlyCollectionRemoveEvent<T>)d);
少し長くなってしまいましたが、以上で当初の目的であった「ReactiveCollectionをReadOnlyかつ中身も直接ではなくインターフェイスで公開する」ことができます!
使ってみる
問題となったコード
ReactiveCollectionをReadOnlyかつ中身もインターフェイス公開にしたいのにできません。
IReadOnlyReactiveCollectionインターフェイスがTの不変性を保証していないからです。
改善したコード
対して、対策したIReadOnlyReactiveCollectionOutインターフェイスで公開すると、
エラーが出なくなっています!
IReadOnlyReactiveCollectionOutインターフェイスが、Tの不変性を保証しているからですね。
最後に
ReactiveCollectionをReadOnlyにして公開することは多々あると思いますが、今回はどうしても中身のクラスもインターフェイスにして公開したかったので無理やり対策してみました。
UniRx標準で対応されていると嬉しいのですが…。
今のところ問題ありませんが、今後不都合など出てきたら記事書こうと思います。