TryGetNonEnumeratedCount
attempts to determine the number of elements in a sequence without forcing an enumeration. It returns true
if it could and false
if it couldn't. The API was added with .NET 6 - let's have a look how that thing works.
A simple example
Have a look at the following example:
var myEnumeration = new[] { 1, 2, 3, 4 };
var could = myEnumeration.TryGetNonEnumeratedCount(out var count);
This one is easy. We have an Array
and an Array
has a Length
property the function could retrieve. So could
is true
and count
is 4
.
Internally TryGetNonEnumeratedCount
calls ICollection<T>.Count
if the type implements ICollection<T>
. Array
implements ICollection<T>
so it's easy to get the count.
A more complex example
Have a look at the following example:
var range = Enumerable.Range(1, 4);
var could = myEnumeration.TryGetNonEnumeratedCount(out var count);
Still, could
is true
and count
is 4
. But how did that work? Enumerable.Range
returns an IEnumerable<int>
, and IEnumerable<T>
does not have a Count
property. So, how did TryGetNonEnumeratedCount
get the count? The magic lies in a special internal
interface called: IIListProvider
. The description of said type:
An iterator that can produce an array or
List<TElement>
through an optimized path.
And guess what? There is a RangeIterator
. This is the actual type your Enumerable.Range
method returns. IIListProvider
has a GetCount
method which returns the count. So TryGetNonEnumeratedCount
calls GetCount
on the RangeIterator
and gets the count. The method signature is:
internal interface IIListProvider<TElement>
{
int GetCount(bool onlyIfCheap);
}
The onlyIfCheap
parameter is a hint to the implementation that it should only return the count if it's cheap to do so:
public int GetCount(bool onlyIfCheap) => unchecked(_end - _start);
That is the actual implementation of the method inside the RangeIterator
. You defined the start and end, so it is easy for the method to calculate how many items it would hold.
LINQ
- Let's make it spicey!
Okay, even though that seems straightforward, we can go a bit deeper into the rabbit hole. Let's have a look at the following example:
var myEnumeration = Enumerable.Range(1, 4);
var newEnumeration = myEnumeration
.Order()
.Select(s => s * s);
var could = newEnumeration.TryGetNonEnumeratedCount(out var count);
What do you think could
and count
is? could
is true
and count
is 4
. But how did that work? And why does it know the count is four without enumerating? Well, let's have a look from a logical/abstract point of view. We know that Enumerable.Range
returns a RangeIterator
, which is an IIListProvider<int>
. We also know that Select
or Order
doesn't change anything in the quantity of our enumeration, so why should I care then?
And that is exactly how it works. We have certain iterators that just look up what the parent does. Let's take the last Select
statement. The real call looks like this:
public int GetCount(bool onlyIfCheap)
{
if (!onlyIfCheap)
{
// ...
}
return _source.GetCount(onlyIfCheap);
}
And guess who our _source
is? It's the RangeIterator
we already know. So the Select
iterator just asks the RangeIterator
for the count. And the RangeIterator
knows the count because it's an IIListProvider<int>
.
If we throw in a Where()
statement the IIListProvider
has no idea how many items will be returned. It would have to call the Where
statement on the concrete instance. As this would enumerate, it will return false
and count
will not be set.
All in all, it boils down to many internal iterators that represent common operations in the .NET framework that implement IIListProvider
and can therefore possibly return the count without enumerating. It is a clever way without exposing the internals!
Special case: IQueryable<T>
Short: It doesn't matter what you do by default IQueryable<T>
will always return false
and count
will not be set:
var myEnumeration = Enumerable.Range(1, 4).AsQueryable();
var could = myEnumeration.TryGetNonEnumeratedCount(out var count);
Even though it looks almost like the above, TryGetNonEnumeratedCount
will not work here. IQueryable
is neither an ICollection
nor does it implement IIListProvider
.
Conclusion
TryGetNonEnumeratedCount
is a nice addition to the .NET world. It allows you to get the count of an enumeration without forcing an enumeration. This can be useful in certain scenarios. Hope you understand the internals a bit better.