我看到了这个问题Update all objects in a collection using LINQ。
课程
public class A
{
public int B { get; set; }
}
样本数据
var arr1 = new List<A>()
{
new A() {B = 10},
new A() {B = 20}
};
var arr2 = new List<A>()
{
new A() {B = 10},
new A() {B = 20}
};
arr1.Select(x=>{x.B=0;return x;}).ToList();
arr2.Select(x=>{x.B=0;return x;});
arr2.ToList();
结果
arr1 <===============>
0
0
arr2 <===============>
10
20
我的问题
为什么arr1.Select(x=>{x.B=0;return x;}).ToList();
设置arr1
的值而没有像
arr1
arr = arr1.Select(x=>{x.B=0;return x;}).ToList();
但是arr2
使用以下代码不能做同样的事情
arr2.Select(x=>{x.B=0;return x;});
arr2.ToList();
我知道这可能是懒惰评估
但是我想知道这个问题有任何官方链接或更详尽的解释吗?
答案 0 :(得分:2)
您的标题说“ ...无需重新分配自身...”:因为A
是引用类型,所以Select()
不会在源集合中创建项目的副本。您的lambda依次获取每个实际对象。设置x.B = 0
时,它将作用于仍在集合中的原始项目。
更有趣的问题是,为什么arr1
和arr2
代码的行为不同。
让我们看看Select()
返回的内容:
var z = arr2.Select(x => { x.B = 0; return x; });
arr2.ToList();
在第二行设置断点,我们发现这是z
的类型;这是arr2.Select(x => { x.B = 0; return x; })
返回的东西。与您在ToList()
行中调用arr1
的对象类型相同:
System.Linq.Enumerable.SelectListIterator<ConsoleApp3.A, ConsoleApp3.A>
Select()没什么用。它返回一个 prepared 对象,依次遍历arr2
中的每个项目,设置每个项目的B
属性,然后依次返回每个项目。
已经准备好了。但是它并没有做到这一点,并且直到您要求它(您建议的惰性评估)之后,它才这样做。让我们尝试一下:
var a = z.First();
这告诉SelectListIterator
仅对Select()
中的第一项评估arr2
λ。这就是全部。现在arr2
中的第一项有B == 0
,但其余项则没有,因为您尚未触摸它们。因此,让我们触摸所有它们:
var b = z.ToList();
现在,ToList()调用将强制SelectListIterator遍历并为arr2
中的每个项目执行Select()lambda表达式。您立即为arr1
进行了此操作,这就是B
中每个项目arr1
为零的原因。您根本没有为代码中的arr2
做过。起作用的不是Select()
,而是对象由 Select()
返回。对于arr2
,您在不枚举对象的情况下丢弃了该对象,因此该对象从未完成工作。
我们现在知道arr2.ToList()
并没有做任何事情:对于arr1
,t是对ToList()
的结果调用Select()
的行为将Select()
的更改应用到arr1
的文件。如果您改为致电arr1.ToList();
,那也将无效。它只会创建arr1
的精确副本,如果您未将其分配给任何内容,它将被丢弃。
所有这些都是为什么我们从不在LINQ表达式中添加副作用的原因之一:即使在为StackOverflow问题创建的最小,高度简化的示例中,您所创建的效果也令人困惑。您无需在生产代码中使用它。
另一个原因是我们永远不需要。
答案 1 :(得分:1)
arr1.Select(x=>{x.B=0;return x;}).ToList(); //Enumerates the Select, so it is executed
arr2.Select(x=>{x.B=0;return x;}); //Creates the query, it is not executed
arr2.ToList(); //Enumerates the list you already have
答案 2 :(得分:0)
您正在做的事情会在选择中产生副作用,也就是说,不是在选择数据,而是在选择内分配数据。
执行此操作时,只要枚举集合,就会执行代码(更改数据的代码),但是在此处枚举的集合都不是arr1和arr2:
// Here arr1 is not getting enumerated, what is getting enumerated is what
// is returned by the select, and it is enumerated immdiately because the
// ToList materializes it. This means that while the collection arr1 is
// unchanged, you are changing the value of its members, hence why it shows in
// your further console writelines
arr1.Select(x=>{x.B=0;return x;}).ToList();
// here you have the same select, but you discard it! a select doesn't affect
// the collection at all, arr2 is the SAME before and after the select, you
// would have to call ToList on what was RETURNED by the select, which is why
// it worked on arr1 (because you chained the ToList, so it was applied to
// what was returned by the Select)
arr2.Select(x=>{x.B=0;return x;});
// This does strictly nothing, you create a new list from arr2, which you do
// not store
// arr2.ToList();
基本上,如果要拆分对arr2的查询,则必须这样编写: var tmp = arr2.Select(x => {x.B = 0; return x;}); tpm.ToList(); //在TMP而不是arr2上调用它! arr2未被更改,但是tmp是选择返回的内容!
还请注意,总的来说,绝对不要执行任何操作,如果要更改集合的每个元素,请使用foreach,linq可以在其中成形和选择数据,而不是对其进行修改。