如何判断 IEnumerable<T> 是不是需要延迟执行?

Posted

技术标签:

【中文标题】如何判断 IEnumerable<T> 是不是需要延迟执行?【英文标题】:How to tell if an IEnumerable<T> is subject to deferred execution?如何判断 IEnumerable<T> 是否需要延迟执行? 【发布时间】:2021-10-17 22:18:00 【问题描述】:

我一直认为,如果我在 LINQ to 对象的上下文中使用 Select(x=&gt; ...),那么新集合将立即创建并保持静态。我不太确定我为什么假设这个,这是一个非常糟糕的假设,但我做到了。我经常在其他地方使用.ToList(),但在这种情况下通常不会。

此代码表明,即使是简单的“选择”也会延迟执行:

var random = new Random();
var animals = new[]  "cat", "dog", "mouse" ;
var randomNumberOfAnimals = animals.Select(x => Math.Floor(random.NextDouble() * 100) + " " + x + "s");

foreach (var i in randomNumberOfAnimals)

    testContextInstance.WriteLine("There are " + i);


foreach (var i in randomNumberOfAnimals)

    testContextInstance.WriteLine("And now, there are " + i);

这会输出以下内容(每次迭代集合时都会调用随机函数):

There are 75 cats
There are 28 dogs
There are 62 mouses
And now, there are 78 cats
And now, there are 69 dogs
And now, there are 43 mouses

我在很多地方都有IEnumerable&lt;T&gt; 作为班级成员。 LINQ 查询的结果通常分配给这样的IEnumerable&lt;T&gt;。通常对我来说,这不会导致问题,但我最近在我的代码中发现了一些地方,它不仅会造成性能问题。

在尝试检查我犯了这个错误的地方时,我想我可以检查一个特定的IEnumerable&lt;T&gt; 是否属于IQueryable 类型。我想这会告诉我收藏是否“推迟”。事实证明,上面 Select 运算符创建的枚举器的类型是 System.Linq.Enumerable+WhereSelectArrayIterator``[System.String,System.String] 而不是 IQueryable

我使用Reflector 来查看这个接口继承自什么,结果证明它根本没有从任何表明它是“LINQ”的东西继承——因此无法根据集合类型进行测试。

我很高兴现在将.ToArray() 放在任何地方,但我希望有一个机制来确保这个问题将来不会发生。 Visual Studio 似乎知道如何做到这一点,因为它给出了一条关于“扩展结果视图将评估集合”的消息。

我想出的最好的是:

bool deferred = !object.ReferenceEquals(randomNumberOfAnimals.First(),
                                        randomNumberOfAnimals.First());

编辑:这仅适用于使用“选择”创建新对象且不是通用解决方案的情况。无论如何我都不推荐它!这是解决方案的一个小舌头。

【问题讨论】:

你为什么关心延迟执行?您不应该注意这一点,并将其视为您的 IEnumerable&lt;T&gt; 的私有实现细节。 因为我每次都得到不同的对象,我没想到会这样。我正在修改它们的数量,然后该更改丢失了 这与延迟执行有什么关系?我认为您弄错了问题的根源。 请记住,查询表达式为您提供了一个表示查询本身的对象。对象不代表查询的RESULTS,对象代表查询。把它想象成一个 SQL 查询字符串,只会更聪明。您向查询询问其结果,然后查询执行。你再问一次,查询再次执行;不保证您第二次询问的结果相同;从那时起,世界可能发生了变化。 我遇到的最好的是here,虽然不是万无一失..! 【参考方案1】:

关于扩展结果视图将评估集合的消息是为所有IEnumerable 对象呈现的标准消息。我不确定是否有任何万无一失的方法来检查IEnumerable 是否被推迟,主要是因为即使yield 也被推迟了。绝对确保不会延迟的唯一方法是接受ICollectionIList&lt;T&gt;

【讨论】:

如果IList&lt;T&gt; 有一个虚拟的Item getter,它也可以被推迟。 是的,困惑的雪佛兰/日产杂交种说了什么。如果你想确定,只接受一个具体的数组。 ICollection 似乎效果最好。当我意识到这一点时-我倾向于忘记那个。由于开销,我不想要 List 或 IList,但如果我忘记使用 ToArray() [或有人使用我的软件],ICollection 会告诉我【参考方案2】:

