Linq のCount()
、つまりint Enumerable.Count(IEnumerable<T>)
は原理的には全列挙による数え上げだが、配列ライクなものは初めから要素数を持っているのでO(N)
を回す必要はない。そのため、配列ライクなものについては直接要素数を取るよう最適化が施されている。
それで、ここで言う「配列ライク」なものというのはICollection
とICollection<T>
。(.NET Core の場合はもう一つIIListProvider<T>
も入っているが、こちらはinternal
なのでユーザーにはあまり関係ない)
このあたりの継承関係は、ジェネリック版は
IList<T>
→ICollection<T>
→IEnumerable<out T>
→IEnumerable
非ジェネリックは
IList
→ICollection
→IEnumerable
の継承関係にある。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
を持っているにもかかわらず、ICollection
とICollection<T>
のどちらでもないため、Linq のCount()
の最適化にヒットしない。
では、なぜIReadOnlyCollection<out T>
に最適化が入っていないかだが、おそらくis
演算子のコストがタダではないからだと思われる。(あくまで個人的推測) IReadOnlyCollection<out T>
を継承しているクラスは高確率でICollection<T>
を継承しているはずなので、そこでヒットするだろうという考えな気がする。
でも、ICollection<T>
やICollection
は余計なメソッドがいっぱいある上、setter を提供してしまうのであまり ReadOnly なクラスにつけたくない。もちろん、いらないものは明示的実装で隠蔽しつつ、インターフェース経由で呼ばれたらNotSupportedException
でも投げればいいのだけど、Linq の内部実装の最適化ヒット読みでわざわざそんなことしないといけないなら、なぜIReadOnlyCollection<out T>
なんていうインタフェースをわざわざ作ったのか……と思わなくもない。特に、本来静的エラーにできるものを実行時エラーで解決するのは何のための静的型付けだ!!と思ってしまう。
最適化が入ってないのは何か理由があるんですかね?