前言
當我們撰寫 C# 代碼時,經常需要處理大量的資料集合,在傳統的方式中,我們往往需要先將整個資料集合加載到記憶體中,然后再進行操作,但是如果資料集合非常大,這種方式就會導致記憶體占用過高,甚至可能導致程式崩潰,
C# 中的yield return
機制可以幫助我們解決這個問題,通過使用yield return
,我們可以將資料集合按需生成,而不是一次性生成整個資料集合,這樣可以大大減少記憶體占用,并且提高程式的性能,
在本文中,我們將深入討論 C# 中yield return
的機制和用法,幫助您更好地理解這個強大的功能,并在實際開發中靈活使用它,
使用方式
上面我們提到了yield return
將資料集合按需生成,而不是一次性生成整個資料集合,接下來通過一個簡單的示例,我們看一下它的作業方式是什么樣的,以便加深對它的理解
foreach (var num in GetInts())
{
Console.WriteLine("外部遍歷了:{0}", num);
}
IEnumerable<int> GetInts()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("內部遍歷了:{0}", i);
yield return i;
}
}
首先,在GetInts
方法中,我們使用yield return
關鍵字來定義一個迭代器,這個迭代器可以按需生成整數序列,在每次回圈時,使用yield return
回傳當前的整數,通過1foreach
回圈來遍歷 GetInts
方法回傳的整數序列,在迭代時GetInts
方法會被執行,但是不會將整個序列加載到記憶體中,而是在需要時,按需生成序列中的每個元素,在每次迭代時,會輸出當前迭代的整數對應的資訊,所以輸出的結果為
內部遍歷了:0
外部遍歷了:0
內部遍歷了:1
外部遍歷了:1
內部遍歷了:2
外部遍歷了:2
內部遍歷了:3
外部遍歷了:3
內部遍歷了:4
外部遍歷了:4
可以看到,整數序列是按需生成的,并且在每次生成時都會輸出相應的資訊,這種方式可以大大減少記憶體占用,并且提高程式的性能,當然從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("內部遍歷了:{0}", i);
yield return i;
}
}
和上面不同的是,如果需要用異步的方式,我們需要回傳IAsyncEnumerable
型別,這種方式的執行結果和上面同步的方式執行的結果是一致的,我們就不做展示了,上面我們的示例都是基于回圈持續迭代的,其實使用yield return
的方式還可以按需的方式去輸出,這種方式適合靈活迭代的方式,如下示例所示
foreach (var num in GetInts())
{
Console.WriteLine("外部遍歷了:{0}", num);
}
IEnumerable<int> GetInts()
{
Console.WriteLine("內部遍歷了:0");
yield return 0;
Console.WriteLine("內部遍歷了:1");
yield return 1;
Console.WriteLine("內部遍歷了:2");
yield return 2;
}
foreach
回圈每次會呼叫GetInts()
方法,GetInts()
方法的內部便使用yield return
關鍵字回傳一個結果,每次遍歷都會去執行下一個yield return
,所以上面代碼輸出的結果是
內部遍歷了:0
外部遍歷了:0
內部遍歷了:1
外部遍歷了:1
內部遍歷了:2
外部遍歷了:2
探究本質
上面我們展示了yield return
如何使用的示例,它是一種延遲加載的機制,它可以讓我們逐個地處理資料,而不是一次性地將所有資料讀取到記憶體中,接下來我們就來探究一下神奇操作的背后到底是如何實作的,方便讓大家更清晰的了解迭代體系相關,
foreach本質
首先我們來看一下foreach
為什么可以遍歷,也就是如果可以被foreach
遍歷的物件,被遍歷的操作需要滿足哪些條件,這個時候我們可以反編譯工具來看一下編譯后的代碼是什么樣子的,相信大家最熟悉的就是List<T>
集合的遍歷方式了,那我們就用List<T>
的示例來演示一下
List<int> ints = new List<int>();
foreach(int item in ints)
{
Console.WriteLine(item);
}
上面的這段代碼很簡單,我們也沒有給它任何初始化的資料,這樣可以排除干擾,讓我們能更清晰的看到反編譯的結果,排除其他干擾,它反編譯后的代碼是這樣的
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#
反編譯網站sharplab.io,其中dnSpy
還可以除錯反編譯的代碼,
通過上面的反編譯之后的代碼我們可以看到foreach
會被編譯成一個固定的結構,也就是我們經常提及的設計模式中的迭代器模式結構
Enumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
}
通過這段固定的結構我們總結一下foreach
的作業原理
- 可以被
foreach
的物件需要要包含GetEnumerator()
方法 - 迭代器物件包含
MoveNext()
方法和Current
屬性 MoveNext()
方法回傳bool
型別,判斷是否可以繼續迭代,Current
屬性回傳當前的迭代結果,
我們可以看一下List<T>
類可迭代的原始碼結構是如何實作的
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
,他們兩個定義了可以實作迭代的能力抽象,實作方式如下
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
bool MoveNext();
object Current{ get; }
void Reset();
}
如果類實作IEnumerable
介面并實作了GetEnumerator()
方法便可以被foreach
,迭代的物件是IEnumerator
型別,包含一個MoveNext()
方法和Current
屬性,上面的介面是原始物件的方式,這種操作都是針對object
型別集合物件,我們實際開發程序中大多數都是使用的泛型集合,當然也有對應的實作方式,如下所示
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
new T Current{ get; }
}
可以被
foreach
迭代并不意味著一定要去實作IEnumerable
介面,這只是給我們提供了一個可以被迭代的抽象的能力,只要類中包含GetEnumerator()
方法并回傳一個迭代器,迭代器里包含回傳bool
型別的MoveNext()
方法和獲取當前迭代物件的Current
屬性即可,
yield return本質
上面我們看到了可以被foreach
迭代的本質是什么,那么yield return
的回傳值可以被IEnumerable<T>
接收說明其中必有蹊蹺,我們反編譯一下我們上面的示例看一下反編譯之后代碼,為了方便大家對比反編譯結果,這里我把上面的示例再次粘貼一下
foreach (var num in GetInts())
{
Console.WriteLine("外部遍歷了:{0}", num);
}
IEnumerable<int> GetInts()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("內部遍歷了:{0}", i);
yield return i;
}
}
它的反編譯結果,這里咱們就不全部展示了,只展示一下核心的邏輯
//foeach編譯后的結果
IEnumerator<int> enumerator = GetInts().GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int current = enumerator.Current;
Console.WriteLine("外部遍歷了:{0}", current);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
//GetInts方法編譯后的結果
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
本質是語法糖
,我們看一下<GetInts>d__1
類的實作
//生成的類即實作了IEnumerable介面也實作了IEnumerator介面
//說明它既包含了GetEnumerator()方法,也包含MoveNext()方法和Current屬性
private sealed class <>GetIntsd__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
private int <>1__state;
//當前迭代結果
private int <>2__current;
private int <>l__initialThreadId;
public C <>4__this;
private int <i>5__1;
//當前迭代到的結果
int IEnumerator<int>.Current
{
get{ return <>2__current; }
}
//當前迭代到的結果
object IEnumerator.Current
{
get{ return <>2__current; }
}
//建構式包含狀態欄位,變向說明靠狀態機去實作核心流程流轉
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;
}
//控制狀態
<>1__state = -1;
//自增 也就是代碼里回圈的i++
<i>5__1++;
}
else
{
<>1__state = -1;
<i>5__1 = 0;
}
//回圈終止條件 上面回圈里的i<5
if (<i>5__1 < 5)
{
Console.WriteLine("內部遍歷了:{0}", <i>5__1);
//把當前迭代結果賦值給Current屬性
<>2__current = <i>5__1;
<>1__state = 1;
//說明可以繼續迭代
return true;
}
//迭代結束
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
{
//給狀態機初始化
<GetInts>d__ = new <GetInts>d__1(0);
<GetInts>d__.<>4__this = <>4__this;
}
//因為<GetInts>d__1實作了IEnumerator介面所以可以直接回傳
return <GetInts>d__;
}
IEnumerator IEnumerable.GetEnumerator()
{
//因為<GetInts>d__1實作了IEnumerator介面所以可以直接轉換
return ((IEnumerable<int>)this).GetEnumerator();
}
void IEnumerator.Reset()
{
}
void IDisposable.Dispose()
{
}
}
通過它生成的類我們可以看到,該類即實作了IEnumerable
介面也實作了IEnumerator
介面說明它既包含了GetEnumerator()
方法,也包含MoveNext()
方法和Current
屬性,用這一個類就可以滿足可被foeach
迭代的核心結構,我們手動寫的for
代碼被包含到了MoveNext()
方法里,它包含了定義的狀態機制代碼,并且根據當前的狀態機代碼將迭代移動到下一個元素,我們大概講解一下我們的for
代碼被翻譯到MoveNext()
方法里的執行流程
- 首次迭代時
<>1__state
被初始化成0,代表首個被迭代的元素,這個時候Current
初始值為0,回圈控制變數<i>5__1
初始值也為0, - 判斷是否滿足終止條件,不滿足則執行回圈里的邏輯,并更改裝填機
<>1__state
為1,代表首次迭代執行完成, - 回圈控制變數
<i>5__1
繼續自增并更改并更改裝填機<>1__state
為-1,代表可持續迭代,并回圈執行回圈體的自定義邏輯, - 不滿足迭代條件則回傳
false
,也就是代表了MoveNext()
以不滿足迭代條件while (enumerator.MoveNext())
邏輯終止,
上面我們還展示了另一種yield return
的方式,就是同一個方法里包含多個yield return
的形式
IEnumerable<int> GetInts()
{
Console.WriteLine("內部遍歷了:0");
yield return 0;
Console.WriteLine("內部遍歷了:1");
yield return 1;
Console.WriteLine("內部遍歷了:2");
yield return 2;
}
上面這段代碼反編譯的結果如下所示,這里咱們只展示核心的方法MoveNext()
的實作
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
Console.WriteLine("內部遍歷了:0");
<>2__current = 0;
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
Console.WriteLine("內部遍歷了:1");
<>2__current = 1;
<>1__state = 2;
return true;
case 2:
<>1__state = -1;
Console.WriteLine("內部遍歷了: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
表示可以繼續迭代,
IAsyncEnumerable介面
上面我們展示了同步yield return
方式,c# 8
開始新增了IAsyncEnumerable<T>
介面,用于完成異步迭代,也就是迭代器邏輯里包含異步邏輯的場景,IAsyncEnumerable<T>
介面的實作代碼如下所示
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
上面,但是實際作用的則是每一次迭代執行的MoveNextAsync()
方法,可以大致理解為下面的作業方式
IAsyncEnumerator<int> enumerator = list.GetAsyncEnumerator();
while (enumerator.MoveNextAsync().GetAwaiter().GetResult())
{
var current = enumerator.Current;
}
當然,實際編譯成的代碼并不是這個樣子的,我們在之前的文章<研究c#異步操作async await狀態機的總結>一文中講解過async await
會被編譯成IAsyncStateMachine
異步狀態機,所以IAsyncEnumerator<T>
結合yield return
的實作比同步的方式更加復雜而且包含更多的代碼,不過實作原理可以結合同步的方式類比一下,但是要同時了解異步狀態機的實作,這里咱們就不過多展示異步yield return
的編譯后實作了,有興趣的同學可以自行了解一下,
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;
}
}
}
這個功能確實比較強大,滿足開放封閉原則,我們可以在不修改原始代碼的情況,增強代碼的功能,可以說是非常的實用,我們來看一下它的編譯后的結果是啥
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()
本質也是語法糖,會把擴展能力編譯成擴展類.GetEnumerator(被擴展實體)
的方式,也就是我們寫代碼時候的原始方式,只是編譯器幫我們生成了它的呼叫方式,接下來我們看一下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本質
這一節里講到的語法糖生成方式是一樣的了,同樣的編譯時候也是生成了一個對應類,這里的類是<GetEnumerator>d__0
,我們看一下該類的結構
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遍歷方式
//這里也被編譯成了實際生產方式
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
生成的代碼結構都是一樣的,只是MoveNext()
里的邏輯取決于我們寫代碼時候的具體邏輯,不同的邏輯生成不同的代碼,這里咱們就不在講解它生成的代碼了,因為和上面咱們講解的代碼邏輯是差不多的,
總結
通過本文我們介紹了c#
中的yield return
語法,并探討了由它帶來的一些思考,我們通過一些簡單的例子,展示了yield return
的使用方式,知道了迭代器來是如何按需處理大量資料,同時,我們通過分析foreach
迭代和yield return
語法的本質,講解了它們的實作原理和底層機制,好在涉及到的知識整體比較簡單,仔細閱讀相關實作代碼的話相信會了解背后的實作原理,這里就不過多贅述了,
當你遇到挑戰和困難時,請不要輕易放棄,無論你面對的是什么,只要你肯努力去嘗試,去探索,去追求,你一定能夠克服困難,走向成功,記住,成功不是一蹴而就的,它需要我們不斷努力和堅持,相信自己,相信自己的能力,相信自己的潛力,你一定能夠成為更好的自己,

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/553911.html
標籤:C#
上一篇:第二單元 常用快捷鍵,注釋,變數
下一篇:返回列表