I came across an interesting question today where I have two methods that, at a quick glance, both do the same thing. That is return an IEnumerable of Foo objects.
我今天遇到了一个有趣的问题,我有两种方法,一目了然,它们都做同样的事情。那是返回一个IEnumerable的Foo对象。
I have defined them below as List1 and List2:
我在下面将它们定义为List1和List2:
public class Foo
{
public int ID { get; set; }
public bool Enabled { get; set;}
}
public static class Data
{
public static IEnumerable<Foo> List1
{
get
{
return new List<Foo>
{
new Foo {ID = 1, Enabled = true},
new Foo {ID = 2, Enabled = true},
new Foo {ID = 3, Enabled = true}
};
}
}
public static IEnumerable<Foo> List2
{
get
{
yield return new Foo {ID = 1, Enabled = true};
yield return new Foo {ID = 2, Enabled = true};
yield return new Foo {ID = 3, Enabled = true};
}
}
}
Now consider the following tests:
现在考虑以下测试:
IEnumerable<Foo> listOne = Data.List1;
listOne.Where(item => item.ID.Equals(2)).First().Enabled = false;
Assert.AreEqual(false, listOne.ElementAt(1).Enabled);
Assert.AreEqual(false, listOne.ToList()[1].Enabled);
IEnumerable<Foo> listTwo = Data.List2;
listTwo.Where(item => item.ID.Equals(2)).First().Enabled = false;
Assert.AreEqual(false, listTwo.ElementAt(1).Enabled);
Assert.AreEqual(false, listTwo.ToList()[1].Enabled);
These two methods seem to do the "same" thing.
这两种方法似乎做了“相同”的事情。
Why do the second assertions in the test code fail?
Why is listTwo's second "Foo" item not getting set to false when it is in listOne?
为什么测试代码中的第二个断言失败了?为什么listTwo的第二个“Foo”项目在listOne中时没有设置为false?
NOTE: I'm after an explanation of why this is allowed to happen and what the differences in the two are. Not how to fix the second assertion as I know that if I add a ToList call to List2 it will work.
注意:我之前解释了为什么允许这种情况发生以及这两者的区别是什么。不是如何修复第二个断言,因为我知道如果我向List2添加ToList调用它将起作用。
4 个解决方案
#1
The first block of code builds the items once and returns a list with the items.
第一个代码块构建一次项目并返回包含项目的列表。
The second block of code builds those items each time the IEnumerable is walked through.
每次IEnumerable走过时,第二个代码块都会构建这些项目。
This means that the second and third line of the first block operate on the same object instance. The second block's second and third line operate on different instances of Foo (new instances are created as you iterate through).
这意味着第一个块的第二行和第三行在同一个对象实例上运行。第二个块的第二行和第三行在Foo的不同实例上运行(在迭代时创建新实例)。
The best way to see this would be to set breakpoints in the methods and run this code under the debugger. The first version will only hit the breakpoint once. The second version will hit it twice, once during the .Where() call, and once during the .ElementAt call. (edit: with the modified code, it will also hit the breakpoint a third time, during the ToList() call.)
查看此方法的最佳方法是在方法中设置断点并在调试器下运行此代码。第一个版本只会打破一次断点。第二个版本将打两次,一次是在.Where()调用期间,一次是在.ElementAt调用期间。 (编辑:使用修改后的代码,它也会在ToList()调用期间第三次命中断点。)
The thing to remember here is that an iterator method (ie. it uses yield return) will be run every time the enumerator is iterated through, not just when the initial return value is constructed.
这里要记住的是,迭代器方法(即它使用yield return)将在每次迭代枚举器时运行,而不仅仅是在构造初始返回值时。
#2
Those are definitely not the same thing.
那些绝对不是一回事。
The first builds and returns a list the moment you call it, and you can cast it back to list and list-y things with it if you want, including add or remove items, and once you've put the results in a variable you're acting on that single set of results. Calling the function would produce another set of results, but re-using the result of a single call acts on the same objects.
第一个构建并在您调用它时返回一个列表,如果需要,您可以将其强制转换为列表并列出其中的内容,包括添加或删除项目,并且一旦将结果放入变量中'对这一组结果采取行动。调用该函数会产生另一组结果,但重新使用单个调用的结果会对相同的对象起作用。
The second builds an IEnumerable. You can enumerate it, but you can't treat it as a list without first calling .ToList()
on it. In fact, calling the method doesn't do anything until you actually iterate over it. Consider:
第二个构建IEnumerable。您可以枚举它,但如果没有先调用.ToList()就不能将其作为列表处理。实际上,在实际迭代之前,调用该方法不会做任何事情。考虑:
var fooList = Data.List2().Where(f => f.ID > 1);
// NO foo objects have been created yet.
foreach (var foo in fooList)
{
// a new Foo object is created, but NOT until it's actually used here
Console.WriteLine(foo.Enabled.ToString());
}
Note that the code above will create the first (unused) Foo instance, but not until entering the foreach loop. So the items aren't actually created until called for. But that means every time you call for them, you're building a new set of items.
请注意,上面的代码将创建第一个(未使用的)Foo实例,但在进入foreach循环之前不会创建。因此,在调用之前,实际上并未创建项目。但这意味着每次你打电话给他们,你都会建立一套新的物品。
#3
listTwo is an iterator - a state machine.
listTwo是一个迭代器 - 一个状态机。
ElementAt must start at the beginning of the iterator to correctly get the i-th index in the IEnumerable (whether or not it is an iterator state machine or a true IEnumerable instance), and as such, listTwo will be reinitialized with the default values of Enabled = true for all three items.
ElementAt必须从迭代器的开头开始,以正确获取IEnumerable中的第i个索引(无论它是否是迭代器状态机或真正的IEnumerable实例),因此,listTwo将使用默认值重新初始化对于所有三个项目,Enabled = true。
#4
Suggestion: Compile the code and open with reflector. Yield is a syntactical suger. You would be able to see the code logic difference in the code your wrote and the code generated for the yield keyword. Both are not the same.
建议:编译代码并用反射器打开。收益率是一种语法上的混乱。您将能够看到您编写的代码中的代码逻辑差异以及为yield关键字生成的代码。两者都不一样。
#1
The first block of code builds the items once and returns a list with the items.
第一个代码块构建一次项目并返回包含项目的列表。
The second block of code builds those items each time the IEnumerable is walked through.
每次IEnumerable走过时,第二个代码块都会构建这些项目。
This means that the second and third line of the first block operate on the same object instance. The second block's second and third line operate on different instances of Foo (new instances are created as you iterate through).
这意味着第一个块的第二行和第三行在同一个对象实例上运行。第二个块的第二行和第三行在Foo的不同实例上运行(在迭代时创建新实例)。
The best way to see this would be to set breakpoints in the methods and run this code under the debugger. The first version will only hit the breakpoint once. The second version will hit it twice, once during the .Where() call, and once during the .ElementAt call. (edit: with the modified code, it will also hit the breakpoint a third time, during the ToList() call.)
查看此方法的最佳方法是在方法中设置断点并在调试器下运行此代码。第一个版本只会打破一次断点。第二个版本将打两次,一次是在.Where()调用期间,一次是在.ElementAt调用期间。 (编辑:使用修改后的代码,它也会在ToList()调用期间第三次命中断点。)
The thing to remember here is that an iterator method (ie. it uses yield return) will be run every time the enumerator is iterated through, not just when the initial return value is constructed.
这里要记住的是,迭代器方法(即它使用yield return)将在每次迭代枚举器时运行,而不仅仅是在构造初始返回值时。
#2
Those are definitely not the same thing.
那些绝对不是一回事。
The first builds and returns a list the moment you call it, and you can cast it back to list and list-y things with it if you want, including add or remove items, and once you've put the results in a variable you're acting on that single set of results. Calling the function would produce another set of results, but re-using the result of a single call acts on the same objects.
第一个构建并在您调用它时返回一个列表,如果需要,您可以将其强制转换为列表并列出其中的内容,包括添加或删除项目,并且一旦将结果放入变量中'对这一组结果采取行动。调用该函数会产生另一组结果,但重新使用单个调用的结果会对相同的对象起作用。
The second builds an IEnumerable. You can enumerate it, but you can't treat it as a list without first calling .ToList()
on it. In fact, calling the method doesn't do anything until you actually iterate over it. Consider:
第二个构建IEnumerable。您可以枚举它,但如果没有先调用.ToList()就不能将其作为列表处理。实际上,在实际迭代之前,调用该方法不会做任何事情。考虑:
var fooList = Data.List2().Where(f => f.ID > 1);
// NO foo objects have been created yet.
foreach (var foo in fooList)
{
// a new Foo object is created, but NOT until it's actually used here
Console.WriteLine(foo.Enabled.ToString());
}
Note that the code above will create the first (unused) Foo instance, but not until entering the foreach loop. So the items aren't actually created until called for. But that means every time you call for them, you're building a new set of items.
请注意,上面的代码将创建第一个(未使用的)Foo实例,但在进入foreach循环之前不会创建。因此,在调用之前,实际上并未创建项目。但这意味着每次你打电话给他们,你都会建立一套新的物品。
#3
listTwo is an iterator - a state machine.
listTwo是一个迭代器 - 一个状态机。
ElementAt must start at the beginning of the iterator to correctly get the i-th index in the IEnumerable (whether or not it is an iterator state machine or a true IEnumerable instance), and as such, listTwo will be reinitialized with the default values of Enabled = true for all three items.
ElementAt必须从迭代器的开头开始,以正确获取IEnumerable中的第i个索引(无论它是否是迭代器状态机或真正的IEnumerable实例),因此,listTwo将使用默认值重新初始化对于所有三个项目,Enabled = true。
#4
Suggestion: Compile the code and open with reflector. Yield is a syntactical suger. You would be able to see the code logic difference in the code your wrote and the code generated for the yield keyword. Both are not the same.
建议:编译代码并用反射器打开。收益率是一种语法上的混乱。您将能够看到您编写的代码中的代码逻辑差异以及为yield关键字生成的代码。两者都不一样。