我已经看到在 Stack Overflow 和博客上大量使用了 yield 关键字。我不使用LINQ。有人可以解释 yield 关键字吗?
我知道存在类似的问题。但是没有人真正用简单的语言解释它的用途。
我已经看到在 Stack Overflow 和博客上大量使用了 yield 关键字。我不使用LINQ。有人可以解释 yield 关键字吗?
我知道存在类似的问题。但是没有人真正用简单的语言解释它的用途。
到目前为止(我见过的)最好的解释是 Jon Skeet 的书——那一章是免费的!第 6 章,深入了解 C#。我可以在这里添加任何未涵盖的内容。
然后买书;你会成为一个更好的 C# 程序员。
问:为什么我不在这里写一个更长的答案(从评论中转述);简单的。正如 Eric Lippert 所观察到的(这里),yield
构造(以及它背后的魔力)是C# 编译器中最复杂的一段代码,在这里尝试在简短的回复中描述它充其量是幼稚的。IMO有很多细微差别yield
,最好是参考预先存在的(和完全合格的)资源。
Eric 的博客现在有 7 个条目(这只是最近的)讨论yield
. 我非常尊重 Eric,但他的博客可能更适合作为熟悉该主题的人(在这种情况下)的“更多信息”,因为它通常描述了许多背景设计注意事项。最好在合理的基础上完成。yield
(是的,第 6 章确实下载了;我验证了...)
该关键字与返回oryield
的方法一起使用,它使编译器生成一个类,该类实现了使用迭代器所需的管道。例如IEnumerable<T>
IEnumerator<T>
public IEnumerator<int> SequenceOfOneToThree() {
yield return 1;
yield return 2;
yield return 3;
}
鉴于上述情况,编译器将生成一个实现IEnumerator<int>
, IEnumerable<int>
and的类(实际上它也将实现andIDisposable
的非泛型版本)。IEnumerable
IEnumerator
SequenceOfOneToThree
这允许您像这样在foreach
循环中调用该方法
foreach(var number in SequenceOfOneToThree) {
Console.WriteLine(number);
}
一个迭代器就是一个状态机,所以每次yield
被调用的时候在方法中的位置都会被记录下来。如果迭代器移动到下一个元素,则该方法会在该位置之后立即恢复。所以第一次迭代返回 1 并标记该位置。下一个迭代器在一个之后立即恢复,因此返回 2,依此类推。
不用说,您可以以任何您喜欢的方式生成序列,因此您不必像我一样对数字进行硬编码。此外,如果你想打破循环,你可以使用yield break
.
为了揭开神秘面纱,我将避免谈论迭代器,因为它们本身可能是谜团的一部分。
yield return 和 yield break 语句最常用于提供集合的“延迟评估”。
这意味着当您获取使用 yield return 的方法的值时,您尝试获取的东西的集合还不存在(它本质上是空的)。当您遍历它们(使用 foreach)时,它将在那时执行该方法并获取枚举中的下一个元素。
某些属性和方法会导致一次计算整个枚举(例如“Count”)。
这是返回集合和返回产量之间区别的一个简单示例:
string[] names = { "Joe", "Jim", "Sam", "Ed", "Sally" };
public IEnumerable<string> GetYieldEnumerable()
{
foreach (var name in names)
yield return name;
}
public IEnumerable<string> GetList()
{
var list = new List<string>();
foreach (var name in names)
list.Add(name);
return list;
}
// we're going to execute the GetYieldEnumerable() method
// but the foreach statement inside it isn't going to execute
var yieldNames = GetNamesEnumerable();
// now we're going to execute the GetList() method and
// the foreach method will execute
var listNames = GetList();
// now we want to look for a specific name in yieldNames.
// only the first two iterations of the foreach loop in the
// GetYieldEnumeration() method will need to be called to find it.
if (yieldNames.Contains("Jim")
Console.WriteLine("Found Jim and only had to loop twice!");
// now we'll look for a specific name in listNames.
// the entire names collection was already iterated over
// so we've already paid the initial cost of looping through that collection.
// now we're going to have to add two more loops to find it in the listNames
// collection.
if (listNames.Contains("Jim"))
Console.WriteLine("Found Jim and had to loop 7 times! (5 for names and 2 for listNames)");
如果您需要在源数据具有值之前获取对 Enumeration 的引用,也可以使用此方法。例如,如果名称集合开始时不完整:
string[] names = { "Joe", "Jim", "Sam", "Ed", "Sally" };
public IEnumerable<string> GetYieldEnumerable()
{
foreach (var name in names)
yield return name;
}
public IEnumerable<string> GetList()
{
var list = new List<string>();
foreach (var name in names)
list.Add(name);
return list;
}
var yieldNames = GetNamesEnumerable();
var listNames = GetList();
// now we'll change the source data by renaming "Jim" to "Jimbo"
names[1] = "Jimbo";
if (yieldNames.Contains("Jimbo")
Console.WriteLine("Found Jimbo!");
// Because this enumeration was evaluated completely before we changed "Jim"
// to "Jimbo" it isn't going to be found
if (listNames.Contains("Jimbo"))
// this can't be true
else
Console.WriteLine("Couldn't find Jimbo, because he wasn't there when I was evaluated.");
关键字yield
是编写. IEnumerator
例如:
public static IEnumerator<int> Range(int from, int to)
{
for (int i = from; i < to; i++)
{
yield return i;
}
}
由 C# 编译器转换为类似于:
public static IEnumerator<int> Range(int from, int to)
{
return new RangeEnumerator(from, to);
}
class RangeEnumerator : IEnumerator<int>
{
private int from, to, current;
public RangeEnumerator(int from, int to)
{
this.from = from;
this.to = to;
this.current = from;
}
public bool MoveNext()
{
this.current++;
return this.current < this.to;
}
public int Current
{
get
{
return this.current;
}
}
}
查看MSDN文档和示例。它本质上是一种在 C# 中创建迭代器的简单方法。
public class List
{
//using System.Collections;
public static IEnumerable Power(int number, int exponent)
{
int counter = 0;
int result = 1;
while (counter++ < exponent)
{
result = result * number;
yield return result;
}
}
static void Main()
{
// Display powers of 2 up to the exponent 8:
foreach (int i in Power(2, 8))
{
Console.Write("{0} ", i);
}
}
}
Eric White关于函数式编程的系列文章非常值得一读,但Yield上的条目与我所见的解释一样清晰。
我想出这个来克服 .NET 必须手动深度复制列表的缺点。
我用这个:
static public IEnumerable<SpotPlacement> CloneList(List<SpotPlacement> spotPlacements)
{
foreach (SpotPlacement sp in spotPlacements)
{
yield return (SpotPlacement)sp.Clone();
}
}
在另一个地方:
public object Clone()
{
OrderItem newOrderItem = new OrderItem();
...
newOrderItem._exactPlacements.AddRange(SpotPlacement.CloneList(_exactPlacements));
...
return newOrderItem;
}
我试图想出一个可以做到这一点的oneliner,但这是不可能的,因为 yield 在匿名方法块中不起作用。
编辑:
更好的是,使用通用列表克隆器:
class Utility<T> where T : ICloneable
{
static public IEnumerable<T> CloneList(List<T> tl)
{
foreach (T t in tl)
{
yield return (T)t.Clone();
}
}
}
让我补充一下。产量不是关键字。它仅在您使用“收益回报”时才有效,而不是像普通变量一样工作。
它用于从函数返回迭代器。您可以进一步搜索。我建议搜索“返回数组与迭代器”