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

在“ 如何仅显示IList <>的片段”问题中,答案之一具有以下代码片段:

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

yield关键字在那里做什么? 我已经在几个地方提到过它,还有另一个问题,但是我还没有弄清楚它的实际作用。 我习惯于从一个线程向另一个线程屈服的角度考虑yield,但这在这里似乎无关紧要。


#1楼

简而言之,C#yield关键字允许多次调用代码体(称为迭代器),该代码体知道在完成之前如何返回,并在再次调用时从中断处继续执行-即,它有助于迭代器迭代器在连续调用中返回的序列中,每个项目的状态变为透明。

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


#2楼

收益有两个重大用途,

  1. 它有助于提供自定义迭代,而无需创建临时集合。

  2. 它有助于进行有状态的迭代。 在此处输入图片说明

为了更说明性地解释上述两点,我创建了一个简单的视频,您可以在此处观看


#3楼

yield return与枚举器一起使用。 在yield语句的每次调用中,控制权都返回给调用者,但它可以确保保持被调用者的状态。 因此,当调用方枚举下一个元素时,它会在yield语句之后立即从该语句的被调用方方法中继续执行。

让我们尝试通过一个例子来理解这一点。 在此示例中,我已经与每一行相对应地提到了执行流程的顺序。

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()方法,则将为其重置状态。


#4楼

乍一看,收益回报是返回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运算符允许根据需要创建项目。 这是使用它的一个很好的理由。


#5楼

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

实际上,通常需要根据需要执行最少的工作量,以减少应用程序的资源消耗。

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

  • 由于记录数不会显着影响应用程序的资源需求,因此可伸缩性,可靠性和可预测性可能会得到改善。
  • 性能和响应速度很可能会提高,因为处理可以立即开始,而不必等待整个集合先加载。
  • 由于可以停止,启动,中断或失败应用程序,因此可恢复性和利用率可能会提高。 与仅实际使用一部分结果的预取所有数据相比,只有进行中的项目会丢失。
  • 在添加恒定工作负载流的环境中,可以进行连续处理 。

这是首先建立一个集合(例如一个列表)与使用yield之间的比较。

清单范例

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

控制台输出
ContactListStore:创建联系人1
ContactListStore:创建联系人2
ContactListStore:创建联系人3
准备遍历集合。

注意:整个集合被加载到内存中,甚至不需要列表中的单个项目

产量示例

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

控制台输出
准备遍历集合。

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

控制台输出
准备遍历集合
ContactYieldStore:创建联系人1
你好鲍勃

真好! 当客户从集合中“拉出”该项目时,仅构造了第一个联系人。


#6楼

链接有一个简单的例子

更简单的例子在这里

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

注意,收益率回报不会从方法中回报。 您甚至可以在yield return之后放一个WriteLine

上面产生的IEnumerable为4 int 4,4,4,4

这里有一个WriteLine 。 将添加4到列表中,打印abc,然后添加4到列表中,然后完成该方法,从而真正从该方法返回(一旦该方法完成,就像没有返回的过程一样)。 但这将具有一个值,即intIEnumerable列表,它会在完成时返回。

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

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

您可以将yield与方法的返回类型一起使用IEnumerable 。 如果该方法的返回类型为intList<int>并且您使用yield ,则它将不会编译。 您可以使用没有yield的IEnumerable方法返回类型,但似乎没有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");
}

#7楼

这是一种理解概念的简单方法:基本概念是,如果您希望可以使用“ foreach ”的集合,但是出于某些原因(例如从数据库中查询它们)将项目收集到集合中会很昂贵。 ,而且您通常不需要整个集合,然后创建一个函数,一次生成一个集合并将其交还给消费者(后者可以尽早终止收集工作)。

这样想:您去肉店买一磅切成薄片的火腿。 屠夫将10磅重的火腿放回去,放在切片机上,切成薄片,然后将一堆薄片带回来给你,并从中取出一磅。 (旧方法)。 有了yield ,屠夫将切片机带到柜台,开始切片并“ yield ”到切片上,直到量到1磅,然后为您包装好。 对于屠夫而言,“旧方式”可能更好(让他按照自己喜欢的方式组织机器),但在大多数情况下,“新方式”对于消费者来说效率更高。


#8楼

迭代。 它在“隐藏”状态下创建一个状态机,该状态机会记住您在该函数的每个其他循环中所处的位置,然后从那里开始。


#9楼

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


#10楼

它产生了不可计数的序列。 它实际上是在创建本地IEnumerable序列并将其作为方法结果返回


#11楼

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


#12楼

yield关键字实际上在这里做了很多工作。

该函数返回一个实现IEnumerable<object>接口的IEnumerable<object> 。 如果调用函数开始foreach此对象,则会再次调用该函数,直到“屈服”为止。 这是C#2.0中引入的语法糖。 在早期版本中,您必须创建自己的IEnumerableIEnumerator对象才能执行此类操作。

理解这样的代码最简单的方法是键入示例,设置一些断点,然后看看会发生什么。 尝试逐步执行此示例:

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

#13楼

最近,Raymond Chen也对yield关键字进行了一系列有趣的文章。

虽然通常用于轻松实现迭代器模式,但可以将其推广到状态机中。 引用Raymond毫无意义,最后一部分也链接到其他用途(但是Entin博客中的示例特别好,显示了如何编写异步安全代码)。


#14楼

它正在尝试带来一些Ruby Goodness :)
概念:这是一些示例Ruby代码,可打印出数组的每个元素

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with 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]; 
}

#15楼

yield关键字允许您以迭代器块上的形式创建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#迭代器块序列图

编译器生成的状态机也实现了枚举器,但是为了使图更清楚,我将它们显示为单独的实例。 (当从另一个线程枚举状态机时,您实际上会得到单独的实例,但是这里的细节并不重要。)

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

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多次生成可枚举的元素与存储用于存储枚举的元素以多次访问它们之间需要权衡。


#16楼

如果我正确理解这一点,那么从实现带有yield的IEnumerable的函数的角度来看,这就是我的措辞。

  • 这是一个
  • 如果您需要其他电话,请再次致电。
  • 我会记得我已经给你的。
  • 当您再次致电时,我只能告诉您是否能再给您另一个。

#17楼

关于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仅引用方法而不是项目。 这意味着它尚未执行在方法内部获取项目的过程。 仅在需要时获取数据的一种非常有效的方法。 产量的实际实现可以在ORM中看到,例如Entity Framework和NHibernate等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值