1. Qiita
  2. 投稿
  3. C#

Count<T>()について本気出して考えてみた

  • 20
    いいね
  • 0
    コメント

この記事はC# Advent Calendar 2016の5日目の記事です。

きっと無事5日目に投稿されていることでしょう。
5日目であれば時間なんて気にしません
前日書こうと思ってたけど、ドミニオンしてたら1日が終わってたなんて言えません
ドミニオンいいよ。ドミニオン。陰謀が手に入らなくて辛い。

CSharperなら皆大好きLinqのCount()メソッドのみに注力して記事を書こうと思います。

Count<T>()メソッド

【MSDN】Enumerable.Count(TSource) メソッド
これ貼っちゃったら終わりじゃね?そんな危惧は雪山にでも埋めて淡々と進めていきます。

この子は読んで字のごとく、配列の件数(要素数)を数えてくれるメソッドです。
ですが皆さん、これと似たようなプロパティをご存知ではないでしょうか。

【MSDN】List(T).Count プロパティ
はい、そうです。List<T>()型のCountプロパティですね。

私自身、Count()メソッドが実装された時は
「配列でも簡単に要素数数えられる!ヤッター!!」と思ったものです。
だがしかし、こいつはこんな感じに浮かれてる馬鹿野郎を闇に葬ることなんて朝飯前だったのです。

ここからはCount()メソッドとCountプロパティが何度も登場します。
そこで、これ以降は
「後ろに括弧が付いていたらメソッド」
「付いていなかったらプロパティ」
として表現します。

Count()が抱える闇

Count()のソースコードを見てくれ。こいつをどう思う?

Enumerable.Count()
public static int Count<TSource>(this IEnumerable<TSource> source) {
    // ~~ (省略) ~~

    int count = 0;
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        checked {
            while (e.MoveNext()) count++;
        }
    }
    return count;
}

すごく・・・ループしてます・・・

プロパティと違い、Count()はあくまでもメソッド。
頑張ってぐるぐる回して件数をカウントしてるんですねー。

何も知らないでプロパティ感覚で使うと死ぬってのは、ループのコストでメモリがヤバイって事です。

闇を抜けよう

では、どうすれば良いかを紐解いていきましょう。
ここで重要なのは、さっきのソースコードで省略した部分になります(ォィ
だって、最初から見せるとこの記事の存在価値が・・・(

Enumerable.Count()
public static int Count<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    ICollection<TSource> collectionoft = source as ICollection<TSource>;
    if (collectionoft != null) return collectionoft.Count;
    ICollection collection = source as ICollection;
    if (collection != null) return collection.Count;

    int count = 0;
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        checked {
            while (e.MoveNext()) count++;
        }
    }
    return count;
}
  • 呼出し元のオブジェクトが「ICollection<TSource>」を実装している
  • 呼出し元のオブジェクトが「ICollection」を実装している

場合に限り、ループせずにプロパティの値を返してくれます。
Linqの優しい心遣い。MSの無慈悲の愛に全力で甘えましょう。

どういうことだってばよ

IEnumerable<int> intArray = Enumerable.Range(1, 100000);
IEnumerable<int> intList = Enumerable.Range(1, 100000).ToList();

// ICollectionを実装して無い為、全要素をループして件数をカウンティングする。
Console.WriteLine("件数:{0}", intArray.Count());
// 宣言はIEnumerable<T>でも実体はICollectionを実装している為、プロパティの値を返してくれる。
Console.WriteLine("件数:{0}", intList.Count());     

ってことですね。
型宣言と実体型の意識をしっかり意識していれば大丈夫だったりします。
逆を言えば、意識してないとあっさり死にます。

どういう時に注意する?

この事を知ったときに、一番注意すべきだと思ったのは遅延実行系でしょうか。
EntityFrameworkとか良い例ですね。

プロパティ感覚でCount()を連打した場合、そいつが遅延実行のオブジェクトだった場合に
毎回ループするだけでなく配列を作る処理自体も毎回実行されるので、
そりゃもう連日お祭り騒ぎのようになります。
配列作成処理でDBやファイルを関与していた場合にはもう目も当てられません。

遅延実行の場合はCount()だけでなく、Linqの処理それぞれに注意したほうがいいですね。
必要に応じて.ToList()や.ToArray()で実体にして持ち回りましょう。
勿論メモリには気をつけて。

Linqを実行する際の型はちゃんと意識しましょう!

ワンポイントアドバイス

要素存在チェックを行う場合ですが

アンチパターン
if (hogeArray.Count() > 0) { }

これでは全ての要素を舐めて件数を取得する為、効率が良く有りません。
1件あればいいのに、要素数に比例して処理時間がかかってしまいます。

対策方法
if (hogeArray.Any()) { }

これならば要素が1つ見つかった時点でループを抜けるので、
要素数に関わらず低コストで実行できます。

件数の有無を調べるだけならAny()を使ってあげましょう。
これを使えば、案外Count()の出番は少なかったりします!

Linqを理解して、良いCShaper's Christmasをお過ごしください!!