提问人:Andrew 提问时间:2/8/2022 最后编辑:Rand RandomAndrew 更新时间:2/10/2022 访问量:534
IEnumerable<T> 来自 Enumerable.FromRange()。Select() 与 ToList()
IEnumerable<T> from Enumerable.FromRange().Select() vs ToList()
问:
这真的难倒了我,正如我所期望的“通过引用”行为一样。我期望此代码打印“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# 中,我们可以将方法存储在变量中,就像我们可以存储数据一样:
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;
这意味着你会得到一个数据列表(而不是提供数据的方法列表),如果你随后循环这个列表,你正在改变存储在列表中的数据,而不是运行方法,改变返回的值并将其扔掉
评论
new X
Select
生成具有惰性计算的对象。这意味着所有步骤都是按需执行的。
因此,当此行被执行时:
IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 });
尚未创建任何实例 - 仅创建具有计算所指示内容的所有信息的 Enumerable。MyObj
当您调用 时,这将在迭代期间执行,并且所有对象都会被分配给它们的属性。Alter
foreach
5
但是当你打印它时,一切都会在这里再次执行:
Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));
因此,在执行 期间,将创建并打印全新的实例。string.Join
MyObj
new MyObj { Name = 7 }
评论
Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 })
list
ToList()