IEnumerable<T> 来自 Enumerable.FromRange()。Select() 与 ToList()

IEnumerable<T> from Enumerable.FromRange().Select() vs ToList()

提问人:Andrew 提问时间:2/8/2022 最后编辑:Rand RandomAndrew 更新时间:2/10/2022 访问量:534

问:

这真的难倒了我,正如我所期望的“通过引用”行为一样。我期望此代码打印“5,5,5”,但它打印的是“7,7,7”。

IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 });
Alter(list);
Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));
Console.ReadLine();

void Alter(IEnumerable<MyObj> list)
{
    foreach(MyObj obj in list)
    {
        obj.Name = 5;
    }
}

class MyObj
{
    public int Name { get; set; }
}

而这按预期打印“7,7,7”。

IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 }).ToList();
Alter(list);
Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));
Console.ReadLine();

void Alter(IEnumerable<MyObj> list)
{
    foreach(MyObj obj in list)
    {
        obj.Name = 5;
    }
}

class MyObj
{
    public int Name { get; set; }
}

显然,这是我正在编写的实际代码的简化版本。这感觉更像是我在实例化对象的新实例的属性中遇到的行为。在这里我明白了为什么每次引用时都会得到一个新实例。Mine

public MyObj Mine => new MyObj();

我只是非常惊讶地在上面的代码中看到这种行为,感觉更像是我“锁定”了枚举的对象。谁能帮我理解这一点?

C# 列表 引用传递 IEnumerable

评论

6赞 Jon Skeet 2/8/2022
这与通过引用无关。(代码中没有任何内容。这是关于懒惰的评估。每次迭代时,它都会创建一个新的对象序列。这使得你对名称的使用没有帮助,因为它不是一个列表 - 它是一个懒惰计算的序列。调用 时,返回的值是对列表的引用 - 它是具体化的。我建议您搜索有关“LINQ 延迟计算”或“延迟执行”的教程。Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 })listToList()
0赞 Andrew 2/9/2022
非常感谢。我将来必须记住“延迟执行”一词。

答:

2赞 Caius Jard 2/8/2022 #1

这感觉更像是我在实例化对象的新实例的属性时遇到的行为

你的感觉是正确的

作为这个答案的序言,我想指出,在 C# 中,我们可以将方法存储在变量中,就像我们可以存储数据一样:

var method = () => new MyObj();

是“方法”;它没有名称,不带任何参数并返回一个新的 MyObj。我们实际上并没有将它称为方法,我们倾向于称它为 lambda,但就所有意图和目的而言,它的行为就像您熟悉的“方法”:() => new MyObj()

public MyObj SomeName(){
    return new MyObj();
}

您可能可以挑选出公共部分 - 编译器猜测返回类型,在内部为其提供名称,因为我们不在乎,当它是生成值的单个语句时,编译器也会为我们填充关键字。这是一个非常紧凑的方法逻辑。return

所以,回到这个变量,叫做:method

var method = () => new MyObj();

您可以像这样运行它:

var myObj = method.Invoke();

您甚至可以删除 Invoke 单词并只写 ,但为了清楚起见,我现在将 Invoke 保留在里面。调用该方法时,将运行,返回新的 MyObj 数据,该数据将存储在 ..method()myObj

因此,大多数时候,当我们编码时,变量存储数据,但有时它会存储方法,这很方便。


当您进行 LINQ Select 时,它要求您为其提供一个方法,每次枚举生成的可枚举对象时,该方法都将运行该方法:

var enumerable = new []{1,2,3}.Select(x => new MyObj());

它不会运行您在调用 Select 时给出的方法 x => new MyObj(),它不会生成任何数据;它只是存储方法以供以后使用

每次循环(枚举)它都会运行该方法()最多 3 次 - 我说最多,因为您不必完全枚举,但如果您这样做,数组中有 3 个元素,因此枚举最多可以导致该方法的 3 次调用。x => new MyObj()

LINQ Select 不会运行该方法并获取数据 - 它有效地创建数据提供方法的集合,而不是数据集合

从概念上讲,它就像 Select 生成了一组充满方法的方法:

var enumerable = new[]{
  () => new MyObj(),
  () => new MyObj(),
  () => new MyObj()
};

您可以枚举此方法并自行运行以下方法:

foreach(var method in enumerable)
  method.Invoke();

你的 Alter() 运行枚举(做了一个循环),从每个方法返回每个新对象,修改它,然后它被扔掉。从概念上讲,您的 Alter 方法执行了以下操作:

foreach(var method in enumerable)
  method.Invoke().Name = 5;

运行该方法,使用 Name 的一些默认值创建一个新的 MyObj,然后将 Name 更改为 5,然后丢弃对象数据

在完成 Alter 之后,您再次枚举它,再次创建新对象,因为方法再次运行,名称当然没有更改 - 新对象,没有更改。这里没有任何东西可以记住正在生成的数据;唯一记住的是生成数据的方法列表


当你在末尾放一个 ToList() 时,情况就不同了 - ToList 在内部枚举可枚举,导致生成对象的方法运行,但关键是它会将生成的对象隐藏在列表中,并为您提供对象列表。它不是提供值的方法集合,而是可以更改的值集合。

就像它这样做一样:

var r = new List<MyObj>();

foreach(var method in enumerable)
  r.Add(method.Invoke());

return r;

这意味着你会得到一个数据列表(而不是提供数据的方法列表),如果你随后循环这个列表,你正在改变存储在列表中的数据,而不是运行方法,改变返回的值并将其扔掉

评论

0赞 Johnathan Barclay 2/8/2022
lambda 表达式不是方法;这是一种创建(匿名)函数的方法。
0赞 Caius Jard 2/8/2022
当你在学校的低年级时,你的老师会告诉你,你不能取负数的根。所有的教学都是通过将概念与已经理解的事物联系起来来建立理解的渐进步骤。如果你改变一切,包括术语,那不是学习曲线,而是悬崖。一旦对正在发生的事情的理解得到赞赏,就会有足够的时间来学习“顺便说一句,这被称为......”
0赞 Johnathan Barclay 2/8/2022
我明白你的意思,但为什么假设 OP 对术语“方法”比“函数”更熟悉呢?方法是一种特定类型的函数,通常人们会先学习后一项,然后再学习前一项。此外,充斥着不正确的术语会适得其反,对那些来这里学习的人来说会适得其反。
0赞 Caius Jard 2/8/2022
我从“人们知道一些 C# 但相对惊讶于/最近发现返回 a 的属性不记得任何东西”的基础;对我来说,这个级别是“属性是数据,方法做操作”——对于我习惯的教育系统,我认为我们一开始不会在 C# 中将任何东西称为函数——它们是方法,其他术语稍后会输入new X
1赞 Andrew 2/9/2022
感谢大家的帮助。我真的很感激这个行业的每一个人都愿意帮助。从各种不同角度进行解释通常可以帮助更多人理解一个概念,否则每个人都无法理解。
3赞 BartoszKP 2/8/2022 #2

Select生成具有惰性计算的对象。这意味着所有步骤都是按需执行的。

因此,当此行被执行时:

IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 });

尚未创建任何实例 - 仅创建具有计算所指示内容的所有信息的 Enumerable。MyObj

当您调用 时,这将在迭代期间执行,并且所有对象都会被分配给它们的属性。Alterforeach5

但是当你打印它时,一切都会在这里再次执行:

Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));

因此,在执行 期间,将创建并打印全新的实例。string.JoinMyObjnew MyObj { Name = 7 }