ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C# 雑記) Linq の Count() と IReadOnlyCollection<T>

LinqCount()、つまりint Enumerable.Count(IEnumerable<T>)は原理的には全列挙による数え上げだが、配列ライクなものは初めから要素数を持っているのでO(N)を回す必要はない。そのため、配列ライクなものについては直接要素数を取るよう最適化が施されている。

それで、ここで言う「配列ライク」なものというのはICollectionICollection<T>。(.NET Core の場合はもう一つIIListProvider<T>も入っているが、こちらはinternalなのでユーザーにはあまり関係ない)

このあたりの継承関係は、ジェネリック版は

IList<T>ICollection<T>IEnumerable<out T>IEnumerable

ジェネリック

IListICollectionIEnumerable

の継承関係にある。Countを持っているのはICollection<T>ICollection

これとは別に、.NET Framework 4.5 の時点で後から追加されたインターフェースにIReadOnlyList<out T>IReadOnlyCollection<out T>があり、それぞれ

IReadOnlyList<out T>T this[int index] { get; }

IReadOnlyCollection<out T>int Count { get; }

のみを提供している。継承関係は

IReadOnlyList<out T>IReadOnlyCollection<out T>IEnumerable<out T>IEnumerable

になっている。

ここで、IReadOnlyCollection<out T>Countを持っているにもかかわらず、ICollectionICollection<T>のどちらでもないため、LinqCount()の最適化にヒットしない。

では、なぜIReadOnlyCollection<out T>に最適化が入っていないかだが、おそらくis演算子のコストがタダではないからだと思われる。(あくまで個人的推測) IReadOnlyCollection<out T>を継承しているクラスは高確率でICollection<T>を継承しているはずなので、そこでヒットするだろうという考えな気がする。

でも、ICollection<T>ICollectionは余計なメソッドがいっぱいある上、setter を提供してしまうのであまり ReadOnly なクラスにつけたくない。もちろん、いらないものは明示的実装で隠蔽しつつ、インターフェース経由で呼ばれたらNotSupportedExceptionでも投げればいいのだけど、Linq の内部実装の最適化ヒット読みでわざわざそんなことしないといけないなら、なぜIReadOnlyCollection<out T>なんていうインタフェースをわざわざ作ったのか……と思わなくもない。特に、本来静的エラーにできるものを実行時エラーで解決するのは何のための静的型付けだ!!と思ってしまう。

最適化が入ってないのは何か理由があるんですかね?