C#中yield关键字的作用是什么?

1035

如何仅公开 IList <> 的片段问题中,其中一个答案包含以下代码片段:

IEnumerable<object> FilteredList()
{
    foreach(object item in FullList)
    {
        if(IsItemInPartialList(item))
            yield return item;
    }
}

yield关键字在这里有什么作用?我在一些地方看到它被提到,还有一个问题,但我没有完全弄清楚它实际上是做什么的。我习惯于将yield视为一个线程让给另一个线程,但这似乎与此处无关。


1
这里只是有关它的 MSDN 链接 http://msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx - NoWar
25
这并不令人意外。混淆源于我们习惯将“return”视为函数输出,但当它前面带有“yield”时,它并不是这样。 - Larry
20个回答

29
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);

下面是输出结果(没有意外):

开始
1
1后
2
2后
42
结束

如上所述,foreach 是一种语法糖:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

为了梳理这个问题,我创建了一个去除了抽象部分的序列图:

C# iterator block sequence diagram

编译器生成的状态机也实现了枚举器,但为了使序列图更加清晰,我将它们显示为单独的实例。(当状态机从另一个线程枚举时,您确实会获得单独的实例,但该细节在此处并不重要。)

每次调用迭代器块时,都会创建状态机的新实例。但是,在第一次执行 enumerator.MoveNext() 之前,迭代器块中的任何代码都不会被执行。这就是延迟执行的工作原理。以下是一个(相当愚蠢的)示例:

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

此时迭代器尚未执行。 Where 子句创建一个新的 IEnumerable<T>,该枚举包装了由 IteratorBlock 返回的 IEnumerable<T>,但是此可枚举对象尚未被枚举。当您执行 foreach 循环时,就会发生这种情况:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

如果你多次枚举可枚举对象,那么每次都会创建一个状态机的新实例,并且你的迭代器块将执行相同的代码两次。

请注意,像ToList()ToArray()First()Count()等LINQ方法将使用foreach循环来枚举可枚举对象。例如,ToList()将枚举可枚举对象的所有元素并将它们存储在一个列表中。现在,您可以访问该列表以获取可枚举对象的所有元素,而不需要再次执行迭代器块。当使用ToList()等方法时,使用CPU多次生成可枚举对象的元素和使用内存存储枚举的元素以便多次访问之间存在权衡。


26

关于Yield关键字的一个重要点是延迟执行。现在我所说的延迟执行是指需要时才执行。更好的方式是通过举例来说明。

例如:不使用Yield,即没有延迟执行。

public static IEnumerable<int> CreateCollectionWithList()
{
    var list =  new List<int>();
    list.Add(10);
    list.Add(0);
    list.Add(1);
    list.Add(2);
    list.Add(20);

    return list;
}

示例:使用Yield,即惰性执行。

public static IEnumerable<int> CreateCollectionWithYield()
{
    yield return 10;
    for (int i = 0; i < 3; i++) 
    {
        yield return i;
    }

    yield return 20;
}

现在,当我调用这两种方法时。

var listItems = CreateCollectionWithList();
var yieldedItems = CreateCollectionWithYield();

你会注意到listItems里面有5个项目(在调试时将鼠标悬停在listItems上)。 而yieldItems只是引用了该方法,而不是其中的项目。 这意味着它尚未执行获取方法内项目的过程。这是一种仅在需要时获取数据的非常有效的方法。 yield的实际实现可以在ORM(例如Entity Framework和NHibernate等)中看到。


查看所有答案后,这个答案告诉我yield是一种针对语言核心中不良设计的hack。在这种情况下,潜在问题是IEnumerable和foreach。此外,返回的是一个项目。这意味着如果需要每个项目,则会有大量额外的CPU开销。可能与一次性返回所有内容的效率差不多。更好的解决方案是在两者之间找到平衡。一次返回100-1000个项目(或任何“合理”的数量),枚举它们,返回下一个块等。SQL游标就是这样做的。 - CubicleSoft

17

C# 的yield关键字,简单来说,允许多次调用一个名为迭代器的代码体,该迭代器知道如何在完成之前返回,并在再次调用时从上次离开的地方继续 - 也就是帮助迭代器透明地针对迭代器返回的每个序列项成为有状态的。

在JavaScript中,相同的概念被称为生成器(Generators)。


1
迄今为止最好的解释。这些在Python中也是同样的生成器吗? - petrosmm

13

创建对象的可枚举性是一种非常简单易行的方法。编译器会创建一个类来包装您的方法,并实现IEnumerable<object>接口。如果没有使用yield关键字,您将需要创建一个实现IEnumerable<object>接口的对象。


7

它正在生成可枚举序列。它的实际作用是创建本地的可枚举序列并将其作为方法结果返回。


