计算IOrderedEnumerable而不消耗它

时间:2013-07-05 16:12:12

标签: c# performance linq reflection 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

长版:

要确定 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

}

3 个答案:

答案 0 :(得分:7)

让我们考虑一下这段代码的实际情况:

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

相同
int count = Enumerable.Count(Enumerable.OrderBy(new[]{ 2, 4, 6, 1, 9 }, x => x));

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();
    }
}

用法:

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

答案 1 :(得分:0)

如果你正在寻找创建一个高性能的解决方案,我会考虑创建一个带有集合或IOrderedEnumerable等的重载。所有“是”和“as”类型检查和强制转换不适合那种类型你正在创造的东西。

你正在重新发明轮子。 linq的“Count()”函数几乎可以随心所欲。

另外,添加this关键字并将其变成一个漂亮的扩展方法,以取悦自己和其他人使用代码。

DoSomething(this Collection source);
DoSomething<T>(this List<T> source);
DoSomething<T>(this IOrderedEnumerable<T> source);

等...

答案 2 :(得分:0)

另一种方法是实现一个实现IOrderedEnumerable<T>的类。然后,您可以实现将使常用Linq扩展方法短路的类成员,并提供查看原始枚举的计数方法。

public class MyOrderedEnumerable<T> : IOrderedEnumerable<T>
{
    private IEnumerable<T> Original;
    private IOrderedEnumerable<T> Sorted;

    public MyOrderedEnumerable(IEnumerable<T> orig)
    {
            Original = orig;
            Sorted = null;
    }

    private void ApplyOrder<TKey>(Func<T, TKey> keySelector, IComparer<TKey> comparer, bool descending)
    {
            var before = Sorted != null ? Sorted : Original;
            if (descending)
                    Sorted = before.OrderByDescending(keySelector, comparer);
            else
                    Sorted = before.OrderBy(keySelector, comparer);
    }

    #region Interface Implementations

    public IEnumerator<T> GetEnumerator()
    {
            return Sorted != null ? Sorted.GetEnumerator() : Original.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
            return GetEnumerator();
    }

    public IOrderedEnumerable<T> CreateOrderedEnumerable<TKey>(
            Func<T, TKey> keySelector,
            IComparer<TKey> comparer,
            bool descending)
    {
            var newSorted = new MyOrderedEnumerable<T>(Original);
            newSorted.ApplyOrder(keySelector, comparer, descending);
            return newSorted;
    }

    #endregion Interface Implementations


    //Ensure that OrderBy returns the right type. 
    //There are other variants of OrderBy extension methods you'll have to short-circuit
    public MyOrderedEnumerable<T> OrderBy<TKey>(Func<T, TKey> keySelector)
    {   
            Console.WriteLine("Ordering");
            var newSorted = new MyOrderedEnumerable<T>(Original);
            newSorted.Sorted = (Sorted != null ? Sorted : Original).OrderBy(keySelector);
            return newSorted;
    }

    public int Count()
    {
            Console.WriteLine("Fast counting..");
            var collection = Original as ICollection;
            return collection == null ? Original.Count() : collection.Count;
    }

    public static void Test()
    {
            var nums = new MyOrderedEnumerable<int>(Enumerable.Range(0,10).ToList());
            var nums2 = nums.OrderBy(x => -x);
            var z = nums.Count() + nums2.Count();
    }
}