關(guān)于C#中yield?return用法的思考
前言
當(dāng)我們編寫 C# 代碼時(shí),經(jīng)常需要處理大量的數(shù)據(jù)集合。在傳統(tǒng)的方式中,我們往往需要先將整個(gè)數(shù)據(jù)集合加載到內(nèi)存中,然后再進(jìn)行操作。但是如果數(shù)據(jù)集合非常大,這種方式就會導(dǎo)致內(nèi)存占用過高,甚至可能導(dǎo)致程序崩潰。
C# 中的yield return機(jī)制可以幫助我們解決這個(gè)問題。通過使用yield return,我們可以將數(shù)據(jù)集合按需生成,而不是一次性生成整個(gè)數(shù)據(jù)集合。這樣可以大大減少內(nèi)存占用,并且提高程序的性能。
在本文中,我們將深入討論 C# 中yield return的機(jī)制和用法,幫助您更好地理解這個(gè)強(qiáng)大的功能,并在實(shí)際開發(fā)中靈活使用它。
使用方式
上面我們提到了yield return將數(shù)據(jù)集合按需生成,而不是一次性生成整個(gè)數(shù)據(jù)集合。接下來通過一個(gè)簡單的示例,我們看一下它的工作方式是什么樣的,以便加深對它的理解
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)鍵字來定義一個(gè)迭代器。這個(gè)迭代器可以按需生成整數(shù)序列。在每次循環(huán)時(shí),使用yield return返回當(dāng)前的整數(shù)。通過1foreach循環(huán)來遍歷 GetInts方法返回的整數(shù)序列。在迭代時(shí)GetInts方法會被執(zhí)行,但是不會將整個(gè)序列加載到內(nèi)存中。而是在需要時(shí),按需生成序列中的每個(gè)元素。在每次迭代時(shí),會輸出當(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ù)序列是按需生成的,并且在每次生成時(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ù)迭代的,其實(shí)使用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)鍵字返回一個(gè)結(jié)果。每次遍歷都會去執(zhí)行下一個(gè)yield return。所以上面代碼輸出的結(jié)果是
內(nèi)部遍歷了:0
外部遍歷了:0
內(nèi)部遍歷了:1
外部遍歷了:1
內(nèi)部遍歷了:2
外部遍歷了:2
探究本質(zhì)
上面我們展示了yield return如何使用的示例,它是一種延遲加載的機(jī)制,它可以讓我們逐個(gè)地處理數(shù)據(jù),而不是一次性地將所有數(shù)據(jù)讀取到內(nèi)存中。接下來我們就來探究一下神奇操作的背后到底是如何實(shí)現(xiàn)的,方便讓大家更清晰的了解迭代體系相關(guān)。
foreach本質(zhì)
首先我們來看一下foreach為什么可以遍歷,也就是如果可以被foreach遍歷的對象,被遍歷的操作需要滿足哪些條件,這個(gè)時(shí)候我們可以反編譯工具來看一下編譯后的代碼是什么樣子的,相信大家最熟悉的就是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會被編譯成一個(gè)固定的結(jié)構(gòu),也就是我們經(jīng)常提及的設(shè)計(jì)模式中的迭代器模式結(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)是如何實(shí)現(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()
{
}
}
}這里涉及到了兩個(gè)核心的接口IEnumerable<和IEnumerator,他們兩個(gè)定義了可以實(shí)現(xiàn)迭代的能力抽象,實(shí)現(xiàn)方式如下
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
bool MoveNext();
object Current{ get; }
void Reset();
}如果類實(shí)現(xiàn)IEnumerable接口并實(shí)現(xiàn)了GetEnumerator()方法便可以被foreach,迭代的對象是IEnumerator類型,包含一個(gè)MoveNext()方法和Current屬性。上面的接口是原始對象的方式,這種操作都是針對object類型集合對象。我們實(shí)際開發(fā)過程中大多數(shù)都是使用的泛型集合,當(dāng)然也有對應(yīng)的實(shí)現(xiàn)方式,如下所示
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
new T Current{ get; }
}可以被foreach迭代并不意味著一定要去實(shí)現(xiàn)IEnumerable接口,這只是給我們提供了一個(gè)可以被迭代的抽象的能力。只要類中包含GetEnumerator()方法并返回一個(gè)迭代器,迭代器里包含返回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()方法里原來的代碼不見了,而是多了一個(gè)<GetInts>d__1l類型,也就是說yield return本質(zhì)是語法糖。我們看一下<GetInts>d__1類的實(shí)現(xiàn)
//生成的類即實(shí)現(xiàn)了IEnumerable接口也實(shí)現(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)機(jī)去實(shí)現(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()
{
//實(shí)例化<GetInts>d__1實(shí)例
<GetInts>d__1 <GetInts>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<GetInts>d__ = this;
}
else
{
//給狀態(tài)機(jī)初始化
<GetInts>d__ = new <GetInts>d__1(0);
<GetInts>d__.<>4__this = <>4__this;
}
//因?yàn)?lt;GetInts>d__1實(shí)現(xiàn)了IEnumerator接口所以可以直接返回
return <GetInts>d__;
}
IEnumerator IEnumerable.GetEnumerator()
{
//因?yàn)?lt;GetInts>d__1實(shí)現(xiàn)了IEnumerator接口所以可以直接轉(zhuǎn)換
return ((IEnumerable<int>)this).GetEnumerator();
}
void IEnumerator.Reset()
{
}
void IDisposable.Dispose()
{
}
}通過它生成的類我們可以看到,該類即實(shí)現(xiàn)了IEnumerable接口也實(shí)現(xiàn)了IEnumerator接口說明它既包含了GetEnumerator()方法,也包含MoveNext()方法和Current屬性。用這一個(gè)類就可以滿足可被foeach迭代的核心結(jié)構(gòu)。我們手動(dòng)寫的for代碼被包含到了MoveNext()方法里,它包含了定義的狀態(tài)機(jī)制代碼,并且根據(jù)當(dāng)前的狀態(tài)機(jī)代碼將迭代移動(dòng)到下一個(gè)元素。我們大概講解一下我們的for代碼被翻譯到MoveNext()方法里的執(zhí)行流程
- 首次迭代時(shí)
<>1__state被初始化成0,代表首個(gè)被迭代的元素,這個(gè)時(shí)候Current初始值為0,循環(huán)控制變量<i>5__1初始值也為0。 - 判斷是否滿足終止條件,不滿足則執(zhí)行循環(huán)里的邏輯。并更改裝填機(jī)
<>1__state為1,代表首次迭代執(zhí)行完成。 - 循環(huán)控制變量
<i>5__1繼續(xù)自增并更改并更改裝填機(jī)<>1__state為-1,代表可持續(xù)迭代。并循環(huán)執(zhí)行循環(huán)體的自定義邏輯。 - 不滿足迭代條件則返回
false,也就是代表了MoveNext()以不滿足迭代條件while (enumerator.MoveNext())邏輯終止。
上面我們還展示了另一種yield return的方式,就是同一個(gè)方法里包含多個(gè)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()的實(shí)現(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;
}
}通過編譯后的代碼我們可以看到,多個(gè)yield return的形式會被編譯成switch...case的形式,有幾個(gè)yield return則會編譯成n+1個(gè)case,多出來的一個(gè)case則代表的MoveNext()終止條件,也就是返回false的條件。其它的case則返回true表示可以繼續(xù)迭代。
IAsyncEnumerable接口
上面我們展示了同步yield return方式,c# 8開始新增了IAsyncEnumerable<T>接口,用于完成異步迭代,也就是迭代器邏輯里包含異步邏輯的場景。IAsyncEnumerable<T>接口的實(shí)現(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上面,但是實(shí)際作用的則是每一次迭代執(zhí)行的MoveNextAsync()方法??梢源笾吕斫鉃橄旅娴墓ぷ鞣绞?/p>
IAsyncEnumerator<int> enumerator = list.GetAsyncEnumerator();
while (enumerator.MoveNextAsync().GetAwaiter().GetResult())
{
var current = enumerator.Current;
}當(dāng)然,實(shí)際編譯成的代碼并不是這個(gè)樣子的,我們在之前的文章<研究c#異步操作async await狀態(tài)機(jī)的總結(jié)>一文中講解過async await會被編譯成IAsyncStateMachine異步狀態(tài)機(jī),所以IAsyncEnumerator<T>結(jié)合yield return的實(shí)現(xiàn)比同步的方式更加復(fù)雜而且包含更多的代碼,不過實(shí)現(xiàn)原理可以結(jié)合同步的方式類比一下,但是要同時(shí)了解異步狀態(tài)機(jī)的實(shí)現(xiàn),這里咱們就不過多展示異步y(tǒng)ield return的編譯后實(shí)現(xiàn)了,有興趣的同學(xué)可以自行了解一下。
foreach增強(qiáng)
c# 9增加了對foreach的增強(qiáng)的功能,即通過擴(kuò)展方法的形式,對原本具備包含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定義擴(kuò)展方法
public static IEnumerator<int> GetEnumerator(this Foo foo)
{
foreach (int item in foo.Ints)
{
yield return item;
}
}
}這個(gè)功能確實(shí)比較強(qiáng)大,滿足開放封閉原則,我們可以在不修改原始代碼的情況,增強(qiáng)代碼的功能,可以說是非常的實(shí)用。我們來看一下它的編譯后的結(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();
}
}這里我們看到擴(kuò)展方法GetEnumerator()本質(zhì)也是語法糖,會把擴(kuò)展能力編譯成擴(kuò)展類.GetEnumerator(被擴(kuò)展實(shí)例)的方式。也就是我們寫代碼時(shí)候的原始方式,只是編譯器幫我們生成了它的調(diào)用方式。接下來我們看一下GetEnumerator()擴(kuò)展方法編譯成了什么
public static IEnumerator<int> GetEnumerator(Foo foo)
{
<GetEnumerator>d__0 <GetEnumerator>d__ = new <GetEnumerator>d__0(0);
<GetEnumerator>d__.foo = foo;
return <GetEnumerator>d__;
}看到這個(gè)代碼是不是覺得很眼熟了,不錯(cuò)和上面yield return本質(zhì)這一節(jié)里講到的語法糖生成方式是一樣的了,同樣的編譯時(shí)候也是生成了一個(gè)對應(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;
//因?yàn)槭纠械腎nts我們使用的是List<T>
<>s__1 = foo.Ints.GetEnumerator();
<>1__state = -3;
}
//因?yàn)樯厦娴臄U(kuò)展方法里使用的是foreach遍歷方式
//這里也被編譯成了實(shí)際生產(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()里的邏輯取決于我們寫代碼時(shí)候的具體邏輯,不同的邏輯生成不同的代碼。這里咱們就不在講解它生成的代碼了,因?yàn)楹蜕厦嬖蹅冎v解的代碼邏輯是差不多的。
總結(jié)
通過本文我們介紹了c#中的yield return語法,并探討了由它帶來的一些思考。我們通過一些簡單的例子,展示了yield return的使用方式,知道了迭代器來是如何按需處理大量數(shù)據(jù)。同時(shí),我們通過分析foreach迭代和yield return語法的本質(zhì),講解了它們的實(shí)現(xiàn)原理和底層機(jī)制。好在涉及到的知識整體比較簡單,仔細(xì)閱讀相關(guān)實(shí)現(xiàn)代碼的話相信會了解背后的實(shí)現(xiàn)原理,這里就不過多贅述了。
到此這篇關(guān)于關(guān)于C#中yield return用法的思考的文章就介紹到這了,更多相關(guān)C# yield return內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用C#修改Windows操作系統(tǒng)時(shí)間
這篇文章主要介紹了利用C#修改Windows操作系統(tǒng)時(shí)間,幫助大家更好的利用c#操作系統(tǒng),感興趣的朋友可以了解下2020-10-10
C# DataTable與Model互轉(zhuǎn)的示例代碼
這篇文章主要介紹了C#DataTable與Model互轉(zhuǎn)的示例代碼,幫助大家更好的理解和使用c#,感興趣的朋友可以了解下2020-12-12
C#實(shí)現(xiàn)String字符串轉(zhuǎn)化為SQL語句中的In后接的參數(shù)詳解
在本篇文章中小編給大家分享的是一篇關(guān)于C#實(shí)現(xiàn)String字符串轉(zhuǎn)化為SQL語句中的In后接的實(shí)例內(nèi)容和代碼,需要的朋友們參考下。2020-01-01

