首页 文章

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

提问于
浏览
692

How Can I Expose Only a Fragment of IList<>问题中,其中一个答案包含以下代码段:

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

yield关键字有什么作用?我已经看到它在几个地方被引用,另外一个问题,但我还没弄清楚它实际上做了什么 . 我习惯于在一个线程产生另一个线程的意义上考虑收益率,但这似乎并不重要 .

16 回答

  • 4

    yield关键字实际上在这里做了很多 . 该函数返回一个实现IEnumerable接口的对象 . 如果调用函数开始对该对象进行预处理,则再次调用该函数,直到它“产生”为止 . 这是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);
                    }
                }
            }
        }
    }
    
  • 6

    迭代 . 它创建了一个“在幕后”的状态机,它可以记住你在函数的每个附加周期中的位置并从中获取 .

  • 125

    产量有两大用途,

    • 它有助于在不创建临时集合的情况下提供自定义迭代 .

    • 有助于进行有状态迭代 .
      enter image description here

    为了更具说明性地解释上述两点,我创建了一个简单的视频,你可以观看它here

  • -2

    最近Raymond Chen还在yield关键字上发表了一系列有趣的文章 .

    虽然它名义上用于轻松实现迭代器模式,但可以推广到状态机 . 没有必要引用Raymond,最后一部分也链接到其他用途(但Entin的博客中的示例是esp好,显示如何编写异步安全代码) .

  • 21

    乍一看,yield return是一个返回IEnumerable的.NET糖 .

    如果没有产量,则立即创建集合的所有项目:

    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操作符允许根据需要创建项目 . 这是使用它的一个很好的理由 .

  • 29

    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() 方法,然后状态将被重置 .

  • 10

    直观地说,关键字从函数返回一个值而不离开它,即在你的代码示例中它返回当前的 item 值然后恢复循环 . 更正式地说,编译器使用它来为迭代器生成代码 . 迭代器是返回 IEnumerable 对象的函数 . MSDN有几个关于它们的articles .

  • 56

    列表或数组实现立即加载所有项,而yield实现提供延迟执行解决方案 .

    在实践中,通常希望根据需要执行最少量的工作以减少应用程序的资源消耗 .

    例如,我们可能有一个处理来自数据库的数百万条记录的应用程序 . 当我们在延迟执行基于拉的模型中使用IEnumerable时,可以实现以下好处:

    • Scalability, reliability and predictability 可能会有所改善,因为记录数量不会显着影响应用程序的资源需求 .

    • Performance and responsiveness 可能会有所改进,因为处理可以立即开始,而不是等待首先加载整个集合 .
      由于应用程序可以停止,启动,中断或失败,因此

    • Recoverability and utilisation 可能会有所改进 . 与预取所有仅使用部分结果的数据相比,只有正在进行的项目将丢失 .
      在添加常量工作负载流的环境中,可以使用

    • Continuous processing .

    下面是构建集合的第一个例如列表与使用yield之间的比较 .

    List Example

    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();
        }
    

    Console Output
    ContactListStore:创建联系人1
    ContactListStore:创建联系人2
    ContactListStore:创建联系人3
    准备好迭代整个系列 .

    注意:整个集合已加载到内存中甚至没有要求列表中的单个项目

    Yield Example

    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();
    }
    

    Console Output
    准备好迭代整个系列 .

    注意:集合根本没有执行 . 这是由于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();
    }
    

    Console Output
    准备好迭代整个系列
    ContactYieldStore:创建联系人1
    鲍勃你好

    太好了!当客户端将项目“拉出”集合时,仅构建了第一个联系人 .

  • 335

    这是一个理解这个概念的简单方法:基本思想是,如果你想要一个可以使用“ foreach ”的集合,但是由于某些原因(例如从数据库中查询它们)将项目收集到集合中是昂贵的,并且您通常不需要整个集合,然后您创建一个函数,一次构建一个项目集合并将其返回给消费者(然后可以提前终止集合工作) .

    Think of it this way: 你去肉类柜台,想要买一磅火腿 . 屠夫把一个10磅重的火腿放在后面,把它放在切片机上,切成整片,然后将一堆切片带回给你,然后测出一磅 . (旧方式) . 使用 yield ,屠夫将切片机带到计数器,然后开始切片并将每个切片切割到刻度上,直到它测量到1磅,然后为你包装它就完成了 . 对于屠夫而言,旧方式可能更好(让他按照自己喜欢的方式组织他的机器),但对于消费者而言,新方式在大多数情况下显然更有效 .

  • 30

    yield 关键字允许您在iterator block上的表单中创建 IEnumerable<T> . 这个迭代器块支持延迟执行,如果你不熟悉这个概念,它可能看起来几乎是神奇的 . 但是,在一天结束时,它只是执行代码而没有任何奇怪的技巧 .

    迭代器块可以被描述为语法糖,其中编译器生成状态机,该状态机跟踪可枚举的枚举进展的程度 . 要枚举可枚举,您经常使用 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();
    }
    

    为了解开这个问题,我创建了一个删除了抽象的序列图:

    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() 将枚举可枚举的所有元素并将它们存储在列表中 . 您现在可以访问列表以获取可枚举的所有元素,而无需再次执行迭代器块 . 在使用CPU生成多次可枚举元素和使用 ToList() 等方法存储枚举元素以多次访问它们之间需要进行权衡 .

  • 2

    简单地说,C#yield关键字允许对一个代码体(称为迭代器)的多次调用,它知道如何在它完成之前返回,并且当再次调用时,继续它停止的地方 - 即它有助于迭代器对迭代器在连续调用中返回的序列中的每个项目变为透明状态 .

    在JavaScript中,相同的概念称为生成器 .

  • 613

    这是为对象创建可枚举的一种非常简单易用的方法 . 编译器创建一个包装您的方法的类,并在这种情况下实现IEnumerable <object> . 如果没有yield关键字,则必须创建一个实现IEnumerable <object>的对象 .

  • 6

    如果我理解正确的话,这就是我如何从使用yield实现IEnumerable的函数的角度来说明这一点 .

    • 这是一个 .

    • 如果您需要另一个,请再次致电 .

    • 我会记得我已经给你的东西了 .

    • 我只会在你再打电话时才知道我能不能给你另一个 .

  • 166

    它产生了可枚举的序列 . 它的作用实际上是创建本地IEnumerable序列并将其作为方法结果返回

  • 16

    这个link有一个简单的例子

    这里有更简单的例子

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

    请注意,yield return不会从方法返回 . 你甚至可以在 yield return 之后放一个 WriteLine

    以上产生了4个int 4,4,4,4的IEnumerable

    这里有 WriteLine . 将列表添加4,打印abc,然后将4添加到列表中,然后完成方法,从而真正从方法返回(一旦方法完成,就像没有返回的过程一样) . 但是这将有一个 intint 列表,它在完成后返回 .

    public static IEnumerable<int> testYieldb()
    {
        yield return 4;
        console.WriteLine("abc");
        yield return 4;
    }
    

    另请注意,当您使用yield时,返回的内容与函数的类型不同 . 它是 IEnumerable 列表中元素的类型 .

    您将yield与方法的返回类型一起使用为 IEnumerable . 如果方法的返回类型是 intList<int> 并且您使用 yield ,则它将无法编译 . 你可以使用没有yield的 IEnumerable 方法返回类型,但似乎你可以不使用yield而不使用 IEnumerable 方法返回类型 .

    为了让它执行,你必须以特殊的方式调用它 .

    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");
    }
    
  • 11

    它试图引入一些Ruby Goodness :)
    Concept: 这是一些示例Ruby代码,用于打印出数组的每个元素

    rubyArray = [1,2,3,4,5,6,7,8,9,10]
        rubyArray.each{|x| 
            puts x   # do whatever with x
        }
    

    Array的每个方法实现 yields 控制到调用者('puts x'),数组的 each 元素整齐地表示为x . 然后,调用者可以执行x所需的任何操作 .

    但是 .Net 没有't go all the way here.. C# seems to have coupled yield with IEnumerable, in a way forcing you to write a foreach loop in the caller as seen in 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]; 
    }
    

相关问题