为什么ToList可以在不重新分配自身的情况下修改原始值?

时间:2019-05-02 16:12:37

标签: c# linq

我看到了这个问题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

c# online

我的问题

为什么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();

我知道这可能是懒惰评估

但是我想知道这个问题有任何官方链接或更详尽的解释吗?

3 个答案:

答案 0 :(得分:2)

您的标题说“ ...无需重新分配自身...”:因为A是引用类型,所以Select()不会在源集合中创建项目的副本。您的lambda依次获取每个实际对象。设置x.B = 0时,它将作用于仍在集合中的原始项目。

更有趣的问题是,为什么arr1arr2代码的行为不同。

让我们看看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可以在其中成形和选择数据,而不是对其进行修改。