计算IOrderedEnumerable而不消耗它

Hug*_*une 6 c# linq reflection performance ienumerable

我想做什么,短版:

var source = new[]{2,4,6,1,9}.OrderBy(x=>x);
int count = source.Count; // <-- get the number of elements without performing the sort
Run Code Online (Sandbox Code Playgroud)

长版:

要确定IEnumerable中元素的数量,必须迭代所有元素.这可能是一项非常昂贵的操作.

如果可以将IEnumerable转换ICollection,则可以快速确定计数而无需迭代.LINQ Count()方法自动执行此操作.

函数myEnumerable.OrderBy()返回一个IOrderedEnumerable.一个IOrderedEnumerable显然不能被强制转换为ICollection的,因此调用COUNT()会消耗整个事情.

但排序不会改变元素的数量,IOrderedEnumerable必须保持对其源的引用.因此,如果该源是ICollection,则应该可以从IOrderedEnumerable中确定计数而不消耗它.

我的目标是有一个库方法,它接受带有n个元素的IEnumerable,然后例如检索位置为n/2的元素;

我想避免迭代IEnumerable两次只是为了得到它的计数,但我也想避免创建一个不必要的副本,如果可能的话.


这是我想要创建的函数的框架

public void DoSomething(IEnumerable<T> source)
{
    int count; // What we do with the source depends on its length

    if (source is ICollection)
    {
        count = source.Count(); // Great, we can use ICollection.Count
    }
    else if (source is IOrderedEnumerable)
    {
        // TODO: Find out whether this is based on an ICollection, 
        // TODO: then determine the count of that ICollection
    }
    else
    {
        // Iterating over the source may be expensive, 
        // to avoid iterating twice, make a copy of the source
        source = source.ToList();
        count = source.Count();
    }

    // do some stuff

}
Run Code Online (Sandbox Code Playgroud)

Ser*_*kiy 7

让我们来看看这段代码实际上是什么样的:

var source = new[]{ 2, 4, 6, 1, 9 }.OrderBy(x => x);
int count = source.Count();
Run Code Online (Sandbox Code Playgroud)

它是一样的

int count = Enumerable.Count(Enumerable.OrderBy(new[]{ 2, 4, 6, 1, 9 }, x => x));
Run Code Online (Sandbox Code Playgroud)

结果Enumerable.OrderBy(new[]{ 2, 4, 6, 1, 9 }, x => x)传递到Count扩展名.你无法避免OrderBy执行.因此它是非流媒体操作符,它会在返回内容之前消耗所有源,这将被传递给Count.

因此,避免迭代所有集合的唯一方法是避免OrderBy- 在排序之前计算项目.


更新:您可以在任何上调用此扩展方法OrderedEnumerable- 它将使用反射来获取保存源序列的source字段OrderedEnumerable<T>.然后检查此序列是否为集合,并在Count不执行排序的情况下使用:

public static class Extensions
{
    public static int Count<T>(this IOrderedEnumerable<T> ordered)
    {
        // you can check if ordered is of type OrderedEnumerable<T>
        Type type = ordered.GetType();
        var flags = BindingFlags.NonPublic | BindingFlags.Instance;
        var field = type.GetField("source", flags);
        var source = field.GetValue(ordered);
        if (source is ICollection<T>)
            return ((ICollection<T>)source).Count;

        return ordered.Count();
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

var source = new[]{ 2, 4, 6, 1, 9 }.OrderBy(x => x);
int count = source.Count();
Run Code Online (Sandbox Code Playgroud)