【问题标题】:Count an IOrderedEnumerable without consuming it计算 IOrderedEnumerable 而不消耗它
【发布时间】:2013-07-05 16:12:12
【问题描述】:

我想做的事,简短版:

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()返回一个IOrderedEnumerableIOrderedEnumerable 显然不能强制转换为 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

}

【问题讨论】:

  • 不幸的是,LINQ 的设计方式使这完全不可能。
  • @SLaks:通常“完全不可能”被证明是可能的。我认为您可以使用表达式树来做到这一点,但这超出了我的能力范围。
  • 您能否创建自己的包装类来保存原始源代码,并在需要时应用任何排序?
  • 为什么不先数数再排序?
  • 我想创建一个库函数,无需深入了解其内部工作原理即可调用。当然,我可以让调用者将计数作为附加参数传递给 DoSomething(),或者要求源是一些 IMyCustomOrderedEnumerable,但两者看起来都很麻烦和不优雅。

标签: c# performance linq reflection ienumerable


【解决方案1】:

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

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 =&gt; x) 的结果被传递给 Count 扩展。你无法避免OrderBy 的执行。因此它是非流式操作符,它会在返回之前消耗所有源代码,然后将其传递给Count

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


更新:您可以在任何OrderedEnumerable 上调用此扩展方法 - 它将使用反射来获取包含源序列的OrderedEnumerable&lt;T&gt;source 字段。然后检查这个序列是否是集合,并使用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();

【讨论】:

  • 我知道我发布的代码并不能避免 OrderBy 的执行。这就是为什么我要求不同的方式。但是 OrderBy 创建的 IOrderedEnumerable 必须保留对原始列表的引用才能执行其延迟执行业务,因此理论上应该可以从该引用中获取计数。
  • 我只是在尝试相同的反射想法 - 它有效,但如果可能的话,应该避免依赖私有字段。可能无法保证框架更新中会存在相同的字段
  • @Rob 没有从 OrderedEnumerable 获取 source 的公开方式,并且类本身是内部的。所以,实际上你甚至不能确定OrderedEnumerable&lt;T&gt; 会在更新的框架中被OrderBy 返回。将来您将拥有的只是IOrderedEnumerable&lt;T&gt; 接口,它不会向您提供source。因此,反射是获取源代码的唯一途径。
  • @lazyb - 是的。我并不是要暗示有更安全的方法来获取source。我的意思是 OP 应该仔细考虑这种方法的潜在性能优势是否超过了脆弱性。
  • 你也可以处理ICollection coll = source as ICollection,所以你也支持非泛型集合。此外,如果您检查field 是否为空,您可以在一定程度上对其进行验证(即故障转移到慢速Count()
【解决方案2】:

如果您希望创建一个高性能的解决方案,我会考虑创建采用集合或 IOrderedEnumerable 等的重载。所有“是”和“作为”类型检查和强制转换都不适合这种类型你正在创造的东西。

您正在重新发明轮子。 linq 的“Count()”函数几乎可以满足您的需求。

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

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

等等……

【讨论】:

  • 整个事情是一个具有多个重载的扩展方法,我把它从我的简化代码示例中省略了,因为它不会改变根本问题。本机 Count() 函数专门不能做我想做的事:根据列表计算 IOrderedEnumerable,而不对列表进行排序。
【解决方案3】:

另一种方法是实现一个实现IOrderedEnumerable&lt;T&gt; 的类。然后,您可以实现将常用的 Linq 扩展方法短路的类成员,并提供一个查看原始枚举的 count 方法。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-10
    • 1970-01-01
    • 2019-04-19
    • 2011-12-18
    • 1970-01-01
    • 2019-02-11
    • 1970-01-01
    相关资源
    最近更新 更多