關(guān)于C#中yield?return用法的思考
前言
當(dāng)我們編寫 C# 代碼時,經(jīng)常需要處理大量的數(shù)據(jù)集合。在傳統(tǒng)的方式中,我們往往需要先將整個數(shù)據(jù)集合加載到內(nèi)存中,然后再進行操作。但是如果數(shù)據(jù)集合非常大,這種方式就會導(dǎo)致內(nèi)存占用過高,甚至可能導(dǎo)致程序崩潰。
C# 中的yield return
機制可以幫助我們解決這個問題。通過使用yield return
,我們可以將數(shù)據(jù)集合按需生成,而不是一次性生成整個數(shù)據(jù)集合。這樣可以大大減少內(nèi)存占用,并且提高程序的性能。
在本文中,我們將深入討論 C# 中yield return
的機制和用法,幫助您更好地理解這個強大的功能,并在實際開發(fā)中靈活使用它。
使用方式
上面我們提到了yield return
將數(shù)據(jù)集合按需生成,而不是一次性生成整個數(shù)據(jù)集合。接下來通過一個簡單的示例,我們看一下它的工作方式是什么樣的,以便加深對它的理解
foreach (var num in GetInts()) { Console.WriteLine("外部遍歷了:{0}", num); } IEnumerable<int> GetInts() { for (int i = 0; i < 5; i++) { Console.WriteLine("內(nèi)部遍歷了:{0}", i); yield return i; } }
首先,在GetInts
方法中,我們使用yield return
關(guān)鍵字來定義一個迭代器。這個迭代器可以按需生成整數(shù)序列。在每次循環(huán)時,使用yield return
返回當(dāng)前的整數(shù)。通過1foreach
循環(huán)來遍歷 GetInts
方法返回的整數(shù)序列。在迭代時GetInts
方法會被執(zhí)行,但是不會將整個序列加載到內(nèi)存中。而是在需要時,按需生成序列中的每個元素。在每次迭代時,會輸出當(dāng)前迭代的整數(shù)對應(yīng)的信息。所以輸出的結(jié)果為
內(nèi)部遍歷了:0
外部遍歷了:0
內(nèi)部遍歷了:1
外部遍歷了:1
內(nèi)部遍歷了:2
外部遍歷了:2
內(nèi)部遍歷了:3
外部遍歷了:3
內(nèi)部遍歷了:4
外部遍歷了:4
可以看到,整數(shù)序列是按需生成的,并且在每次生成時都會輸出相應(yīng)的信息。這種方式可以大大減少內(nèi)存占用,并且提高程序的性能。當(dāng)然從c# 8
開始異步迭代的方式同樣支持
await foreach (var num in GetIntsAsync()) { Console.WriteLine("外部遍歷了:{0}", num); } async IAsyncEnumerable<int> GetIntsAsync() { for (int i = 0; i < 5; i++) { await Task.Yield(); Console.WriteLine("內(nèi)部遍歷了:{0}", i); yield return i; } }
和上面不同的是,如果需要用異步的方式,我們需要返回IAsyncEnumerable
類型,這種方式的執(zhí)行結(jié)果和上面同步的方式執(zhí)行的結(jié)果是一致的,我們就不做展示了。上面我們的示例都是基于循環(huán)持續(xù)迭代的,其實使用yield return
的方式還可以按需的方式去輸出,這種方式適合靈活迭代的方式。如下示例所示
foreach (var num in GetInts()) { Console.WriteLine("外部遍歷了:{0}", num); } IEnumerable<int> GetInts() { Console.WriteLine("內(nèi)部遍歷了:0"); yield return 0; Console.WriteLine("內(nèi)部遍歷了:1"); yield return 1; Console.WriteLine("內(nèi)部遍歷了:2"); yield return 2; }
foreach
循環(huán)每次會調(diào)用GetInts()
方法,GetInts()
方法的內(nèi)部便使用yield return
關(guān)鍵字返回一個結(jié)果。每次遍歷都會去執(zhí)行下一個yield return
。所以上面代碼輸出的結(jié)果是
內(nèi)部遍歷了:0
外部遍歷了:0
內(nèi)部遍歷了:1
外部遍歷了:1
內(nèi)部遍歷了:2
外部遍歷了:2
探究本質(zhì)
上面我們展示了yield return
如何使用的示例,它是一種延遲加載的機制,它可以讓我們逐個地處理數(shù)據(jù),而不是一次性地將所有數(shù)據(jù)讀取到內(nèi)存中。接下來我們就來探究一下神奇操作的背后到底是如何實現(xiàn)的,方便讓大家更清晰的了解迭代體系相關(guān)。
foreach本質(zhì)
首先我們來看一下foreach
為什么可以遍歷,也就是如果可以被foreach
遍歷的對象,被遍歷的操作需要滿足哪些條件,這個時候我們可以反編譯工具來看一下編譯后的代碼是什么樣子的,相信大家最熟悉的就是List<T>
集合的遍歷方式了,那我們就用List<T>
的示例來演示一下
List<int> ints = new List<int>(); foreach(int item in ints) { Console.WriteLine(item); }
上面的這段代碼很簡單,我們也沒有給它任何初始化的數(shù)據(jù),這樣可以排除干擾,讓我們能更清晰的看到反編譯的結(jié)果,排除其他干擾。它反編譯后的代碼是這樣的
List<int> list = new List<int>(); List<int>.Enumerator enumerator = list.GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; Console.WriteLine(current); } } finally { ((IDisposable)enumerator).Dispose(); }
可以反編譯代碼的工具有很多,我用的比較多的一般是ILSpy
、dnSpy
、dotPeek
和在線c#
反編譯網(wǎng)站sharplab.io,其中dnSpy
還可以調(diào)試反編譯的代碼。
通過上面的反編譯之后的代碼我們可以看到foreach
會被編譯成一個固定的結(jié)構(gòu),也就是我們經(jīng)常提及的設(shè)計模式中的迭代器模式結(jié)構(gòu)
Enumerator enumerator = list.GetEnumerator(); while (enumerator.MoveNext()) { var current = enumerator.Current; }
通過這段固定的結(jié)構(gòu)我們總結(jié)一下foreach
的工作原理
- 可以被
foreach
的對象需要要包含GetEnumerator()
方法 - 迭代器對象包含
MoveNext()
方法和Current
屬性 MoveNext()
方法返回bool
類型,判斷是否可以繼續(xù)迭代。Current
屬性返回當(dāng)前的迭代結(jié)果。
我們可以看一下List<T>
類可迭代的源碼結(jié)構(gòu)是如何實現(xiàn)的
public class List<T> : IList<T>, IList, IReadOnlyList<T> { public Enumerator GetEnumerator() => new Enumerator(this); IEnumerator<T> IEnumerable<T>.GetEnumerator() => Count == 0 ? SZGenericArrayEnumerator<T>.Empty : GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<T>)this).GetEnumerator(); public struct Enumerator : IEnumerator<T>, IEnumerator { public T Current => _current!; public bool MoveNext() { } } }
這里涉及到了兩個核心的接口IEnumerable<
和IEnumerator
,他們兩個定義了可以實現(xiàn)迭代的能力抽象,實現(xiàn)方式如下
public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { bool MoveNext(); object Current{ get; } void Reset(); }
如果類實現(xiàn)IEnumerable
接口并實現(xiàn)了GetEnumerator()
方法便可以被foreach
,迭代的對象是IEnumerator
類型,包含一個MoveNext()
方法和Current
屬性。上面的接口是原始對象的方式,這種操作都是針對object
類型集合對象。我們實際開發(fā)過程中大多數(shù)都是使用的泛型集合,當(dāng)然也有對應(yīng)的實現(xiàn)方式,如下所示
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { new T Current{ get; } }
可以被foreach
迭代并不意味著一定要去實現(xiàn)IEnumerable
接口,這只是給我們提供了一個可以被迭代的抽象的能力。只要類中包含GetEnumerator()
方法并返回一個迭代器,迭代器里包含返回bool
類型的MoveNext()
方法和獲取當(dāng)前迭代對象的Current
屬性即可。
yield return本質(zhì)
上面我們看到了可以被foreach
迭代的本質(zhì)是什么,那么yield return
的返回值可以被IEnumerable<T>
接收說明其中必有蹊蹺,我們反編譯一下我們上面的示例看一下反編譯之后代碼,為了方便大家對比反編譯結(jié)果,這里我把上面的示例再次粘貼一下
foreach (var num in GetInts()) { Console.WriteLine("外部遍歷了:{0}", num); } IEnumerable<int> GetInts() { for (int i = 0; i < 5; i++) { Console.WriteLine("內(nèi)部遍歷了:{0}", i); yield return i; } }
它的反編譯結(jié)果,這里咱們就不全部展示了,只展示一下核心的邏輯
//foeach編譯后的結(jié)果 IEnumerator<int> enumerator = GetInts().GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; Console.WriteLine("外部遍歷了:{0}", current); } } finally { if (enumerator != null) { enumerator.Dispose(); } } //GetInts方法編譯后的結(jié)果 private IEnumerable<int> GetInts() { <GetInts>d__1 <GetInts>d__ = new <GetInts>d__1(-2); <GetInts>d__.<>4__this = this; return <GetInts>d__; }
這里我們可以看到GetInts()
方法里原來的代碼不見了,而是多了一個<GetInts>d__1
l類型,也就是說yield return
本質(zhì)是語法糖
。我們看一下<GetInts>d__1
類的實現(xiàn)
//生成的類即實現(xiàn)了IEnumerable接口也實現(xiàn)了IEnumerator接口 //說明它既包含了GetEnumerator()方法,也包含MoveNext()方法和Current屬性 private sealed class <>GetIntsd__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable { private int <>1__state; //當(dāng)前迭代結(jié)果 private int <>2__current; private int <>l__initialThreadId; public C <>4__this; private int <i>5__1; //當(dāng)前迭代到的結(jié)果 int IEnumerator<int>.Current { get{ return <>2__current; } } //當(dāng)前迭代到的結(jié)果 object IEnumerator.Current { get{ return <>2__current; } } //構(gòu)造函數(shù)包含狀態(tài)字段,變向說明靠狀態(tài)機去實現(xiàn)核心流程流轉(zhuǎn) public <GetInts>d__1(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } //核心方法MoveNext private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } //控制狀態(tài) <>1__state = -1; //自增 也就是代碼里循環(huán)的i++ <i>5__1++; } else { <>1__state = -1; <i>5__1 = 0; } //循環(huán)終止條件 上面循環(huán)里的i<5 if (<i>5__1 < 5) { Console.WriteLine("內(nèi)部遍歷了:{0}", <i>5__1); //把當(dāng)前迭代結(jié)果賦值給Current屬性 <>2__current = <i>5__1; <>1__state = 1; //說明可以繼續(xù)迭代 return true; } //迭代結(jié)束 return false; } //IEnumerator的MoveNext方法 bool IEnumerator.MoveNext() { return this.MoveNext(); } //IEnumerable的IEnumerable方法 IEnumerator<int> IEnumerable<int>.IEnumerable() { //實例化<GetInts>d__1實例 <GetInts>d__1 <GetInts>d__; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; <GetInts>d__ = this; } else { //給狀態(tài)機初始化 <GetInts>d__ = new <GetInts>d__1(0); <GetInts>d__.<>4__this = <>4__this; } //因為<GetInts>d__1實現(xiàn)了IEnumerator接口所以可以直接返回 return <GetInts>d__; } IEnumerator IEnumerable.GetEnumerator() { //因為<GetInts>d__1實現(xiàn)了IEnumerator接口所以可以直接轉(zhuǎn)換 return ((IEnumerable<int>)this).GetEnumerator(); } void IEnumerator.Reset() { } void IDisposable.Dispose() { } }
通過它生成的類我們可以看到,該類即實現(xiàn)了IEnumerable
接口也實現(xiàn)了IEnumerator
接口說明它既包含了GetEnumerator()
方法,也包含MoveNext()
方法和Current
屬性。用這一個類就可以滿足可被foeach
迭代的核心結(jié)構(gòu)。我們手動寫的for
代碼被包含到了MoveNext()
方法里,它包含了定義的狀態(tài)機制代碼,并且根據(jù)當(dāng)前的狀態(tài)機代碼將迭代移動到下一個元素。我們大概講解一下我們的for
代碼被翻譯到MoveNext()
方法里的執(zhí)行流程
- 首次迭代時
<>1__state
被初始化成0,代表首個被迭代的元素,這個時候Current
初始值為0,循環(huán)控制變量<i>5__1
初始值也為0。 - 判斷是否滿足終止條件,不滿足則執(zhí)行循環(huán)里的邏輯。并更改裝填機
<>1__state
為1,代表首次迭代執(zhí)行完成。 - 循環(huán)控制變量
<i>5__1
繼續(xù)自增并更改并更改裝填機<>1__state
為-1,代表可持續(xù)迭代。并循環(huán)執(zhí)行循環(huán)體的自定義邏輯。 - 不滿足迭代條件則返回
false
,也就是代表了MoveNext()
以不滿足迭代條件while (enumerator.MoveNext())
邏輯終止。
上面我們還展示了另一種yield return
的方式,就是同一個方法里包含多個yield return
的形式
IEnumerable<int> GetInts() { Console.WriteLine("內(nèi)部遍歷了:0"); yield return 0; Console.WriteLine("內(nèi)部遍歷了:1"); yield return 1; Console.WriteLine("內(nèi)部遍歷了:2"); yield return 2; }
上面這段代碼反編譯的結(jié)果如下所示,這里咱們只展示核心的方法MoveNext()
的實現(xiàn)
private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; Console.WriteLine("內(nèi)部遍歷了:0"); <>2__current = 0; <>1__state = 1; return true; case 1: <>1__state = -1; Console.WriteLine("內(nèi)部遍歷了:1"); <>2__current = 1; <>1__state = 2; return true; case 2: <>1__state = -1; Console.WriteLine("內(nèi)部遍歷了:2"); <>2__current = 2; <>1__state = 3; return true; case 3: <>1__state = -1; return false; } }
通過編譯后的代碼我們可以看到,多個yield return
的形式會被編譯成switch...case
的形式,有幾個yield return
則會編譯成n+1
個case
,多出來的一個case
則代表的MoveNext()
終止條件,也就是返回false
的條件。其它的case
則返回true
表示可以繼續(xù)迭代。
IAsyncEnumerable接口
上面我們展示了同步yield return
方式,c# 8
開始新增了IAsyncEnumerable<T>
接口,用于完成異步迭代,也就是迭代器邏輯里包含異步邏輯的場景。IAsyncEnumerable<T>
接口的實現(xiàn)代碼如下所示
public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { ValueTask<bool> MoveNextAsync(); T Current { get; } }
它最大的不同則是同步的IEnumerator
包含的是MoveNext()
方法返回的是bool
,IAsyncEnumerator
接口包含的是MoveNextAsync()
異步方法,返回的是ValueTask<bool>
類型。所以上面的示例代碼
await foreach (var num in GetIntsAsync()) { Console.WriteLine("外部遍歷了:{0}", num); }
所以這里的await
雖然是加在foreach
上面,但是實際作用的則是每一次迭代執(zhí)行的MoveNextAsync()
方法??梢源笾吕斫鉃橄旅娴墓ぷ鞣绞?/p>
IAsyncEnumerator<int> enumerator = list.GetAsyncEnumerator(); while (enumerator.MoveNextAsync().GetAwaiter().GetResult()) { var current = enumerator.Current; }
當(dāng)然,實際編譯成的代碼并不是這個樣子的,我們在之前的文章<研究c#異步操作async await狀態(tài)機的總結(jié)>一文中講解過async await
會被編譯成IAsyncStateMachine
異步狀態(tài)機,所以IAsyncEnumerator<T>
結(jié)合yield return
的實現(xiàn)比同步的方式更加復(fù)雜而且包含更多的代碼,不過實現(xiàn)原理可以結(jié)合同步的方式類比一下,但是要同時了解異步狀態(tài)機的實現(xiàn),這里咱們就不過多展示異步y(tǒng)ield return
的編譯后實現(xiàn)了,有興趣的同學(xué)可以自行了解一下。
foreach增強
c# 9
增加了對foreach的增強的功能,即通過擴展方法的形式,對原本具備包含foreach
能力的對象增加GetEnumerator()
方法,使得普通類在不具備foreach
的能力的情況下也可以使用來迭代。它的使用方式如下
Foo foo = new Foo(); foreach (int item in foo) { Console.WriteLine(item); } public class Foo { public List<int> Ints { get; set; } = new List<int>(); } public static class Bar { //給Foo定義擴展方法 public static IEnumerator<int> GetEnumerator(this Foo foo) { foreach (int item in foo.Ints) { yield return item; } } }
這個功能確實比較強大,滿足開放封閉原則,我們可以在不修改原始代碼的情況,增強代碼的功能,可以說是非常的實用。我們來看一下它的編譯后的結(jié)果是啥
Foo foo = new Foo(); IEnumerator<int> enumerator = Bar.GetEnumerator(foo); try { while (enumerator.MoveNext()) { int current = enumerator.Current; Console.WriteLine(current); } } finally { if (enumerator != null) { enumerator.Dispose(); } }
這里我們看到擴展方法GetEnumerator()
本質(zhì)也是語法糖,會把擴展能力編譯成擴展類.GetEnumerator(被擴展實例)
的方式。也就是我們寫代碼時候的原始方式,只是編譯器幫我們生成了它的調(diào)用方式。接下來我們看一下GetEnumerator()
擴展方法編譯成了什么
public static IEnumerator<int> GetEnumerator(Foo foo) { <GetEnumerator>d__0 <GetEnumerator>d__ = new <GetEnumerator>d__0(0); <GetEnumerator>d__.foo = foo; return <GetEnumerator>d__; }
看到這個代碼是不是覺得很眼熟了,不錯和上面yield return本質(zhì)
這一節(jié)里講到的語法糖生成方式是一樣的了,同樣的編譯時候也是生成了一個對應(yīng)類,這里的類是<GetEnumerator>d__0
,我們看一下該類的結(jié)構(gòu)
private sealed class <GetEnumerator>d__0 : IEnumerator<int>, IEnumerator, IDisposable { private int <>1__state; private int <>2__current; public Foo foo; private List<int>.Enumerator <>s__1; private int <item>5__2; int IEnumerator<int>.Current { get{ return <>2__current; } } object IEnumerator.Current { get{ return <>2__current; } } public <GetEnumerator>d__0(int <>1__state) { this.<>1__state = <>1__state; } private bool MoveNext() { try { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -3; } else { <>1__state = -1; //因為示例中的Ints我們使用的是List<T> <>s__1 = foo.Ints.GetEnumerator(); <>1__state = -3; } //因為上面的擴展方法里使用的是foreach遍歷方式 //這里也被編譯成了實際生產(chǎn)方式 if (<>s__1.MoveNext()) { <item>5__2 = <>s__1.Current; <>2__current = <item>5__2; <>1__state = 1; return true; } <>m__Finally1(); <>s__1 = default(List<int>.Enumerator); return false; } catch { ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { return this.MoveNext(); } void IDisposable.Dispose() { } void IEnumerator.Reset() { } private void <>m__Finally1() { } }
看到編譯器生成的代碼,我們可以看到yield return
生成的代碼結(jié)構(gòu)都是一樣的,只是MoveNext()
里的邏輯取決于我們寫代碼時候的具體邏輯,不同的邏輯生成不同的代碼。這里咱們就不在講解它生成的代碼了,因為和上面咱們講解的代碼邏輯是差不多的。
總結(jié)
通過本文我們介紹了c#
中的yield return
語法,并探討了由它帶來的一些思考。我們通過一些簡單的例子,展示了yield return
的使用方式,知道了迭代器來是如何按需處理大量數(shù)據(jù)。同時,我們通過分析foreach
迭代和yield return
語法的本質(zhì),講解了它們的實現(xiàn)原理和底層機制。好在涉及到的知識整體比較簡單,仔細(xì)閱讀相關(guān)實現(xiàn)代碼的話相信會了解背后的實現(xiàn)原理,這里就不過多贅述了。
到此這篇關(guān)于關(guān)于C#中yield return用法的思考的文章就介紹到這了,更多相關(guān)C# yield return內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實現(xiàn)Post數(shù)據(jù)或文件到指定的服務(wù)器進行接收
這篇文章主要為大家詳細(xì)介紹了如何通過C#實現(xiàn)Post數(shù)據(jù)或文件到指定的服務(wù)器進行接收,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考下2024-03-03C#?CefSharp?根據(jù)輸入日期段自動選擇日期的操作代碼
這篇文章主要介紹了C#?CefSharp?根據(jù)輸入日期段自動選擇日期的操作代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01測試框架nunit之a(chǎn)ssertion斷言使用詳解
NUnit是.Net平臺的測試框架,廣泛用于.Net平臺的單元測試和回歸測試中,下面我們用示例詳細(xì)學(xué)習(xí)一下他的使用方法2014-01-01