Linq的IEnumerable.Select是否返回对原始IEnumerable的引用?

时间:2018-09-21 20:06:29

标签: c# .net linq

我试图在代码中克隆一个List,因为我需要将该List输出到其他代码,但是稍后将清除原始引用。因此,我想到了使用Select扩展方法来创建对具有相同元素的IEnumerable的新引用,例如:

List<int> ogList = new List<int> {1, 2, 3};
IEnumerable<int> enumerable = ogList.Select(s => s);

现在ogList.Clear()之后,我很惊讶地发现我的新枚举也为空。

所以我开始在LINQPad中摆弄,发现即使我的Select完全返回了不同的对象,其行为也是相同的。

List<int> ogList = new List<int> {1, 2, 3};
IEnumerable<int> enumerable = ogList.Select(s => 5); // Doesn't return the original int
enumerable.Count().Dump(); // Count is 3
ogList.Clear();
enumerable.Count().Dump(); // Count is 0!

请注意,在LINQPad中,Dump()Console.WriteLine()等效。

现在可能我首先需要克隆列表是由于设计不良,即使我不想重新考虑设计,我也可以轻松地正确克隆它。但这让我考虑了Select扩展方法实际上做什么

根据Select的{​​{3}}:

  

此方法通过使用延迟执行来实现。立即返回值是一个对象,该对象存储执行操作所需的所有信息。在通过直接调用其GetEnumerator方法或在Visual C#中使用foreach或在Visual Basic中使用For Each枚举该对象之前,不会执行此方法表示的查询。

因此,然后我尝试在清除之前添加以下代码:

foreach (int i in enumerable)
{
    i.Dump();
}

结果仍然相同。

最后,我尝试了最后一件事,以确定新枚举中的引用是否与旧引用相同。我没有清除原始列表,而是这样做了:

ogList.Add(4);

然后我打印出我的可枚举(“克隆”的)的内容,希望在其末尾看到“ 4”。相反,我得到了:

5
5
5
5 // Huh?

现在,我别无选择,只能承认我不知道Select扩展方法在后台如何工作。发生了什么事?

4 个答案:

答案 0 :(得分:4)

List/List<T>用于所有可调整大小的花哨数组。他们拥有并保存诸如您的int之类的值类型的数据或内存中对引用类型的数据的引用,并且他们始终知道它们有多少个项目。

IEnumerable/IEnumerable<T>是不同的野兽。他们提供不同的服务/合同。 IEnumerable是虚构的,不存在。它可以凭空创建数据,而无需物理支持。他们唯一的保证是他们有一个称为GetEnumerator()的公共方法,该方法返回一个IEnumerator/IEnumerator<T>IEnumerator做出的承诺很简单: 当您决定需要某个项目时,某些项目可能可用或不可用。这是通过IEnumerator接口具有的一种简单方法来实现的:bool MoveNext()-在枚举完成时返回false,或者在实际上有一个新项目需要返回时返回true。您可以通过IEnumerator接口具有的属性(通常称为Current)读取数据。

要回到您的观察/问题:就示例中的IEnumerable而言,它甚至不会考虑数据,除非您的代码告诉它获取某些数据。

写作时:

List<int> ogList = new List<int> {1, 2, 3};
IEnumerable<int> enumerable = ogList.Select(s => s);

您的意思是:在这里IEnumerable听,我可能会在将来某个时候向您询问此列表中的某些项目。我会告诉您何时需要它们,暂时不动,不做任何事情。使用Select(s => s),您将在概念上定义int到int的身份投影。

您编写的选择的一个非常粗糙的,简化的,非现实的实现是:

IEnumerable<T> Select(this IEnumerable<int> source, Func<int,T> transformer) something like
{
    foreach (var i in source) //create an enumerator for source and starts enumeration
    {
        yield return transformer(i); //yield here == return an item and wait for orders
    }
}

(这解释了为什么您期望for时得到5,而您的变换是s => 5)

对于值类型,例如您所用的整数:如果要克隆列表,请使用通过List实现的枚举结果来克隆整个列表或列表的一部分,以供将来进行枚举。这样,您可以创建一个列表,该列表是原始列表的克隆,与原始列表完全分离

IEnumerable<int> cloneOfEnumerable = ogList.Select(s => s).ToList();

您要在此处创建的是:可枚举结果的列表,该列表通过IEnumerable<int>界面进一步消耗。考虑到我上面对IListIEnumerable的本质所说的话,我更愿意写/读:

IList<int> cloneOfEnumerable = ogList.Select(s => s).ToList();

注意:注意参考类型。 IList/List不保证对象“安全”,对于所有IList,它们都可以变为null。关键字(如果需要):深度克隆。

答案 1 :(得分:1)

提供的答案解释了为什么您没有获得克隆列表(由于某些LINQ扩展方法的延迟执行)。

但是,请记住,list.Select(e => e).ToList()仅在处理诸如int之类的值类型时才会获得真实的克隆。

如果您有引用类型的列表,您将收到一个对已存在对象的引用的克隆列表。在这种情况下,您应该考虑使用solutions provided here for deep-cloninghere中我最喜欢的一种(可能受对象内部结构的限制)。

答案 2 :(得分:1)

您必须注意,实现IEnumerable的对象本身不必是集合。它是一个对象,可以获取实现IEnumerator的对象。有了枚举器后,您可以要求第一个元素和下一个元素,直到没有其他下一个元素为止。

每个返回IEnumerable的LINQ函数都不是序列本身,它只能使您请求枚举数。如果需要序列,则必须使用ToList

还有其他一些LINQ函数,它们不返回IEnumerable,而是返回Dictionary或仅返回一个元素({{1},FirstOrDefault(),{{1 }},Max()。这些函数将从Single()获取枚举数并开始枚举,直到得到结果为止。Any()仅需检查是否可以开始枚举。{{1 }}将枚举所有元素,并记住最大的元素。等等。

您将必须知道:只要您的LINQ语句是某物的IEnumerable,您的源序列就尚未被访问。如果您在开始枚举之前更改了源序列,则枚举将覆盖更改后的源序列。

如果您不想这样做,则必须在更改源之前进行枚举。通常,该名称为Any,但这可以是任何非递延函数:MaxIEnumerableToList等。

Max()

因此,每个不返回IEnumerable的LINQ函数都将开始在Any()上进行枚举,因为序列是在您开始枚举的那一刻开始的。 IEnumerable不是序列本身。

答案 3 :(得分:0)

这是一个枚举。

var enumerable = ogList.Select(s => s);

如果您遍历此可枚举,LINQ将依次遍历原始结果集。每次。如果您对原始枚举数做任何事情,结果也将反映在您的LINQ调用中。

如果您需要冻结数据,请改为将其存储在列表中:

var enumerable = ogList.Select(s => s).ToList();

现在,您已经制作了副本。遍历此列表将不会触及原始枚举。