【问题标题】:C#: IEnumerable<T>.Select() inefficient in some cases?C#:IEnumerable<T>.Select() 在某些情况下效率低下?
【发布时间】:2016-04-27 06:00:25
【问题描述】:

我最近了解到,由 .NET 的 LINQ 实现创建的对象对于特定的枚举类型效率低下。

看看这段代码:

public class DummyCollection : ICollection<int>
{
        public IEnumerator<int> GetEnumerator()
        {
            throw new Exception();
        }
        public int Count
        {
            get
            {
                return 10;
            }
        }
    //some more interface methods
}

基本上,DummyCollection 实例的大小为 10,但如果实际枚举,则会引发异常。

现在在这里:

var d = new DummyCollection();
Console.WriteLine(d.Count());

打印出10没有错误,但是这段代码:

var l = d.Select(a=> a);
Console.WriteLine(l.Count());

抛出一个异常,尽管说 l 的大小也是 10 是微不足道的(因为 Select 提供 1 对 1 映射)。这基本上意味着,当检查 Ienumerable 的长度时,输入可能是 Select-wrapped Collection,从而将计算时间从 O(1) 延长到惊人的 O(n)(如果选择功能特别麻烦)。

我知道当您请求 LINQ 的泛型时会牺牲效率,但这似乎是一个很容易解决的问题。我在网上查了一下,找不到任何人解决这个问题。有没有办法绕过这个缺点?有人在调查这个吗?有人修这个吗?这只是一个没有太大问题的边缘案例吗?任何见解都值得赞赏。

【问题讨论】:

  • 无论如何,在GetEnumerator 中抛出异常并能够在任何情况下调用Count() 的真正用例是什么......?
  • @MatíasFidemraizer 异常抛出只是一个占位符,以确保尽可能避免枚举。它可以被一个非常长的方法调用,或者只是一个非常大的集合,或者在枚举时发生变异的集合(例如锁定对象,这在异步环境中可能存在风险)来替换。
  • OrderBy 有同样的问题。 OrderBy 不会更改计数,但调用 Count() 仍会执行完整排序。您无需使用一些反射技巧进行排序即可获得正确的计数。见stackoverflow.com/questions/17493076/…
  • 我认为通过调用 LINQ 方法,您期望它会枚举。这就像说foreach 应该尽可能避免枚举。您希望 LINQ 保护用户免于低效使用它。但是添加魔法会使推理(和控制)它的行为变得非常困难,尤其是。当方法被链接时。我会说“不要犯罪,如果你不能做到这一点。”
  • @bentheiii 如果你实现IEnumerable&lt;T&gt;,那么避免枚举又有什么意义呢?如果您不希望给定的对象是可枚举的,则无需实现整个接口。我错了吗? :O

标签: c# linq


【解决方案1】:

你可以看到Count()扩展方法here是如何实现的。基本上是这样的:

public static int Count<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw Error.ArgumentNull("source");

    ICollection<TSource> collectionoft = source as ICollection<TSource>;

    if (collectionoft != null) return collectionoft.Count;

    ICollection collection = source as ICollection;

    if (collection != null) return collection.Count;

    int count = 0;
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        checked {
            while (e.MoveNext()) count++;
        }
    }
    return count;
}

如您所见,首先检查的方法是source 的类型为ICollection&lt;TSource&gt;ICollection,如果是这种情况,则无需迭代计算元素,只需返回Count 属性。

在您的第一种情况下,调用 Count 属性返回 10 并且永远不会调用 GetEnumerator() 方法。

当您使用Select() 方法时,您将集合包装到另一个不是ICollection 的类型(在上面的链接中您还可以看到Select() 实现),因此迭代是必要的。

在第二种情况下,当您调用 Count() 时,您的 GetEnumerator() 方法会被调用并引发异常。

【讨论】:

  • 我意识到这一点,我是说可以改进 Select 方法以返回 ICollection 类型的对象(如果源也是 ICollection)以优化用法以Count() 方法
  • @bentheiii:确实如此。但可能没有实施以保持延迟执行。
  • @bentheiii :LINQ 是关于懒惰的。 Select 不会返回东西的集合。它返回一些东西,当你迭代它时将通过链一次拉出一个项目。这意味着当您Select 时没有存储空间。将结果存储在集合中对于 LINQ 的预期用例将是灾难性的。把它想象成一个管道,一次处理一个项目(以一种非常短视的方式),而不是产生一个(存储的)集合的操作。
  • @spender 那么为什么不直接返回一个包装输入集合的对象呢?它仍然是惰性枚举,需要最小的存储空间(我认为不会比常规的 Select 枚举器多),而且还维护了 ICollection 接口,并提供了所有的好处。
  • @bentheiii 当您的下游用户想要在您的包装集合上使用ICollection&lt;T&gt;.Add 时会发生什么?您是否需要提供反向投影方法? LINQ 并不是真正关于集合、计数和索引,但您有机会将物化集合与扩展方法(例如 ToListToArrayToDictionaryToLookup)混合使用。试图将所有这些硬塞到核心 LINQ 中会使其变得笨拙(想想需要进行的所有不同类型的特征检测),而且很可能是行不通的。
【解决方案2】:

IEnumerable&lt;T&gt; 没有Count 的概念。这存在于实现中,它(除了这里和那里的奇怪快捷方式)在 LINQ to Objects 中没有任何作用。如果您使用Select 计划实现IEnumerable&lt;T&gt;(例如ICollection&lt;T&gt;),则唯一真正的保证是输出将是IEnumerable&lt;T&gt;... 没有Count

LINQ 应该被认为是处理项目序列,一次一个,仅处理当前和下一个项目(或序列结束)的概念。了解项目的数量是一项(可能)成本高昂的操作,需要对所有被计数的项目进行迭代,而不是在少数优化的情况下。

鉴于 LINQ 依赖于迭代而不是索引和计数,这意味着 IEnumerable 在您尝试迭代时出错将需要一些超级怪异飞行的特殊外壳。对我来说,这不是一个非常有用的用例。

【讨论】:

    猜你喜欢
    • 2021-07-31
    • 1970-01-01
    • 2014-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-27
    • 1970-01-01
    • 2022-01-12
    相关资源
    最近更新 更多