手动实现一个懒惰的IEnumerator&lt;T&gt; 是绝对有可能的,所以没有“完全通用”的方法。我要记住的是:如果我在枚举与之相关的内容时更改列表中的内容,请始终在 foreach 之前调用 ToArray()

【讨论】:

【参考方案3】:

一般来说,我会说你应该尽量避免担心它是否被推迟。

IEnumerable&lt;T&gt; 的流式执行性质具有优势。这是真的 - 有时它是不利的,但我建议总是专门处理那些(罕见的)时间 - 要么转到 ToList()ToArray() 将其转换为适当的列表或数组。

剩下的时间,最好让它被推迟。需要经常检查这似乎是一个更大的设计问题......

【讨论】:

同意。这个问题更多地是出于好奇,想知道如何做到这一点,以及寻找发生此问题的地方。我绝对想几乎总是做'ToArray()'。我刚刚没有它就离开了,因为a)看起来有点难看,b)我认为“选择”投影立即实现了。【参考方案4】:

延迟执行 LINQ 让很多人陷入困境,你并不孤单。

我为避免这个问题所采取的方法如下:

方法的参数 - 使用IEnumerable&lt;T&gt;,除非需要更具体的接口。

局部变量 - 通常在我创建 LINQ 的时候,所以我会知道是否可以进行惰性求值。

类成员 - 永远不要使用 IEnumerable&lt;T&gt;,始终使用 List&lt;T&gt;。并始终将它们设为私有。

属性 - 使用IEnumerable&lt;T&gt;,并在setter 中转换为存储。

public IEnumerable<Person> People 

    get  return people; 
    set  people = value.ToList(); 

private List<People> people;

虽然存在这种方法行不通的理论案例,但我还没有遇到过这种情况,而且自 Beta 后期以来我一直在热情地使用 LINQ 扩展方法。

顺便说一句:我很好奇你为什么使用 ToArray(); 而不是 ToList(); - 对我来说,列表有更好的 API,而且(几乎)没有性能成本。

更新:一些评论者正确地指出数组具有理论上的性能优势,因此我将上面的陈述修改为“......(几乎)没有性能成本。”

更新 2:我编写了一些代码来对数组和列表之间的性能差异进行一些微基准测试。在我的笔记本电脑上,在我的特定基准测试中,每次访问的差异约为 5ns(即 nano 秒)。我想在某些情况下,每个循环节省 5ns 是值得的……但我从来没有遇到过。在运行时间长到足以准确测量之前,我必须将测试提高到 100 百万次迭代。

【讨论】:

Bevan:我同意你的大部分观点——但最后一句话。列表有性能成本,它们非常非常小。我同意,在几乎所有情况下,它都可以忽略不计,但 List (微观上)比数组慢,所以如果你正在做高性能。代码,有时使用 ToArray() 是有原因的。 @Bevan:数组整洁的一个小例子是基线 JIT 可以从它们那里获得与优化 JIT 可以从 List&lt;T&gt; 获得的性能相似的性能。有使用数组的直接 IL 指令 - 不需要单独的方法内联。 任何不为类成员使用 ICollection 或 IList 的具体原因。我倾向于为班级成员使用 ICollection @Simon - List 类具有丰富且富有表现力的 API。我发现通过该 API 进行访问而不是人为地将自己限制为 IList 或 ICollection 是有益的,尤其是当我可以轻松地确保我总是有一个 List 可以使用时。 在我看来,性能并不是做某事的最佳理由,除非性能已被证明是一个问题。为工作使用正确的结构比节省几毫秒更重要,尤其是对于大多数应用程序而言。栈溢出的创造者曾发过博文,搜索编码恐怖和微优化。此外,这符合不过早优化的概念。您可以对限制您的性能做出假设。甚至可以想象优化器会撤销你所做的一切(正如许多人在基准测试中遇到的那样)。【参考方案5】:

这是对延迟执行的一种有趣反应 - 大多数人认为它是积极的,因为它允许您转换数据流而无需缓冲所有内容。

