在How Can I Expose Only a Fragment of IList<>问题中,其中一个答案包含以下代码段:
IEnumerable<object> FilteredList()
{
foreach(object item in FullList)
{
if(IsItemInPartialList(item))
yield return item;
}
}
yield关键字有什么作用?我已经看到它在几个地方被引用,另外一个问题,但我还没弄清楚它实际上做了什么。我习惯于在一个线程产生另一个线程的意义上考虑收益率,但这似乎并不重要。
yield
关键字实际上在这里做了很多。
该函数返回一个实现IEnumerable<object>
接口的对象。如果调用函数在此对象上启动foreach
ing,则再次调用该函数,直到它“屈服”。这是C#2.0中引入的语法糖。在早期版本中,你必须创建自己的IEnumerable
和IEnumerator
对象来做这样的事情。
理解这样的代码的最简单方法是键入一个示例,设置一些断点并查看会发生什么。尝试单步执行此示例:
public void Consumer()
{
foreach(int i in Integers())
{
Console.WriteLine(i.ToString());
}
}
public IEnumerable<int> Integers()
{
yield return 1;
yield return 2;
yield return 4;
yield return 8;
yield return 16;
yield return 16777216;
}
当您单步执行该示例时,您会发现第一次调用Integers()
会返回1
。第二次调用返回2
,并且不会再次执行yield return 1
行。
这是一个现实生活中的例子:
public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
using (var connection = CreateConnection())
{
using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
{
command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return make(reader);
}
}
}
}
}
yield
关键字允许您在IEnumerable<T>
上创建表单中的iterator block。这个迭代器块支持延迟执行,如果你不熟悉这个概念,它可能看起来几乎是神奇的。但是,在一天结束时,它只是执行代码而没有任何奇怪的技巧。
迭代器块可以被描述为语法糖,其中编译器生成状态机,该状态机跟踪可枚举枚举的进度。要枚举可枚举,您经常使用foreach
循环。然而,foreach
循环也是语法糖。因此,您从实际代码中删除了两个抽象,这就是为什么它最初可能很难理解它是如何一起工作的。
假设您有一个非常简单的迭代器块:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
真正的迭代器块通常具有条件和循环,但是当您检查条件并展开循环时,它们最终仍然是与其他代码交错的yield
语句。
要枚举迭代器块,使用foreach
循环:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
这是输出(这里没有惊喜):
Begin 1 After 1 2 After 2 42 End
如上所述foreach
是语法糖:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
为了解开这个问题,我创建了一个删除了抽象的序列图:
编译器生成的状态机也实现了枚举器,但为了使图更清晰,我将它们显示为单独的实例。 (当从另一个线程枚举状态机时,您实际上会获得单独的实例,但这里的详细信息并不重要。)
每次调用迭代器块时,都会创建一个新的状态机实例。但是,在第一次执行enumerator.MoveNext()
之前,迭代器块中的所有代码都不会执行。这是延迟执行的工作原理。这是一个(相当愚蠢)的例子:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
此时迭代器尚未执行。 Where
条款创建了一个新的IEnumerable<T>
,包裹了IEnumerable<T>
返回的IteratorBlock
,但这个可枚举的内容尚未列举。执行foreach
循环时会发生这种情况:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
如果枚举可枚举的两次,则每次都会创建一个新的状态机实例,并且迭代器块将执行两次相同的代码。
请注意,像ToList()
,ToArray()
,First()
,Count()
等LINQ方法将使用foreach
循环来枚举可枚举。例如,ToList()
将枚举可枚举的所有元素并将它们存储在列表中。您现在可以访问列表以获取可枚举的所有元素,而无需再次执行迭代器块。在使用CPU生成多次可枚举元素和使用ToList()
等方法存储枚举元素以多次访问它们之间需要进行权衡。
如果我理解正确的话,这就是我如何从使用yield实现IEnumerable的函数的角度来说明这一点。
简单地说,C#yield关键字允许对一个代码体(称为迭代器)的多次调用,它知道如何在它完成之前返回,并且当再次调用时,继续它停止的地方 - 即它有助于迭代器对迭代器在连续调用中返回的序列中的每个项目变为透明状态。
在JavaScript中,相同的概念称为生成器。
这是为对象创建可枚举的一种非常简单易用的方法。编译器创建一个包装您的方法的类,并在这种情况下实现IEnumerable <object>。如果没有yield关键字,则必须创建一个实现IEnumerable <object>的对象。
它产生了可枚举的序列。它的作用实际上是创建本地IEnumerable序列并将其作为方法结果返回
这个link有一个简单的例子
这里有更简单的例子
public static IEnumerable<int> testYieldb()
{
for(int i=0;i<3;i++) yield return 4;
}
请注意,yield return不会从方法返回。你甚至可以在WriteLine
之后放一个yield return
以上产生了4个int 4,4,4,4的IEnumerable
这里有一个WriteLine
。将列表中的4添加,打印abc,然后将4添加到列表中,然后完成方法,然后从方法返回(一旦方法完成,就像没有返回的过程一样)。但这将有一个值,一个IEnumerable
的int
s列表,它在完成后返回。
public static IEnumerable<int> testYieldb()
{
yield return 4;
console.WriteLine("abc");
yield return 4;
}
另请注意,使用yield时,返回的内容与函数的类型不同。它是IEnumerable
列表中元素的类型。
您将yield与方法的返回类型一起用作IEnumerable
。如果方法的返回类型是int
或List<int>
并且你使用yield
,那么它将无法编译。你可以使用没有屈服的IEnumerable
方法返回类型,但似乎你不能使用没有IEnumerable
方法返回类型的yield。
要让它执行,你必须以特殊的方式调用它。
static void Main(string[] args)
{
testA();
Console.Write("try again. the above won't execute any of the function!\n");
foreach (var x in testA()) { }
Console.ReadLine();
}
// static List<int> testA()
static IEnumerable<int> testA()
{
Console.WriteLine("asdfa");
yield return 1;
Console.WriteLine("asdf");
}
它试图引入一些Ruby Goodness :) 概念:这是一些示例Ruby代码,用于打印出数组的每个元素
rubyArray = [1,2,3,4,5,6,7,8,9,10]
rubyArray.each{|x|
puts x # do whatever with x
}
Array的每个方法实现都会控制调用者('puts x'),数组的每个元素都整齐地表示为x。然后,调用者可以执行x所需的任何操作。
然而.Net并不是一直都在这里.C#似乎与IEnumerable的耦合产量,在某种程度上强迫你在调用者中编写一个foreach循环,如Mendelt的响应中所示。少一点优雅。
//calling code
foreach(int i in obCustomClass.Each())
{
Console.WriteLine(i.ToString());
}
// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
for(int iLooper=0; iLooper<data.Length; ++iLooper)
yield return data[iLooper];
}
迭代。它创建了一个“幕后”的状态机,可以记住你在函数的每个附加周期中的位置并从那里获取。
最近,Raymond Chen还在yield关键字上发表了一系列有趣的文章。
虽然它名义上用于轻松实现迭代器模式,但可以推广到状态机。没有必要引用Raymond,最后一部分也链接到其他用途(但Entin的博客中的例子非常好,显示了如何编写异步安全代码)。
乍一看,yield return是一个返回IEnumerable的.NET糖。
如果没有yield,则会立即创建集合的所有项:
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
return new List<SomeData> {
new SomeData(),
new SomeData(),
new SomeData()
};
}
}
使用yield的相同代码,它逐项返回:
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
yield return new SomeData();
yield return new SomeData();
yield return new SomeData();
}
}
使用yield的优点是,如果消耗数据的函数只需要集合的第一项,则不会创建其余项。
yield操作符允许根据需要创建项目。这是使用它的一个很好的理由。
yield return
与调查员一起使用。在每次调用yield语句时,控制权返回给调用者,但它确保维持被调用者的状态。因此,当调用者枚举下一个元素时,它将继续在yield
语句之后的语句中的callee方法中执行。
让我们试着通过一个例子来理解这一点。在这个例子中,对应于每一行,我已经提到了执行流程的顺序。
static void Main(string[] args)
{
foreach (int fib in Fibs(6))//1, 5
{
Console.WriteLine(fib + " ");//4, 10
}
}
static IEnumerable<int> Fibs(int fibCount)
{
for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
{
yield return prevFib;//3, 9
int newFib = prevFib + currFib;//6
prevFib = currFib;//7
currFib = newFib;//8
}
}
此外,每个枚举都保持状态。假设,我再次调用Fibs()
方法然后状态将被重置为它。
列表或数组实现立即加载所有项,而yield实现提供延迟执行解决方案。
在实践中,通常希望根据需要执行最少量的工作以减少应用程序的资源消耗。
例如,我们可能有一个处理来自数据库的数百万条记录的应用程序。当我们在延迟执行基于拉的模型中使用IEnumerable时,可以实现以下好处:
下面是构建一个集合(如列表与使用yield)之间的比较。
列表示例
public class ContactListStore : IStore<ContactModel>
{
public IEnumerable<ContactModel> GetEnumerator()
{
var contacts = new List<ContactModel>();
Console.WriteLine("ContactListStore: Creating contact 1");
contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
Console.WriteLine("ContactListStore: Creating contact 2");
contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
Console.WriteLine("ContactListStore: Creating contact 3");
contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
return contacts;
}
}
static void Main(string[] args)
{
var store = new ContactListStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection.");
Console.ReadLine();
}
控制台输出 ContactListStore:创建联系人1 ContactListStore:创建联系人2 ContactListStore:创建联系人3 准备好迭代整个系列。
注意:整个集合都被加载到内存中,甚至没有要求列表中的单个项目
产量实例
public class ContactYieldStore : IStore<ContactModel>
{
public IEnumerable<ContactModel> GetEnumerator()
{
Console.WriteLine("ContactYieldStore: Creating contact 1");
yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
Console.WriteLine("ContactYieldStore: Creating contact 2");
yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
Console.WriteLine("ContactYieldStore: Creating contact 3");
yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
}
}
static void Main(string[] args)
{
var store = new ContactYieldStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection.");
Console.ReadLine();
}
控制台输出 准备好迭代整个系列。
注意:集合根本没有执行。这是由于IEnumerable的“延迟执行”性质。只有在真正需要时才会构建项目。
让我们再次调用该集合,并在我们获取集合中的第一个联系人时讨论该行为。
static void Main(string[] args)
{
var store = new ContactYieldStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection");
Console.WriteLine("Hello {0}", contacts.First().FirstName);
Console.ReadLine();
}
控制台输出 准备好迭代整个系列 ContactYieldStore:创建联系人1 你好鲍勃
太好了!当客户端将项目“拉出”集合时,仅构建了第一个联系人。
这是一个理解这个概念的简单方法:基本思想是,如果你想要一个可以使用“foreach
”的集合,但由于某种原因(例如从数据库中查询它们)将项目收集到集合中是昂贵的,并且您通常不需要整个集合,然后您创建一个函数,一次构建一个项目并将其返回给消费者(然后可以提前终止收集工作)。
可以这样想:你去肉类柜台,想要买一磅切好的火腿。屠夫把一个10磅重的火腿放在后面,把它放在切片机上,切成整片,然后将一堆切片带回给你,然后测出一磅。 (旧方式)。使用yield
,屠夫将切片机带到柜台,然后开始切片并“切割”每个切片到秤上,直到它测量到1磅,然后为你包裹它,你就完成了。对于屠夫而言,旧方式可能更好(让他按照自己喜欢的方式组织他的机器),但对于消费者而言,新方式在大多数情况下显然更有效。