5
这个链接提供了一个简单的例子。
这里还有更简单的例子。
public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

请注意,使用“yield return”不会从方法中返回。甚至可以在“yield return”之后添加“WriteLine”。
以上代码生成了一个包含4个整数的IEnumerable,这些整数都是4。
以下代码包括一个“WriteLine”。它将向列表中添加4,打印abc,然后再向列表中添加4,最后完成该方法并从该方法中真正返回(当方法完成时,就像没有返回值的过程一样)。但是,该方法将有一个值,即在完成时返回的“IEnumerable”类型的整数列表。
public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

注意,使用yield时返回的类型与函数的类型不同。它是在IEnumerable列表中的元素类型。
你需要将方法的返回类型设为IEnumerable并使用yield。如果方法的返回类型是int或List<int>,并且你使用yield,则无法编译。你可以使用IEnumerable方法返回类型而不使用yield,但似乎不能没有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");
}

请注意,如果要理解SelectMany,它使用yield和泛型。以下示例可能有所帮助:public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { yield return t; }public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { return new List<TResult>(); } - barlop
看起来是非常好的解释!这本来可以成为被采纳的答案。 - Gsv
@pongapundit 谢谢,我的回答确实清晰简单,但我自己并没有多少使用 yield 的经验,其他回答者对它的使用和知识比我更丰富。我在这里写的关于 yield 的内容可能只是因为我在头脑中想着如何理解这里和那个 dotnetperls 链接中的一些答案!但由于我不太了解 yield return(除了我提到的简单事情之外),并且没有多少使用它和了解它的用途,我认为这不应该成为被接受的答案。 - barlop

3
现在你可以使用yield关键字来实现异步流。C# 8.0引入了异步流,用于模拟数据的流式来源。数据流通常会异步地检索或生成元素。异步流依赖于.NET Standard 2.1中引入的新接口。这些接口在.NET Core 3.0及更高版本中得到支持。它们为异步流数据源提供了自然的编程模型。来源:Microsoft docs。以下是示例。
using System;
using System.Collections.Generic;               
using System.Threading.Tasks;

public class Program
{
    public static async Task Main()
    {
        List<int> numbers = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        
        await foreach(int number in YieldReturnNumbers(numbers))
        {
            Console.WriteLine(number);
        }
    }
    
    public static async IAsyncEnumerable<int> YieldReturnNumbers(List<int> numbers) 
    {
        foreach (int number in numbers)
        {
            await Task.Delay(1000);
            yield return number;
        }
    }
}

-1
阅读了所有的帖子后,我为自己创造了“yield return”的概念。 首先,“return”退出函数并将结果返回给调用者。 其次,“yield”记住函数的状态(循环步骤或退出位置), 因此下一次函数调用会继续循环,省略初始化或已执行的语句。

-4

理解yield的简单演示

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp_demo_yield {
    class Program
    {
        static void Main(string[] args)
        {
            var letters = new List<string>() { "a1", "b1", "c2", "d2" };

            // Not yield
            var test1 = GetNotYield(letters);

            foreach (var t in test1)
            {
                Console.WriteLine(t);
            }

            // yield
            var test2 = GetWithYield(letters).ToList();

            foreach (var t in test2)
            {
                Console.WriteLine(t);
            }

            Console.ReadKey();
        }

        private static IList<string> GetNotYield(IList<string> list)
        {
            var temp = new List<string>();
            foreach(var x in list)
            {
                
                if (x.Contains("2")) { 
                temp.Add(x);
                }
            }

            return temp;
        }

        private static IEnumerable<string> GetWithYield(IList<string> list)
        {
            foreach (var x in list)
            {
                if (x.Contains("2"))
                {
                    yield return x;
                }
            }
        }
    } 
}

1
我认为需要一些解释来补充这个答案,以澄清你的意思。 - Milad Dastan Zand
1
我不喜欢这个例子,因为ToList会立即评估查询,从而破坏了使用'yield'的目的。 - undefined
我不喜欢这个例子,因为ToList会立即评估查询,从而破坏了使用"yield"的目的。 - Christo

-5

它试图引入一些 Ruby 的好东西 :)
概念:这是一些示例 Ruby 代码,可以打印出数组的每个元素

 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的响应所示。稍微不太优雅。

//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]; 
}

7
我认为这个回答听起来不太对。是的,C#中的yieldIEnumerable紧密相关,而C#缺乏Ruby中“块”的概念。但是,C#有lambda表达式,这样就可以实现一个类似于Ruby中的eachForEach方法。虽然如此,这并不意味着这样做是个好主意,参见这篇文章:http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx。 - rsenna
更好的做法是:public IEnumerable<int> Each() { int index = 0; yield return data[index++]; } - ata

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