您建议的测试不起作用,因为迭代器方法没有理由不能在两次连续尝试中产生与其第一个对象相同的引用对象实例。

IEnumerable<string> Names()

    yield return "Fred";

这将每次返回相同的静态字符串对象,作为序列中的唯一项。

由于您无法可靠地检测从迭代器方法返回的编译器生成的类,因此您必须做相反的事情:检查一些知名容器:

public static IEnumerable<T> ToNonDeferred(this IEnumerable<T> source)

    if (source is List<T> || source is T[]) // and any others you encounter
        return source;

    return source.ToArray();

通过返回IEnumerable&lt;T&gt;,我们将集合保持为只读,这很重要,因为我们可能会取回副本或原件。

【讨论】:

那么视觉工作室是怎么做到的? Visual Studio 为不是ICollectionany 可枚举显示“扩展结果视图将评估集合”。 我喜欢延期执行。我只是做了一个愚蠢的假设(实际上甚至没有意识到) Select(x => x + "foo") 不会被推迟。我完全理解为什么会这样,但直到今天它才[令人惊讶地]导致我的代码中出现错误 @Simon_Weaver 延迟执行很酷,但也很危险。如果您在声明周围放置了一个 try 块,期望它被执行,但直到下一个块才执行,该怎么办?这是一个未处理的异常。我过去一直被这个问题所困扰。这是一个强大的功能,但对于外行来说,它可能会导致问题。如果您在本地块中使用 IEnumerable 和 var,您可能不知道实际放入其中的内容。适当的单元测试应该能识别出这些情况,但解决它们可能会令人困惑。 检查ICollection&lt;T&gt; 比检查List&lt;T&gt;T[] 更好。见this【参考方案6】:

我的五美分。很多时候你必须处理一个你不知道里面有什么的枚举。

您的选择是:

在使用前将其列在列表中,但您可能会遇到无穷无尽的麻烦 按原样使用,你很可能会面临各种延期执行的搞笑事情,你又遇到麻烦了

这是一个例子:

[TestClass]
public class BadExample

    public class Item
    
        public String Value  get; set; 
    
    public IEnumerable<Item> SomebodysElseMethodWeHaveNoControlOver()
    
        var values = "at the end everything must be in upper".Split(' ');
        return values.Select(x => new Item  Value = x );
    
    [TestMethod]
    public void Test()
    
        var items = this.SomebodysElseMethodWeHaveNoControlOver();
        foreach (var item in items)
        
            item.Value = item.Value.ToUpper();
        
        var mustBeInUpper = String.Join(" ", items.Select(x => x.Value).ToArray());
        Trace.WriteLine(mustBeInUpper); // output is in lower: at the end everything must be in upper
        Assert.AreEqual("AT THE END EVERYTHING MUST BE IN UPPER", mustBeInUpper); // <== fails here
    

所以没有办法摆脱它,只有一个方法:在运行的基础上精确地迭代一次。

对于立即和延迟执行场景使用相同的 IEnumerable 接口显然是一个糟糕的设计选择。这两者之间必须有明确的区别,以便从名称或通过检查属性可以清楚地看出枚举是否被延迟。

提示:在您的代码中考虑使用IReadOnlyCollection&lt;T&gt; 而不是普通的IEnumerable&lt;T&gt;,因为除此之外您还获得了Count 属性。这样你就确定它不是无穷无尽的,你可以把它变成一个列表没有问题。

【讨论】:

感谢@bonomo。你能把结果发布在这里给那些没有时间运行它的人吗:-) 谢谢

以上是关于如何判断 IEnumerable<T> 是不是需要延迟执行?的主要内容,如果未能解决你的问题,请参考以下文章

如何将多个 IEnumerable<IEnumerable<T>> 列表添加到 IEnumerable<List<int>>

如何将项目添加到 IEnumerable<T> 集合?

如何在 C# 中将 IEnumerable<T> 转换为 List<T>?

如何将 IEnumerable<t> 或 IQueryable<t> 转换为 EntitySet<t>?

如何订购 IEnumerable<T> 以使特殊项目始终位于底部? [复制]

IEnumerable<T>.ToArray() 如何工作?