【问题标题】:Does Linq's IEnumerable.Select return a reference to the original IEnumerable?Linq 的 IEnumerable.Select 是否返回对原始 IEnumerable 的引用?
【发布时间】:2019-02-26 06:16:55
【问题描述】:

我试图在我的代码中克隆一个 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()s 等效于 Console.WriteLine()

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

根据documentationSelect

这个方法是通过延迟执行来实现的。立即返回值是一个存储执行操作所需的所有信息的对象。在通过直接调用其 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 扩展方法在幕后是如何工作的。怎么回事?

【问题讨论】:

  • Select 本身并没有实现任何东西; foreachEnumerator 你最终会做的遍历。
  • 谁对这个问题和其中一个答案投了反对票,至少给出一个理由?

标签: c# .net linq


【解决方案1】:

List/List&lt;T&gt; 用于所有意图和目的的花哨的可调整大小的数组。他们拥有并保存值类型的数据,例如您的 int 或对内存中引用类型数据的引用,并且他们始终知道自己拥有多少项。

IEnumerable/IEnumerable&lt;T&gt; 是不同的野兽。他们提供不同的服务/合同。 IEnumerable 是虚构的,它不存在。它可以凭空创建数据,无需物理支持。他们唯一的承诺是他们有一个名为GetEnumerator() 的公共方法,它返回一个IEnumerator/IEnumerator&lt;T&gt;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 =&gt; s),您在概念上定义了从 int 到 int 的恒等投影。

您编写的 select 的一个非常粗略的简化非现实实现是:

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();

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

您在这里创建的是:来自可枚举结果的列表,通过IEnumerable&lt;int&gt; 接口进一步使用。考虑到我上面所说的关于IListIEnumerable 的性质,我更愿意写/读:

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

注意:小心引用类型。 IList/List 不承诺保持对象“安全”,它们可以在所有 IList 关心的情况下变为 null。如果需要,请使用关键字:深度克隆。

注意:当心无限或不可回退的 IEnumerables

【讨论】:

  • 很好的解释,关于深度克隆的要点。最后我只使用了ogList.ToList(),因为Select(s =&gt; s) 没有完成任何事情。
【解决方案2】:

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

但是,请记住,list.Select(e =&gt; e).ToList() 只有在处理诸如int 之类的值类型时才会得到真正的克隆。

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

【讨论】:

  • RIght... 还有很多其他的细微差别:) 很难用几段来解释。好点子!
【解决方案3】:

您必须知道,实现IEnumerable 的对象本身不必是集合。它是一个可以获取实现IEnumerator 的对象的对象。一旦你有了枚举器,你就可以请求第一个元素和下一个元素,直到没有更多的下一个元素。

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

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

您必须注意:只要您的 LINQ 语句是 IEnumerable 某事,您的源序列就不会被访问。如果您在开始枚举之前更改了源序列,则枚举将覆盖您更改的源序列。

如果您不希望这样做,则必须在更改源之前进行枚举。通常这将是ToList,但也可以是任何非延迟函数:Max()Any()FirstOrDefault() 等。

List<TSource> sourceItems = ...
var myEnumerable = sourceItems
    .Where(sourceItem => ...)
    .GroupBy(sourceItem => ...)
    .Select(group => ...);

// note: myEnumerable is an IEnumerable, it is not a sequence yet.
var list1 = sourceItems.ToList();         // Enumerate over the sequence
var first = sourceItems.FirstOrDefault(); // Enumerate and stop after the first

// now change the source, and to the same things again
sourceItems.Clear();
var list1 = sourceItems.ToList();         // returns empty list
var first = sourceItems.FirstOrDefault(); // return null: there is no first element

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

【讨论】:

  • 你的第一句话让我大开眼界,我一直只认为 IEnumerable 作为许多集合类型的父级。但我想从它的名字来看,我应该在很久以前就知道了……
  • 同样,IQueryable(也实现了IEnumerable)也不是查询的结果。查询将在您使用Enumerator.MoveNext 获得第一个元素的枚举数和 asj 后立即执行
【解决方案4】:

这是一个可枚举的。

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

如果您遍历此枚举,LINQ 将依次遍历原始结果集。每一次。如果您对原始可枚举进行任何操作,结果也将反映在您的 LINQ 调用中。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-30
    相关资源
    最近更新 更多