我试图在代码中克隆一个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扩展方法在后台如何工作。发生了什么事?
答案 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>
界面进一步消耗。考虑到我上面对IList
和IEnumerable
的本质所说的话,我更愿意写/读:
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-cloning或here中我最喜欢的一种(可能受对象内部结构的限制)。
答案 2 :(得分:1)
您必须注意,实现IEnumerable
的对象本身不必是集合。它是一个对象,可以获取实现IEnumerator
的对象。有了枚举器后,您可以要求第一个元素和下一个元素,直到没有其他下一个元素为止。
每个返回IEnumerable
的LINQ函数都不是序列本身,它只能使您请求枚举数。如果需要序列,则必须使用ToList
。
还有其他一些LINQ函数,它们不返回IEnumerable
,而是返回Dictionary
或仅返回一个元素({{1},FirstOrDefault()
,{{1 }},Max()
。这些函数将从Single()
获取枚举数并开始枚举,直到得到结果为止。Any()
仅需检查是否可以开始枚举。{{1 }}将枚举所有元素,并记住最大的元素。等等。
您将必须知道:只要您的LINQ语句是某物的IEnumerable
,您的源序列就尚未被访问。如果您在开始枚举之前更改了源序列,则枚举将覆盖更改后的源序列。
如果您不想这样做,则必须在更改源之前进行枚举。通常,该名称为Any
,但这可以是任何非递延函数:Max
,IEnumerable
,ToList
等。
Max()
因此,每个不返回IEnumerable的LINQ函数都将开始在Any()
上进行枚举,因为序列是在您开始枚举的那一刻开始的。 IEnumerable不是序列本身。
答案 3 :(得分:0)
这是一个枚举。
var enumerable = ogList.Select(s => s);
如果您遍历此可枚举,LINQ将依次遍历原始结果集。每次。如果您对原始枚举数做任何事情,结果也将反映在您的LINQ调用中。
如果您需要冻结数据,请改为将其存储在列表中:
var enumerable = ogList.Select(s => s).ToList();
现在,您已经制作了副本。遍历此列表将不会触及原始枚举。