詳解C#中yield關(guān)鍵字的用法
〇、前言
yield 關(guān)鍵字的用途是把指令推遲到程序?qū)嶋H需要的時(shí)候再執(zhí)行,這個(gè)特性允許我們更細(xì)致地控制集合每個(gè)元素產(chǎn)生的時(shí)機(jī)。
對(duì)于一些大型集合,加載起來(lái)比較耗時(shí),此時(shí)最好是先返回一個(gè)來(lái)讓系統(tǒng)持續(xù)展示目標(biāo)內(nèi)容。類似于在餐館吃飯,肯定是做好一個(gè)菜就上桌了,而不會(huì)全部的菜都做好一起上。
另外還有一個(gè)好處是,可以提高內(nèi)存使用效率。當(dāng)我們有一個(gè)方法要返回一個(gè)集合時(shí),而作為方法的實(shí)現(xiàn)者我們并不清楚方法調(diào)用者具體在什么時(shí)候要使用該集合數(shù)據(jù)。如果我們不使用 yield 關(guān)鍵字,則意味著需要把集合數(shù)據(jù)裝載到內(nèi)存中等待被使用,這可能導(dǎo)致數(shù)據(jù)在內(nèi)存中占用較長(zhǎng)的時(shí)間。
下面就一起來(lái)看下怎么用 yield 關(guān)鍵字吧。
一、yield 關(guān)鍵字的使用
1.1 yield return:在迭代中一個(gè)一個(gè)返回待處理的值
如下示例,循環(huán)輸出小于 9 的偶數(shù),并記錄執(zhí)行任務(wù)的線程 ID:
class Program
{
static async Task Main(string[] args)
{
foreach (int i in ProduceEvenNumbers(9))
{
ConsoleExt.Write($"{i}-Main");
}
ConsoleExt.Write($"--Main-循環(huán)結(jié)束");
Console.ReadLine();
}
static IEnumerable<int> ProduceEvenNumbers(int upto)
{
for (int i = 0; i <= upto; i += 2)
{
ConsoleExt.Write($"{i}-ProduceEvenNumbers");
yield return i;
ConsoleExt.Write($"{i}-ProduceEvenNumbers-yielded");
}
ConsoleExt.Write($"--ProduceEvenNumbers-循環(huán)結(jié)束");
}
}
public static class ConsoleExt
{
public static void Write(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static void WriteLine(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
}
}輸出結(jié)果如下,可見(jiàn)整個(gè)循環(huán)是單線程運(yùn)行,ProduceEvenNumbers()生產(chǎn)一個(gè),然后Main()就操作一個(gè),Main() 執(zhí)行一次操作后,線程返回生產(chǎn)線,繼續(xù)沿著 return 往后執(zhí)行;生產(chǎn)線循環(huán)結(jié)束后,Main() 也接著結(jié)束:

1.2 yield break:標(biāo)識(shí)迭代中斷
如下示例代碼,通過(guò)條件中斷循環(huán):
class Program
{
static void Main()
{
ConsoleExt.Write(string.Join(" ", TakeWhilePositive(new[] { 2, 3, 4, 5, -1, 3, 4 })));
ConsoleExt.Write(string.Join(" ", TakeWhilePositive(new[] { 9, 8, 7 })));
Console.ReadLine();
}
static IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
{
foreach (int n in numbers)
{
if (n > 0) // 遇到負(fù)數(shù)就中斷循環(huán)
{
yield return n;
}
else
{
yield break;
}
}
}
}
public static class ConsoleExt
{
public static void Write(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static void WriteLine(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
}
}輸出結(jié)果,第一個(gè)數(shù)組中第五個(gè)數(shù)為負(fù)數(shù),因此至此就中斷循環(huán),包括它自己之后的數(shù)字不再返回:

1.3 返回類型為 IAsyncEnumerable<T> 的異步迭代器
實(shí)際上,不僅可以像前邊示例中那樣返回類型為 IEnumerable<T>,還可以使用 IAsyncEnumerable<T> 作為迭代器的返回類型,使得迭代器支持異步。
如下示例代碼,使用 await foreach 語(yǔ)句對(duì)迭代器的結(jié)果進(jìn)行異步迭代:(關(guān)于 await foreach 還有另外一個(gè)示例可參考 3.2 await foreach() 示例)
class Program
{
public static async Task Main()
{
await foreach (int n in GenerateNumbersAsync(5))
{
ConsoleExt.Write(n);
}
Console.ReadLine();
}
static async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
for (int i = 0; i < count; i++)
{
yield return await ProduceNumberAsync(i);
}
}
static async Task<int> ProduceNumberAsync(int seed)
{
await Task.Delay(1000);
return 2 * seed;
}
}
public static class ConsoleExt
{
public static void Write(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static void WriteLine(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
}
}輸出結(jié)果如下,可見(jiàn)輸出的結(jié)果有不同線程執(zhí)行:

1.4 迭代器的返回類型可以是 IEnumerator<T> 或 IEnumerator
以下示例代碼,通過(guò)實(shí)現(xiàn) IEnumerable<T> 接口、GetEnumerator 方法,返回類型為 IEnumerator<T>,來(lái)展現(xiàn) yield 關(guān)鍵字的一個(gè)用法:
class Program
{
public static void Main()
{
var ints = new int[] { 1, 2, 3 };
var enumerable = new MyEnumerable<int>(ints);
foreach (var item in enumerable)
{
Console.WriteLine(item);
}
Console.ReadLine();
}
}
public class MyEnumerable<T> : IEnumerable<T>
{
private T[] items;
public MyEnumerable(T[] ts)
{
this.items = ts;
}
public void Add(T item)
{
int num = this.items.Length;
this.items[num + 1] = item;
}
public IEnumerator<T> GetEnumerator()
{
foreach (var item in this.items)
{
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}1.5 不能使用 yield 的情況
1.yield return 不能套在 try-catch 中;
2.yield break 不能放在 finally 中;

3.yield 不能用在帶有 in、ref 或 out 參數(shù)的方法;
4.yield 不能用在 Lambda 表達(dá)式和匿名方法;
5.yield 不能用在包含不安全的塊(unsafe)的方法。
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/yield
二、使用 yield 關(guān)鍵字實(shí)現(xiàn)惰性枚舉
在 C# 中,可以使用 yield 關(guān)鍵字來(lái)實(shí)現(xiàn)惰性枚舉。惰性枚舉是指在使用枚舉值時(shí),只有在真正需要時(shí)才會(huì)生成它們,這可以提高程序的性能,因?yàn)樵诓恍枰褂妹杜e值時(shí),它們不會(huì)被生成或存儲(chǔ)在內(nèi)存中。
當(dāng)然對(duì)于簡(jiǎn)單的枚舉,實(shí)際上還沒(méi)普通的 List<T> 有優(yōu)勢(shì),因?yàn)槿∶杜e值也會(huì)對(duì)性能有損耗,所以只針對(duì)處理大型集合或延遲加載數(shù)據(jù)才能看到效果。
下面是一個(gè)簡(jiǎn)單示例,展示了如何使用 yield 關(guān)鍵字來(lái)實(shí)現(xiàn)惰性枚舉:
public static IEnumerable<int> enumerableFuc()
{
yield return 1;
yield return 2;
yield return 3;
}
// 使用惰性枚舉
foreach (var number in enumerableFuc())
{
Console.WriteLine(number);
}在上面的示例中,GetNumbers() 方法通過(guò)yield關(guān)鍵字返回一個(gè) IEnumerable 對(duì)象。當(dāng)我們使用 foreach 循環(huán)迭代這個(gè)對(duì)象時(shí),每次循環(huán)都會(huì)調(diào)用 MoveNext() 方法,并執(zhí)行到下一個(gè) yield 語(yǔ)句處,返回一個(gè)元素。這樣就實(shí)現(xiàn)了按需生成枚舉的元素,而不需要一次性生成所有元素。
三、通過(guò) IL 代碼看 yield 的原理
類比上一章節(jié)的示例代碼,用 while 循環(huán)代替 foreach 循環(huán),發(fā)現(xiàn)我們雖然沒(méi)有實(shí)現(xiàn) GetEnumerator(),也沒(méi)有實(shí)現(xiàn)對(duì)應(yīng)的 IEnumerator 的 MoveNext() 和 Current 屬性,但是我們?nèi)匀荒苷J褂眠@些函數(shù)。
static async Task Main(string[] args)
{
// 用 while (enumerator.MoveNext())
// 代替 foreach(int item in enumerableFuc())
IEnumerator<int> enumerator = enumerableFuc().GetEnumerator();
while (enumerator.MoveNext())
{
int current = enumerator.Current;
Console.WriteLine(current);
}
Console.ReadLine();
}
// 一個(gè)返回類型為 IEnumerable<int>,其中包含三個(gè) yield return
public static IEnumerable<int> enumerableFuc()
{
Console.WriteLine("enumerableFuc-yield 1");
yield return 1;
Console.WriteLine("enumerableFuc-yield 2");
yield return 2;
Console.WriteLine("enumerableFuc-yield 3");
yield return 3;
}輸出的結(jié)果:

下面試著簡(jiǎn)單看一下 Program 類的源碼
源碼如下,除了明顯的 Main() 和 enumerableFuc() 兩個(gè)函數(shù)外,反編譯的時(shí)候自動(dòng)生成了一個(gè)新的類 '<enumerableFuc>d__1'。
注:反編譯時(shí),語(yǔ)言選擇:“IL with C#”,有助于理解。

然后看自動(dòng)生成的類的實(shí)現(xiàn),發(fā)現(xiàn)它繼承了 IEnumerable、IEnumerable<T>、IEnumerator、IEnumerator<T>,也實(shí)現(xiàn)了MoveNext()、Reset()、GetEnumerator()、Current 屬性,這時(shí)我們應(yīng)該可以確認(rèn),這個(gè)新的類,就是我們雖然沒(méi)有實(shí)現(xiàn)對(duì)應(yīng)的 IEnumerator 的 MoveNext() 和 Current 屬性,但是我們?nèi)匀荒苷J褂眠@些函數(shù)的原因了。


然后再具體看下 MoveNext() 函數(shù),根據(jù)輸出的備注字段,也能清晰的看到迭代過(guò)程,下圖中紫色部分:

下邊是是第三、四次迭代,可以看到行標(biāo)識(shí)可以對(duì)得上:

每次調(diào)用 MoveNext() 函數(shù)都會(huì)將“ <>1__state”加 1,一共進(jìn)行了 4 次迭代,前三次返回 true,最后一次返回 false,代表迭代結(jié)束。這四次迭代對(duì)應(yīng)被 3 個(gè) yield return 語(yǔ)句分成4部分的 enumberableFuc() 中的語(yǔ)句。
用 enumberableFuc() 來(lái)進(jìn)行迭代的真實(shí)流程就是:
- 運(yùn)行 enumberableFuc() 函數(shù),獲取代碼自動(dòng)生成的類的實(shí)例;
- 接著調(diào)用 GetEnumberator() 函數(shù),將獲取的類自己作為迭代器,準(zhǔn)備開(kāi)始迭代;
- 每次運(yùn)行 MoveNext() “ <>1__state”增加 1,通過(guò) switch 語(yǔ)句可以讓每次調(diào)用 MoveNext() 的時(shí)候執(zhí)行不同部分的代碼;
- MoveNext() 返回 false,結(jié)束迭代。
這也就說(shuō)明了,yield 關(guān)鍵字其實(shí)是一種語(yǔ)法糖,最終還是通過(guò)實(shí)現(xiàn) IEnumberable<T>、IEnumberable、IEnumberator<T>、IEnumberator 接口實(shí)現(xiàn)的迭代功能。
到此這篇關(guān)于詳解C#中yield關(guān)鍵字的用法的文章就介紹到這了,更多相關(guān)C# yield內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# WebService發(fā)布以及IIS發(fā)布
這篇文章主要介紹了C# WebService發(fā)布以及IIS發(fā)布的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-07-07
C#中使用@聲明變量示例(逐字標(biāo)識(shí)符)
這篇文章主要介紹了C#中使用@聲明變量示例(逐字標(biāo)識(shí)符)在C#中,@符號(hào)不僅可以加在字符串常量之前,使字符串不作轉(zhuǎn)義之用,還可以加在變量名之前,使變量名與關(guān)鍵字不沖突,這種用法稱為“逐字標(biāo)識(shí)符”,需要的朋友可以參考下2015-06-06
c#通過(guò)app.manifest使程序以管理員身份運(yùn)行
通常我們使用c#編寫(xiě)的程序不會(huì)彈出這個(gè)提示,也就無(wú)法以管理員身分運(yùn)行。微軟的操作系統(tǒng)使用微軟的產(chǎn)品方法當(dāng)然是有的,通過(guò)app.manifest配置可以使程序打開(kāi)的時(shí)候,彈出UAC提示需要得到允許才可以繼續(xù),這樣就獲得了管理員的權(quán)限來(lái)執(zhí)行程序2015-01-01
C#中this用法系列(二) 通過(guò)this修飾符為原始類型擴(kuò)展方法
定義一個(gè)靜態(tài)類,類中定義靜態(tài)方法,方法中參數(shù)類型前邊加上this修飾符,即可實(shí)現(xiàn)對(duì)參數(shù)類型的方法擴(kuò)展,下面通過(guò)實(shí)例代碼給大家介紹下,需要的朋友參考下吧2016-12-12
C#使用ToUpper()與ToLower()方法將字符串進(jìn)行大小寫(xiě)轉(zhuǎn)換的方法
這篇文章主要介紹了C#使用ToUpper()與ToLower()方法將字符串進(jìn)行大小寫(xiě)轉(zhuǎn)換的方法,實(shí)例分析了C#大小寫(xiě)轉(zhuǎn)換的相關(guān)技巧,需要的朋友可以參考下2015-04-04
C#爬取動(dòng)態(tài)網(wǎng)頁(yè)上信息得流程步驟
動(dòng)態(tài)內(nèi)容網(wǎng)站使用 JavaScript 腳本動(dòng)態(tài)檢索和渲染數(shù)據(jù),爬取信息時(shí)需要模擬瀏覽器行為,否則獲取到的源碼基本是空的,這篇文章主要給大家詳細(xì)介紹了C#爬取動(dòng)態(tài)網(wǎng)頁(yè)上信息得流程步驟,需要的朋友可以參考下2024-10-10
c#動(dòng)態(tài)改變webservice的url訪問(wèn)地址
這篇文章主要介紹了c#動(dòng)態(tài)改變webservice的url訪問(wèn)地址,需要的朋友可以參考下2014-03-03

