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
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
Order
やReverse
のようなソースと同じ要素数になるものだけでなく、Append
やSkip
、そして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
は普通に使いたいメソッドだと思う。