在"How can I expose only a fragment of ilist<>question"中,其中一个答案包含以下代码片段:
1 2 3 4 5 6 7 8
| IEnumerable<object> FilteredList()
{
foreach( object item in FullList )
{
if( IsItemInPartialList( item )
yield return item;
}
} |
yield关键字在那里做什么?我在一些地方看到过它的引用,还有一个问题,但我还没有完全弄清楚它实际上是做什么的。我习惯于从一根线屈服于另一根线的角度来考虑屈服,但这在这里似乎并不相关。
yield关键字实际上在这里做了很多工作。函数返回实现IEnumerable接口的对象。如果调用函数开始对该对象进行调用,则再次调用该函数,直到它"生成"。这是C 2.0中引入的句法糖。在早期版本中,您必须创建自己的IEnumerable和IEnumerator对象来执行类似的操作。
理解这种代码的最简单方法是在一个示例中键入,设置一些断点,然后看看会发生什么。
尝试单步执行此操作,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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"行不再执行。
这是一个真实的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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);
}
}
}
}
} |
迭代。它创建了一个"在封面下"的状态机,它可以记住您在函数的每个附加循环中所处的位置,并从中获取信息。
产量有两大用途,
它有助于在不创建临时集合的情况下提供自定义迭代。
它有助于进行有状态的迭代。
为了更明确地解释以上两点,我制作了一个简单的视频,你可以在这里观看。
最近,RaymondChen也发表了一系列关于yield关键字的有趣文章。
- C语言中迭代器的实现及其后果(第1部分)
- C语言中迭代器的实现及其后果(第2部分)
- C语言中迭代器的实现及其后果(第3部分)
- C语言中迭代器的实现及其后果(第4部分)
虽然它名义上用于轻松实现迭代器模式,但可以概括为状态机。引用Raymond没有意义,最后一部分还链接到其他用途(但是Entin的博客中的例子特别好,展示了如何编写异步安全代码)。
第一眼,产量回报是一种纯糖,可以回报。
3.Without yield,all the items of the collection are created at once:
ZZU1
SAME code using yield,it returns item by item:
1 2 3 4 5 6 7 8 9 10 11
| class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
yield return new SomeData();
yield return new SomeData();
yield return new SomeData();
}
} |
使用产量的优点是,如果功能消耗了你的数据,简单地需要收集的第一个项目,项目的其余部分就不会产生。
应要求,产量算子允许项目的创建。这是一个很好的理由。
与调查者一起使用。在每一次呼唤产量声明时,控制权都回到呼叫者手中,但这意味着Callee's State is maintained.由于这个原因,当Caller Enumerates the next element时,它继续在Callee方法中执行,从EDOCX1&1之后立即声明。
让我们以一个例子来理解这一点。在这个例子中,对应于每一条线路,我在执行流量中提到了顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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()方法,然后国家会重新接通。
直观地说,关键字从函数返回一个值而不离开它,即在代码示例中,它返回当前的item值,然后继续循环。更正式地说,编译器使用它为迭代器生成代码。迭代器是返回IEnumerable对象的函数。MSDN上有几篇关于它们的文章。
一个列表或阵列实现负荷的所有项目立即在产量实现时提供一个缺陷执行方案。
在实践中,这往往是一种令人沮丧的情况,因为需要在工作中达到最小的数量,以减少资源消耗的一种应用。
例如,我们可以在一个数据库中应用数以百万计的记录。The following benefits can be achieved when we use ienumerable in a deferred execution pull-based model:
- Scalability,reliability and predictibility are likely to improve,since the number of records does not impressicantly impact the application's resource requirements.
- 性能和响应性是自从处理能够立即开始等待整个收藏成为第一个装载。
- 由于应用可以停止、开始、中断或失败,因此恢复能力和使用是可以改进的。只有进步中的项目才能与实际使用结果的一部分的所有数据进行比较。
- 在添加恒定工作负荷流的环境中,可进行连续处理。
这是一个比较,比如说一张清单,用产量来比较。
List Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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();
} |
控制台输出
联系人:创建联系人1
联系人2
联系人3
准备通过收藏重新开始。
注:整个收藏都装入了记忆中,甚至没有要求在列表中列入一个单一项目。
Yield Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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();
} |
控制台输出
准备通过收藏重新开始。
注:收藏并非一律执行。这是由于"有缺陷的执行"的性质。建设一个项目只会在需要时出现。
让我们重新召唤收藏,在我们第一次接触收藏的时候,抛弃行为。
1 2 3 4 5 6 7 8
| 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();
} |
控制台输出
准备通过收藏重新开始
联系人1
你好鲍勃
尼斯只有当客户"推出"项目时,才构建第一个接触点。
以下是理解概念的简单方法:基本思想是,如果您想要一个集合,您可以在上面使用"foreach",但是由于某些原因(例如从数据库中查询这些项)将这些项收集到集合中是很昂贵的,并且您通常不需要整个集合,那么您可以创建一个函数,一次生成一个集合,并将其返回给缺点。numer(然后可以提前终止收集工作)。
这样想:你到肉柜台去买一磅火腿片。屠夫把一个10磅重的火腿放在后面,放在切片机上,把整条火腿切片,然后把一堆切片拿回来,量出一磅。(旧路)。有了yield,屠夫把切片机带到柜台上,开始把每一片切片"生产"到秤上,直到它达到1磅,然后为你包装好,你就完成了。旧的方法可能对屠夫更好(让他按照自己喜欢的方式组织他的机器),但新的方法显然在大多数情况下对消费者更有效。
yield关键字允许您在迭代器块上以窗体创建IEnumerable。这个迭代器块支持延迟执行,如果您不熟悉这个概念,它可能看起来很神奇。然而,在一天结束的时候,只是代码执行时没有任何奇怪的技巧。
迭代器块可以被描述为语法糖,在这里编译器生成一个状态机,跟踪枚举的进展情况。要枚举可枚举的,通常使用foreach循环。然而,foreach循环也是句法上的糖分。因此,您是从实际代码中删除的两个抽象,这就是为什么最初很难理解它是如何一起工作的。
假设您有一个非常简单的迭代器块:
1 2 3 4 5 6 7 8 9 10
| 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循环:
1 2
| foreach (var i in IteratorBlock())
Console.WriteLine(i); |
这里是输出(这里没有惊喜):
1 2 3 4 5 6 7
| Begin
1
After 1
2
After 2
42
End |
如上所述,foreach是句法上的糖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
} |
为了解开这个谜团,我用板条箱装了一个序列图,去掉了抽象部分:

编译器生成的状态机也实现了枚举器,但为了使图表更清晰,我将它们作为单独的实例显示出来。(当从另一个线程枚举状态机时,您实际上会得到单独的实例,但这里的细节并不重要。)
每次调用迭代器块时,都会创建状态机的新实例。但是,在enumerator.MoveNext()第一次执行之前,迭代器块中的任何代码都不会执行。这就是延迟执行的工作方式。下面是一个(相当愚蠢的)例子:
1
| var evenNumbers = IteratorBlock().Where(i => i%2 == 0); |
此时迭代器尚未执行。Where条款创建了一个新的IEnumerable,它包装了IteratorBlock返回的IEnumerable,但这个可枚举的尚未枚举。当执行foreach循环时会发生这种情况:
1 2
| foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber); |
如果枚举可枚举的两次,则每次都会创建状态机的新实例,并且迭代器块将执行相同的代码两次。
注意,像ToList()、ToArray()、First()、Count()等linq方法将使用foreach循环来枚举可枚举的。例如,ToList()将枚举可枚举的所有元素并将它们存储在一个列表中。现在可以访问该列表以获取可枚举的所有元素,而无需再次执行迭代器块。在使用ToList()等方法时,在使用CPU生成可枚举多次的元素和存储枚举多次访问的元素的内存之间存在权衡。
简单地说,c yield关键字允许对代码体(称为迭代器)进行多次调用,这些代码体知道如何在完成之前返回,当再次调用时,将在停止的位置继续执行,即,它有助于迭代器在迭代器在连续调用中返回的序列中对每个项透明地具有状态。
在JavaScript中,相同的概念称为生成器。
如果我正确理解这一点,下面将从实现IEnumerable和yield的函数的角度来描述这一点。
- 这里有一个。
- 如果您需要其他电话,请再次致电。
- 我会记得我给你的。
- 我只知道你再打电话时我能不能再给你一个。
这是为对象创建可枚举的非常简单和容易的方法。编译器创建一个类,该类包装您的方法,并在本例中实现IEnumerable
它产生了可枚举序列。它所做的是实际创建本地IEnumerable序列并将其作为方法结果返回
这个链接有一个简单的例子
这里有更简单的例子
1 2 3 4
| public static IEnumerable<int> testYieldb()
{
for(int i=0;i<3;i++) yield return 4;
} |
注意,yield返回不会从方法返回。你甚至可以把一个WriteLine放在yield return之后。
上面生成的IEnumerable为4 ints 4,4,4,4
这里有一个WriteLine。将向列表中添加4,打印ABC,然后向列表中添加4,然后完成方法,从而真正从方法中返回(方法完成后,将发生在没有返回的过程中)。但这将有一个值,一个IEnumerable清单的int,它在完成时返回。
1 2 3 4 5 6
| public static IEnumerable<int> testYieldb()
{
yield return 4;
console.WriteLine("abc");
yield return 4;
} |
还要注意,当使用yield时,返回的类型与函数的类型不同。它是IEnumerable列表中元素的类型。
使用yield和方法的返回类型作为IEnumerable。如果方法的返回类型是int或List,并且您使用yield,那么它将不会编译。您可以使用不带收益的IEnumerable方法返回类型,但如果没有IEnumerable方法返回类型,则可能无法使用收益。
为了让它执行,你必须以一种特殊的方式调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static void Main(string[] args)
{
testA();
Console.Write("try again. the above won't execute any of the function!
");
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代码
1 2 3 4
| rubyArray = [1,2,3,4,5,6,7,8,9,10]
rubyArray.each{|x|
puts x # do whatever with x
} |
数组的每个方法实现都将控制权交给调用者("puts x"),数组的每个元素整齐地呈现为x。然后调用者可以对x做任何它需要做的事情。
然而.NET并不是一路走到这里。C似乎将yield与ienumerable结合在一起,在某种程度上迫使您在调用者中编写foreach循环,如mendelt的响应所示。稍微不那么优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13
| //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];
} |