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

Ale*_*ejo 2 .net c# linq

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

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

现在做完之后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!
Run Code Online (Sandbox Code Playgroud)

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

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

根据该文件Select

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

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

foreach (int i in enumerable)
{
    i.Dump();
}
Run Code Online (Sandbox Code Playgroud)

结果还是一样。

最后,我尝试了最后一件事来弄清楚我的新可枚举中的引用是否与旧的相同。我没有清除原始列表,而是:

ogList.Add(4);
Run Code Online (Sandbox Code Playgroud)

然后我打印出我的 enumerable(“克隆”的)的内容,希望看到 '4' 附加到它的末尾。相反,我得到了:

5
5
5
5 // Huh?
Run Code Online (Sandbox Code Playgroud)

现在我不得不承认,我不知道 Select 扩展方法在幕后是如何工作的。这是怎么回事?

Ale*_*țea 6

List/List<T>出于所有意图和目的,花哨的可调整大小的数组。他们拥有并持有值类型的数据,例如您的整数或对内存中引用类型数据的引用,并且他们总是知道他们有多少项。

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

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

当你写作时:

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

你是在说:听着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
    }
}
Run Code Online (Sandbox Code Playgroud)

(这解释了为什么你在期待 for 时得到 5,你的变换是 s => 5)

对于值类型,例如您的情况中的 int:如果要克隆列表,请使用通过List. 通过这种方式,您可以创建一个列表,该列表是原始列表的克隆,完全与其原始列表分离:

IEnumerable<int> cloneOfEnumerable = ogList.Select(s => s).ToList();
Run Code Online (Sandbox Code Playgroud)

稍后编辑:当然 ogList.Select(s => s) 相当于 ogList。我将在这里留下投影,就像问题中一样。

您在此处创建的是:来自可枚举结果的列表,通过IEnumerable<int>接口进一步消耗。考虑到我上面所说的IListvs的性质IEnumerable,我更愿意写/读:

IList<int> cloneOfEnumerable = ogList.ToList();
Run Code Online (Sandbox Code Playgroud)

注意:小心引用类型。IList/List不承诺保持对象“安全”,它们可能会因所有IList关心而变异为 null 。如果您需要,请使用关键字:深度克隆。

注意:当心无限或不可重绕的 IEnumerables

  • 很好的解释,以及关于深度克隆的好点子。最后我只使用了 `ogList.ToList()`,因为 `Select(s =&gt; s)` 没有完成任何事情。 (2认同)