てくメモ

trivial な notes

【C#】列挙せずに要素の数の取得を試みる IEnumerable<T>.TryGetNonEnumeratedCount

IEnumerable<T>.TryGetNonEnumeratedCount(out int)メソッドは、列挙せずに要素の数の取得を試みることができる。(.NET 6 ~)

とりあえず確認用のクラスを用意して、Count()と並べてみる。

// 確認用クラス
private class SignalItem
{
    public SignalItem Signal()
    {
        Console.WriteLine("Wow!");
        return this;
    }
}
var items = new SignalItem[] { new(), new() };
var selectedItems = items.Select(v => v.Signal());

Console.WriteLine($"NonEnumeratedCount: {(selectedItems.TryGetNonEnumeratedCount(out int c) ? c : selectedItems.Count())}");
Console.WriteLine($"Count: {selectedItems.Count()}");
// NonEnumeratedCount: 2
// Wow!
// Wow!
// Count: 2

LINQで副作用を起こすのはよくないといったのはさておいて、TryGetNonEnumeratedCountメソッドの方では列挙が発生していないのが分かる。


中身としては次のような感じになっている。

  • シーケンスがICollection<T> / ICollectionなら、そのCountを取ってtrue
  • シーケンスがIIListProvider<T>なら、列挙せずに要素数を取れるか試み、可能だったならtrue
  • そうでなければfalse

参考:Source Browser


IIListProvider<T>internalインターフェースで、標準 LINQ メソッドのイテレーターが実装している。

要素の数は決まってくるよなぁ、というメソッドであれば、だいたい取得できるイメージ。

var seq = new int[] { 1, 2, 3, 1 };

int c;
Console.WriteLine($"Append: {(seq.Append(5).TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Prepend: {(seq.Prepend(5).TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Concat: {(seq.Concat(seq).TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"DefaultIfEmpty: {(seq.DefaultIfEmpty().TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Order: {(seq.Order().TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Select: {(seq.Select(_ => (double)_).TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Skip: {(seq.Skip(1).TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Take: {(seq.Take(2).TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
Console.WriteLine($"Reverse: {(seq.Reverse().TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");

// Append: 5
// Prepend: 5
// Concat: 8
// DefaultIfEmpty: 4
// Order: 4
// Select: 4
// Skip: 3
// Take: 2
// Reverse: 4

OrderReverseのようなソースと同じ要素数になるものだけでなく、AppendSkip、そしてConcatのようなものも可能であれば列挙せずに要素の数を返してくれる。


メソッド単発だけでなく、メソッドチェーンをしても可能な場合には取得できる。

Console.WriteLine($"Skip -> Reverse -> Append -> Order: {(seq
    .Skip(2)
    .Reverse()
    .Append(5)
    .Order()
    .TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
// Skip -> Reverse -> Append -> Order: 3


単発ではできていたメソッドの組み合わせでも、取得できなくなる場合もある。
例えば以下。

Console.WriteLine($"Skip -> Reverse -> Append -> Select: {(seq
    .Skip(2)
    .Reverse()
    .Append(5)
    .Select(_ => (double)_) // !
    .TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
// Skip -> Reverse -> Append -> Select: failed

上記の内容に触れる前に、似た例をふたつ。
以下は取得できる。

Console.WriteLine($"Select -> Skip -> Reverse -> Append: {(seq
    .Select(_ => (double)_) // !
    .Skip(2)
    .Reverse()
    .Append(5)
    .TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
// Select -> Skip -> Reverse -> Append: 3
Console.WriteLine($"Skip -> Take -> Select: {(seq
    .Skip(1)
    .Take(2)
    .Select(_ => (double)_) // !
    .TryGetNonEnumeratedCount(out c) ? c.ToString() : "failed")}");
// Skip -> Take -> Select: 2

??? となるかもしれないが、これはSelectが生成するイテレーターが異なってくるのが理由。
後者ふたつは要素数を取れる専用のイテレーターになっている、前者は要素数を取れないイテレーターとなってしまっている。

普通にLINQを使うぶんにはあまり実益がない気がするので、これ以上は深入りしない。


Deep Dive せずとも、TryGetNonEnumeratedCountは普通に使いたいメソッドだと思う。