在C#中使用了哪些关键字?

我怎么能只暴露IList的一个片段<>问题中,其中一个答案有以下代码片段:

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

产量关键字在那里做什么?我看到它在几个地方被引用,还有一个问题,但我还没有完全弄清楚它到底做了什么。我习惯于从一个线程屈服于另一个线程的角度来思考产量,但这在这里似乎并不相关。

457279 次浏览

迭代。它创建了一个“隐藏”的状态机,它会记住你在函数的每个额外周期中的位置,并从那里开始。

这是为您的对象创建可枚举对象的一种非常简单易行的方法。编译器创建一个包装您的方法并实现(在本例中为IENumable<对象>)的类。如果没有产量关键字,您必须创建一个实现IE的对象。

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

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

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

该函数返回一个实现IEnumerable<object>接口的对象。如果调用函数在此对象上开始foreach ing,则再次调用该函数,直到它“屈服”。这是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,第3行不再执行。

下面是一个真实的例子:

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

最近,Raymond Chen也发表了一系列有趣的文章,内容是关于产量关键字的。

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

它试图引入一些红宝石善良:)
这是一些示例Ruby代码,它打印出数组的每个元素

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

数组的each方法实现产生控制给调用者(“放x”),数组的每个元素整齐地呈现为x。然后调用者可以对x做任何需要做的事情。

然而,.净额并没有在这里一直走下去… C#似乎已经将产量与IENumable耦合在一起,在某种程度上迫使您在调用者中编写一个foreach循环,如Mendelt的响应所示。有点不那么优雅。

//calling codeforeach(int i in obCustomClass.Each()){Console.WriteLine(i.ToString());}
// CustomClass implementationprivate 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];}

简单地说,C#产量关键字允许对代码主体进行多次调用,称为迭代器,它知道如何在完成之前返回,并且当再次调用时,继续它离开的地方-即它帮助迭代器在连续调用中返回的序列中的每个项目变得透明有状态。

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

产量有两个伟大的用途,

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

  2. 它有助于进行有状态的迭代。输入图片描述

为了更直观地解释以上两点,我创建了一个简单的视频,您可以观看这里

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

让我们试着用一个例子来理解这一点。在这个例子中,对应于我提到的每一行的执行顺序。

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, 9int newFib = prevFib + currFib;//6prevFib = currFib;//7currFib = newFib;//8}}

此外,每个枚举都会保持状态。假设,我有另一个对Fibs()方法的调用,那么它的状态将被重置。

乍一看,收益率回报是一个. NET糖返回一个可数

如果不产生,则会一次创建集合的所有项:

class SomeData{public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas(){return new List<SomeData> {new SomeData(),new SomeData(),new SomeData()};}}

同样的代码,它会逐项返回:

class SomeData{public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas(){yield return new SomeData();yield return new SomeData();yield return new SomeData();}}

使用产量的好处是,如果使用您的数据的函数只需要集合的第一项,则不会创建其余项。

产量运算符允许根据需要创建项目。这是使用它的一个很好的理由。

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

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

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

  • 可扩展性、可靠性和可预测性可能会有所改善,因为记录的数量不会显着影响应用程序的资源需求。
  • 性能和响应能力可能会有所改善,因为处理可以立即开始,而不是等待首先加载整个集合。
  • 回收和利用可能会有所改善,因为应用程序可以停止、启动、中断或失败。与预取所有数据相比,只有正在进行的项目会丢失,在那里只使用了一部分结果。
  • 持续处理中在添加恒定工作负载流的环境中是可能的。

以下是先构建集合(例如列表)与使用产量之间的比较。

列表示例

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

控制台输出
创建联系人列表
创建联系人2
创建联系人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();}

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

注意:集合根本没有执行。这是由于IENumable的“延迟执行”性质。构造项目只会在真正需要时发生。

让我们再次调用集合,并在获取集合中的第一个联系人时改变行为。

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

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

不错!只有当客户端从集合中“拉出”项目时才构建第一个联系人。

这个链接有一个简单的例子

更简单的例子在这里

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

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

上面产生了一个4 int 4,4,4,4的IENumable

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

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

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

如果方法的返回类型是intList<int>,而你使用的是yield,那么它将无法编译。你可以使用IEnumerable方法返回类型而不使用产量,但似乎你不能使用没有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");}

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

这样想:你去肉类柜台,想买一磅切片火腿。屠夫把10磅重的火腿拿到后面,放在切片机上,把整块切片,然后把一堆切片带回来给你,量出一磅。(老方法)。使用yield,屠夫将切片机带到柜台,开始切片并将每个切片“产生”到秤上,直到它测量1磅,然后为您包装,然后完成。

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

这是输出(这里没有惊喜):

Begin1After 12After 242End

如上所述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()第一次执行。这就是延迟执行的工作原理。这是一个(相当愚蠢的)例子:

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多次生成可枚举的元素和使用内存存储枚举元素以多次访问它们之间存在权衡。

如果我正确理解了这一点,以下是我如何从函数的角度来表达这一点。

  • 这里有一个。
  • 如果你需要另一个,再打电话。
  • 我会记住我已经给你的东西。
  • 我只知道如果我能给你另一个当你再打电话。

一个主要的观点是懒惰执行。现在我所说的懒惰执行是在需要时执行。更好的说法是举个例子

示例:不使用产量,即没有懒惰执行。

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

示例:使用收益即懒惰执行。

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上)。而产生项将只具有对方法的引用,而不是项。这意味着它没有执行在方法中获取项目的过程。仅在需要时获取数据的非常有效的方法。收益率的实际实现可以在实体框架和NHibernate等ORM中看到。

了解产量的简单演示

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 yieldvar test1 = GetNotYield(letters);
foreach (var t in test1){Console.WriteLine(t);}
// yieldvar 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;}}}}}

现在,您可以将yield关键字用于异步流。

C#8.0引入了异步流,它为数据源的流建模。数据流通常异步检索或生成元素。异步流依赖于. NET Standard 2.1中引入的新接口。这些接口在. NET Core 3.0及更高版本中得到支持。它们为异步流数据源提供了自然的编程模型。

来源:microsoftdocs

下面的例子

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