「読み取り専用」は汎化ではない

InfoQ: .NETのリードオンリー コレクションインターフェースの物語

読み取り専用コレクションが.NET4.5で追加されると言う話。

記事は「ようやくかよ!」という感じだが、.NETチームのこれまでの判断は妥当だと思う。

面白いのが、IList<T>は、IReadOnlyList<T>から継承していないことである。全く同じメンバーを持ち、全リストがリードオンリーリストとして表現できるにもかかわらずである。

OOにありがちなミスだが、これは実装継承の最悪なパターンである。確かに、振る舞いはサブセットかも知れないが、契約はサブセットになっていない。つまり、IReadOnlyList の契約は「読み取り専用のリスト」だが、これを派生クラスで「読み取り可能」にすることは基本クラスの契約を派生クラスが覆している。これは避けるべき設計であり、一般にLiskovの置換原則と呼ばれているものを破っている。

この原則を破る最近の例は、Objective-CNSMutableString だろうと思う。NSMutableString は NSString から派生しており、派生クラスで「変更可能」にしている。これが問題になるのは、たとえばあるAPI実装で NSString を引数として受け取ったとき、それが後になって変更されている可能性が否定できないことだ。そのため、もし変更が問題を引き起こすなら、文字列のコピーを作っておかなくてはならない。これは著しい無駄になる。別解として、いちいち型チェックをすることもできる。こちらは計算量が無駄だし、手間が掛かる。現実的にもっとも起こりえるケースは、単にそういう潜在的なバグを埋め込んでしまい、とてつもなく検出が難しくなることだ。

読み取り専用に関するものでは、他に C++const 修飾なんかの例もある。気持ちは分かるがOOとあまり相性が良くなく、正しい機能しているとは言いがたい。

言い換えると、リードオンリーインターフェースをミュータブルな型のベースクラスとして導入する唯一のチャンスは、元々考えついた.NET 2.0の時だった。

記事ではこう書かれているが、それはチャンスではなくトラップだったろう。.NETが正しい判断をしたことを歓迎したい。