C# yield关键字在Nerd Dinner教程中的有趣用法

14

我在学习一个教程(专业ASP.NET MVC - Nerd Dinner)时,遇到了这段代码:

public IEnumerable<RuleViolation> GetRuleViolations() {
    if (String.IsNullOrEmpty(Title))
        yield return new RuleViolation("Title required", "Title");
    if (String.IsNullOrEmpty(Description))
        yield return new RuleViolation("Description required","Description");
    if (String.IsNullOrEmpty(HostedBy))
        yield return new RuleViolation("HostedBy required", "HostedBy");
    if (String.IsNullOrEmpty(Address))
        yield return new RuleViolation("Address required", "Address");
    if (String.IsNullOrEmpty(Country))
        yield return new RuleViolation("Country required", "Country");
    if (String.IsNullOrEmpty(ContactPhone))
        yield return new RuleViolation("Phone# required", "ContactPhone");
    if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
        yield return new RuleViolation("Phone# does not match country", "ContactPhone");
    yield break;
}

我已经研究了yield,但我觉得我的理解还有点模糊。它似乎创造了一个对象,允许在不实际执行循环的情况下循环遍历集合中的项目,除非绝对必要。

然而,这个例子对我来说有些奇怪。我认为它所做的是推迟创建任何RuleViolation实例,直到程序员使用foreach或类似于.ElementAt(2)的LINQ扩展方法请求特定项的集合。

除此之外,我有一些问题:

  1. 当条件语句的部分被评估时?当调用GetRuleViolations()或当枚举实际迭代时?换句话说,如果在我调用GetRuleViolations()和我尝试实际迭代之间,Title的值从null更改为Really Geeky Dinner,那么是否会创建RuleViolation("Title required", "Title")

  2. yield break;为什么是必需的?它在这里真正做了什么?

  3. 假设Title为空或为null。如果我调用GetRuleViolations()然后连续两次迭代生成的可枚举对象,new RuleViolation("Title required", "Title")会被调用多少次?


3
.Net编译器将这种语法糖转换为更加复杂的形式。编译示例,然后在反射器中加载IL代码。您应该能够准确理解正在发生的事情。 - Hamish Grubijan
3个回答

18
一个包含yield命令的函数与一个普通函数的处理方式不同。当调用该函数时,发生的事情是构造了一个特定IEnumerable类型的匿名类型,并且该函数创建了一个该类型的对象并返回它。匿名类包含逻辑,每次调用IEnumerable.MoveNext时都会执行函数体直到下一个yield命令。有点误导人,函数体不像普通函数一样一次性执行完毕,而是分成片段,每个片段在枚举器向前移动一步时执行。
关于您的问题:
  1. 正如我所说,每当迭代到下一个元素时,执行每个if
  2. yield break在上面的例子中确实是不必要的。它的作用是终止枚举。
  3. 每次迭代枚举时,都会强制再次执行代码。在相关行上设置断点并进行测试。

2
小小的澄清 - 在您说“IEnumerable[<T>]”的几个地方,您是指“IEnumerator[<T>]”。 - Marc Gravell
“空集合”是指没有任何if语句被评估为真,还是表示如果我尚未添加任何if语句到我的方法中,则需要使用yield break作为占位符?(或者两者都是?) - devuxer
@Aviad,即使方法体中没有if语句,这也是正确的吗? - devuxer
2
好的,但显然,private IEnumerable<string> Enumerate() { }会让你得到一个“不是所有的代码路径都返回值”的错误。因此,我认为这使我得出以下结论:如果您的方法中没有其他返回语句(yield或其他),则需要yield break作为占位符,或者在函数体内部到达其自然结束之前“在某个地方停止枚举时使用”。 - devuxer
是的,函数体必须包含关键字 yield 才能使其工作。否则编译器会将其视为普通函数(请再次阅读我的回答中的第一句话 :-) ) - Aviad P.
显示剩余3条评论

6

1)以这个较简单的例子为例:

public void Enumerate()
{
    foreach (var item in EnumerateItems())
    {
        Console.WriteLine(item);
    }
}

public IEnumerable<string> EnumerateItems()
{
    yield return "item1";
    yield return "item2";
    yield break;
}

每次从IEnumerator中调用MoveNext()时,代码都会从yield点返回并移动到下一行可执行的代码。
2) yield break;将告诉IEnumerator没有更多要枚举的内容。
3)每次枚举一次。
使用yield break;
public IEnumerable<string> EnumerateUntilEmpty()
{
    foreach (var name in nameList)
    {
        if (String.IsNullOrEmpty(name)) yield break;
        yield return name;
    }     
}

你确定是“3)once”吗?据我所知,这会每次重新评估(请参见此处的其他两个答案)。 - Benjamin Podszun
如果你创建了一个列表,为什么不直接迭代它呢?使用 yield 的方式可以避免显式地构造一个列表。例如,在你的情况下: public IEnumerable<string> Enumerate() { yield return "item1"; yield return "item2"; } - Aviad P.
谢谢,这实现了我最初想展示的内容,就是他示例的简单版本。 - ChaosPandion
在你的第一个例子中,“yield break”也是不必要的。但在你的第二个例子中,它当然是必须的。 - Aviad P.

2

简短版:

1:yield是一个神奇的“停止并稍后返回”关键字,因此在“active”之前的if语句已经被评估过了。

2:yield break明确地结束枚举(类似于switch case中的break)

3:每次都是如此。当然,你可以将其转换为List并在之后迭代,以便缓存结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接