今天我遇到一个有趣的问题,我有两种方法,快速浏览一下,两者都做同样的事情。那是返回一个IEnumerable的Foo对象。
我在下面将它们定义为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};
}
}
}
现在考虑以下测试:
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);
这两种方法似乎做了“相同”的事情。
为什么测试代码中的第二个断言失败?
为什么listTwo的第二个“Foo”项目在listOne中时没有设置为false?
注意:我正在解释为什么允许这种情况发生以及这两者的区别是什么。不是如何修复第二个断言,因为我知道如果我向List2添加ToList调用它将起作用。
答案 0 :(得分:6)
第一个代码块构建一次项目并返回包含项目的列表。
每次IEnumerable走过时,第二个代码块都会构建这些项目。
这意味着第一个块的第二行和第三行在同一个对象实例上运行。第二个块的第二行和第三行在不同的 Foo实例上运行(在迭代时创建新实例)。
查看此方法的最佳方法是在方法中设置断点并在调试器下运行此代码。第一个版本只会打破一次断点。第二个版本将打两次,一次是在.Where()调用期间,一次是在.ElementAt调用期间。 (编辑:使用修改后的代码,它也会在ToList()调用期间第三次点击断点。)
这里要记住的是迭代器方法(即它使用yield return)将在枚举器迭代的每个时间运行,而不仅仅是在构造初始返回值时。< / p>
答案 1 :(得分:2)
那些肯定是不是同样的事情。
第一个构建并在您调用它时返回一个列表,如果需要,您可以将其强制转换为列表并列出其中的内容,包括添加或删除项目,以及将结果放入你正在对这一组结果采取行动的变量。调用该函数会产生另一组结果,但重新使用单个调用的结果会对相同的对象起作用。
第二个构建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());
}
请注意,上面的代码将创建第一个(未使用的)Foo实例,但在进入foreach循环之前不会。因此,在调用之前,实际上并未创建项目。但这意味着每当你打电话给他们时,你就会建立一套新的物品。
答案 2 :(得分:1)
listTwo是一个迭代器 - 一个状态机。
ElementAt必须从迭代器的开头开始才能正确获取IEnumerable中的第i个索引(无论它是否是迭代器状态机或真正的IEnumerable实例),因此,listTwo将重新初始化为所有三个项目的默认值Enabled = true。
答案 3 :(得分:0)
建议:编译代码并使用反射器打开。收益率是一种语法上的混乱。您将能够看到您编写的代码中的代码逻辑差异以及为yield关键字生成的代码。两者都不一样。