最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 82 講:C# 2 之迭代器語(yǔ)句(一):yield 語(yǔ)句

2022-01-25 10:41 作者:SunnieShine  | 我要投稿

今天我們來(lái)學(xué)習(xí)一個(gè)新的語(yǔ)法:yield 語(yǔ)句。這個(gè)語(yǔ)法稍微比較復(fù)雜,它也成為了以后學(xué)習(xí)很關(guān)鍵的語(yǔ)法 async/await 的奠基石,所以我們將這個(gè)語(yǔ)法分成兩講來(lái)說。

Part 1 模式的概念

今天我們?yōu)榱私榻B新語(yǔ)法之前,我們先來(lái)說一個(gè)概念:模式(Pattern)。

在當(dāng)前 C# 的環(huán)境,模式一共是有兩層含義:

  1. 表示一種實(shí)現(xiàn)模型的若干代碼組成的一段代碼,它表示一種固定含義的代碼,可用來(lái)當(dāng)成模板套用到任何地方上去;

  2. 表示一種判斷對(duì)象存儲(chǔ)的數(shù)據(jù)類型和數(shù)值信息的語(yǔ)法。

這兩種東西在 C# 里都叫模式,也都使用的是 pattern 這個(gè)單詞,不過它們使用的場(chǎng)合和環(huán)境不一樣,這里我們所說的是第一種;而從 C# 7 開始,我們才會(huì)介紹第二種模式的概念。

我們今天要說的模式,指的是一段固定邏輯的代碼,它的實(shí)現(xiàn)方式、手段可以變化,但做到的是同一項(xiàng)任務(wù),而且這種代碼可以作為模板代碼,套用到任何你想用到的地方上去。

舉個(gè)例子,我們要想迭代一個(gè)集合(就是使用 foreach 語(yǔ)句來(lái)完成遍歷集合的每一個(gè)元素的過程),我們有兩種手段:

  1. 將數(shù)組這種本身就是集合的類型封裝到整個(gè)類型里當(dāng)字段,然后迭代器直接用 GetEnumerator 方法來(lái)獲取即可;

  2. 自己實(shí)現(xiàn)獨(dú)立的迭代器類型,通過鴨子類型的機(jī)制,滿足迭代器類型即可,無(wú)需實(shí)現(xiàn) IEnumerableIEnumerable<T> 接口。

為了我們介紹后面的內(nèi)容,我們重新復(fù)習(xí)一下,我們之前對(duì)第二種實(shí)現(xiàn)的整體過程。

1-1 引例

假設(shè)我們自己實(shí)現(xiàn)了 List<T> 類型,迭代 T 類型的每個(gè)成員。

如果我們沒有自己的迭代器的話,由于 Array 類型的復(fù)雜性和處理機(jī)制的問題,_array 類型雖然是 T[] 的,我們也都知道可以直接 foreach 語(yǔ)句迭代出 T 類型的實(shí)例,可迭代器里我們并不能這么去書寫代碼,而單純使用 _array.GetEnumerator() 方法返回的是 IEnumerator 非泛型類型的實(shí)例,它自帶的 Current 類型成員是 object 類型而不是 T 的,因此無(wú)論如何都沒辦法精確到 T 上去,要么無(wú)法實(shí)現(xiàn),要么必須裝拆箱。所以,我們要想避免裝拆箱操作,不得不使用自己定義的迭代器類型。

寫法其實(shí)也不難,我們將數(shù)組的迭代語(yǔ)句看成是 while 循環(huán)執(zhí)行語(yǔ)句,那么 foreach 就可以自己等價(jià)換成 while 循環(huán):

可以看到,代碼變化不大。于是我們只需要將數(shù)組迭代的邏輯拆解為反復(fù)的移動(dòng)迭代指針的指向,并反復(fù)獲取當(dāng)前指向元素的過程就可以了。于是,我們可以這么實(shí)現(xiàn)代碼來(lái)完成這樣的操作:

然后,讓這個(gè)類型作為剛才我們自己實(shí)現(xiàn)的 List<T> 類型的 GetEnumerator 方法的返回值類型即可。

這樣我們就可以搞定迭代過程了,雖然稍微復(fù)雜了那么一點(diǎn),但它避免了裝箱提升了性能——為了優(yōu)化,我們肯定得付出更多的努力。

1-2 迭代器模式

可以看到,我們?yōu)榱藘?yōu)化性能,不得不創(chuàng)建一個(gè)帶有 MoveNext 的方法和一個(gè) Current 屬性的新類型(這個(gè) Enumerator 類型),然后讓用來(lái)迭代的類型(這個(gè)自己實(shí)現(xiàn)的 List<T>)直接實(shí)現(xiàn) GetEnumerator 方法,返回值直接用 Enumerator 類型來(lái)代替原本的接口類型。

這些實(shí)現(xiàn)是必要的,但也都是固定代碼。即使你實(shí)在是記不住它們,你也大概有印象,實(shí)現(xiàn)它們需要分三步:

  1. 打開冰箱門

  2. 把大象放進(jìn)去

  3. 關(guān)上冰箱門

呃……不是這個(gè)。是這樣的三步:

  1. 實(shí)現(xiàn) Enumerator 類型,傳入迭代的對(duì)象 _array 用作構(gòu)造器參數(shù);

  2. 實(shí)現(xiàn) Enumerator 類型的 Current 屬性(返回當(dāng)前結(jié)果即可)和 MoveNext 方法(必須返回 bool 類型);

  3. 在用于實(shí)現(xiàn)迭代的類型里,給出 GetEnumerator 方法,用 Enumerator 類型作為返回值。

這個(gè)過程是固定的,實(shí)現(xiàn)的代碼也都是固定的,唯一的區(qū)別是,你自己實(shí)現(xiàn)的時(shí)候可能就變量名換一下,或者是風(fēng)格稍微換一下,先寫屬性 Current 后寫 MoveNext 方法,還是先寫 MoveNext 方法后寫 Current 屬性的問題。但整個(gè)過程完全都是類似的,我們就把這樣的固定模板代碼稱為一種模式。而我們上面介紹的模式,目的是優(yōu)化性能減少甚至根治裝拆箱的方式,允許對(duì)象使用 foreach 循環(huán)迭代自身,即用于迭代器的模式,因此我們就稱為迭代器模式(Iterator Pattern)。

C# 里有眾多的模式等著我們?nèi)グl(fā)現(xiàn),迭代器模式是其中的一種。而有人將其總結(jié)起來(lái)然后整理成了一本書介紹,所以 C# 的寫法和用法都相當(dāng)靈活。如果你想要學(xué)習(xí)模式的實(shí)現(xiàn)的話,有單獨(dú)的一個(gè)介紹分支,叫設(shè)計(jì)模式,它會(huì)告訴你的。不過本教程只考慮語(yǔ)法,因此設(shè)計(jì)模式并不是側(cè)重點(diǎn),所以教程只簡(jiǎn)單提及這些概念;我以后會(huì)考慮出的。確實(shí)想學(xué),我可以給你一個(gè)參考鏈接,請(qǐng)參考這個(gè)鏈接介紹的內(nèi)容。

Part 2 yield 語(yǔ)句

說完了迭代器模式的完整語(yǔ)法,我們可以看到,這種語(yǔ)法下,我們?nèi)耘f需要自己定義和設(shè)計(jì)迭代的內(nèi)容以及過程,而且就只是因?yàn)閮?yōu)化性能的原因創(chuàng)建了許多面向?qū)ο蟮挠纺[代碼,實(shí)際上起作用的也就 MoveNextCurrent 這幾行的代碼而已。因此,C# 2 提供了另外一種處理辦法:yield 語(yǔ)句。這個(gè)所謂的 yield 是一個(gè)新的 C# 關(guān)鍵字,繼 C# 2 開始,我們已經(jīng)見到了不少的上下文關(guān)鍵字了,比如前面說到的 global、alias 關(guān)鍵字等等。yield 是為了簡(jiǎn)化迭代器的實(shí)現(xiàn)語(yǔ)法才產(chǎn)生的。

我們先從簡(jiǎn)單的開始說。找兩個(gè)例子來(lái)給大家介紹一下,yield 的用法。

2-1 有限序列的迭代

假設(shè),我要想迭代一個(gè)可以自己手動(dòng)列舉的集合的話,代碼可以寫得相當(dāng)簡(jiǎn)單。考慮一種情況,我想要迭代一下科目的所有名字,大家其實(shí)都知道,科目也就語(yǔ)數(shù)外政史地理化生這些,先暫時(shí)不考慮體育這些課程的話,那么九個(gè)科目是完全可以手動(dòng)枚舉和列舉出來(lái)的。那么,我們可以這么寫代碼:

請(qǐng)注意一下返回值類型。我們使用 IEnumerable<string> 來(lái)表示一個(gè) string 列表,可用來(lái)使用 foreach 循環(huán)來(lái)迭代序列。里面的代碼則是更為暴力:列舉每一個(gè)名字,然后直接使用了一個(gè)新的語(yǔ)句類型:yield return 語(yǔ)句來(lái)表示。

是的,yield return 語(yǔ)句改變了 return 關(guān)鍵字原本的語(yǔ)義:返回?cái)?shù)值。大家都知道,由于 return 表示返回結(jié)果并退出方法,所以沒有人會(huì) return 疊起來(lái)書寫的。而這次,yield return 貌似并不是返回結(jié)果然后退出,反而是挨個(gè)將這些字符串信息“返回”出去。是的,仍然是一個(gè)返回的概念,但此時(shí)我們不會(huì)再退出方法了。

最后,我們?cè)诘?12 行上加了 yield break; 這樣的語(yǔ)句。顯然,它不是 break 的基本用法:跳出循環(huán),而是跳出整個(gè)迭代器。是的,這才是真正的、退出方法的語(yǔ)句。因此,這里理解起來(lái)有些繞:

  • yield break 語(yǔ)句:迭代結(jié)束,退出迭代過程,返回整個(gè)序列;

  • yield return 語(yǔ)句:將當(dāng)前結(jié)果拿到,作為迭代序列里的其中一員。

反而是這里的 yield break 是返回了,所以這里需要你額外注意這種語(yǔ)法的格式和用法。

那么,這樣的方法,它的用法呢?返回值不是 IEnumerable<string> 類型的對(duì)象么,那么我們直接當(dāng)成結(jié)果,foreach 它就可以了:

是的,Classes() 方法是返回一個(gè)序列,一個(gè)可以 foreach 的序列,每一個(gè)都是 string 類型的元素的序列,因此在迭代的時(shí)候,我們使用 foreach (string @class in Classes()) 的語(yǔ)法就可以搞定,和一般的返回 IEnumerable 以及 IEnumerable<T> 接口的方法用法是完全沒有區(qū)別的。唯一要注意的是,class 是類型的關(guān)鍵字,因此需要你使用 @class 加一個(gè)原義標(biāo)記 @ 表示我們用的是標(biāo)識(shí)符 class,而不是表示關(guān)鍵字 class。

最后稍微說一下。前面介紹的 yield break 語(yǔ)句是可以不寫的,因?yàn)樗『煤竺鏇]有內(nèi)容了。因?yàn)槟銓懗鰜?lái)和不寫出來(lái),后面都因?yàn)闆]有任何的執(zhí)行代碼而直接被中斷掉,所以寫不寫都無(wú)所謂。就和寫在最后一句的 return 語(yǔ)句一樣,寫不寫都沒有什么區(qū)別。另外,yield returnyield break 都是 yield 開頭的語(yǔ)句格式,因此我們將它們統(tǒng)稱為 yield 語(yǔ)句(Yield Statement)。

那么為什么 yield 關(guān)鍵字要用 yield 這個(gè)詞語(yǔ)呢?Yield 是什么意思?“產(chǎn)生”、“提供”的意思。當(dāng)然 yield 還有別的意思,比如“屈服”、“讓步”等等,不過這里我們用的是產(chǎn)生的這個(gè)釋義。是的,yield return 連起來(lái)就是“產(chǎn)生返回結(jié)果”的意思,而 yield break 連起來(lái)則是“產(chǎn)生中斷”的意思,它剛好契合我們這里處理機(jī)制的邏輯和語(yǔ)義,所以 yield 是再合適不過的單詞。

注意,可能你學(xué)過 C# 高級(jí)編程,你可能聽說過 Task.Yield 方法表示讓出主使權(quán)和執(zhí)行權(quán),不過這里的 yield 單詞的意思是“屈服”、“讓步”這一套的意思,和這里迭代器模式里用到的“產(chǎn)生”、“提供”沒有一丁點(diǎn)的關(guān)系。

2-2 循環(huán)迭代

前面講了一下如何簡(jiǎn)單迭代一個(gè)我們完全可以列舉的情況,下面我們來(lái)說一下,如何迭代一個(gè)給定集合。

我們?nèi)允褂们懊孀铋_始的 Enumerator 實(shí)現(xiàn)的那個(gè)例子(_array 迭代的那個(gè))來(lái)講解。這次我們不再自己創(chuàng)建 Enumerator 類型,而是考慮使用 yield 語(yǔ)句的形式。

這個(gè)語(yǔ)句好用就好用在這里。我們這次改成使用 T 類型作為接收結(jié)果的 IEnumerator<T> 類型,作為返回值類型:

請(qǐng)看看這個(gè)例子。這個(gè)例子牛逼就牛逼在,它解決了我們前面說的“無(wú)法實(shí)現(xiàn)”的問題。前文我們說過,_array 類型雖然是 T 類型的元素的數(shù)組,也確實(shí)可以用 T 類型為迭代變量的類型使用在 foreach 循環(huán)里面,可問題就在于它的返回值是 IEnumerable 非泛型的接口類型,因此即使你迭代出來(lái),每一個(gè)元素也都是 object 類型的。因此,我們可以這么去轉(zhuǎn)換一下語(yǔ)義。

我們將返回值類型仍使用原來(lái)本應(yīng)該用到的 IEnumerator<> 類型來(lái)表達(dá),不過這次我們用上泛型接口版本來(lái)代替掉原本的非泛型版本。而我們?cè)谑褂?foreach 迭代集合里的 _array 時(shí),這次我們可以使用 T 類型了,于是通過每次循環(huán)得到的 element,作為結(jié)果來(lái) yield return 到返回序列里去。

最后,循環(huán)執(zhí)行完成后,yield break 終止迭代過程(當(dāng)然,這個(gè) yield break 后面也沒東西了,也可以省略不寫)。


我們對(duì)比兩個(gè)例子,前面是原來(lái)的寫法,后面是現(xiàn)在的寫法,是不是簡(jiǎn)略了不少東西。這就是 yield 語(yǔ)句的魅力。

Part 3 注意事項(xiàng)

是的,yield 語(yǔ)句有非常不錯(cuò)的語(yǔ)法簡(jiǎn)化機(jī)制,因此給我們書寫代碼的時(shí)候帶來(lái)了非常優(yōu)雅的書寫代碼的方式。但是,它也不是隨時(shí)隨地都能用。下面我們來(lái)說一些需要注意的地方。

3-1 返回值只能是 IEnumerableIEnumerator 接口及泛型版本

想必這個(gè)說起來(lái)很容易理解。IEnumerable 和它的泛型版本 IEnumerable<> 的目的是,直接返回一個(gè)可以 foreach 的對(duì)象;而 IEnumerator 和泛型版本 IEnumerator<> 的目的是,直接返回一個(gè)允許當(dāng)前類型的對(duì)象可以 foreach 的迭代器輔助對(duì)象。不過,yield 語(yǔ)句的機(jī)制的特殊性,C# 允許我們?cè)谶@兩種情況下,都可以使用 yield 語(yǔ)句來(lái)完成執(zhí)行任務(wù)。但是,別的類型就不行了。哪怕這個(gè)類型滿足那套鴨子類型的規(guī)則,或者別的什么理由,C# 都是不允許的,這是因?yàn)樗牡讓訉?shí)現(xiàn)機(jī)制導(dǎo)致的。底層的代碼我們將在下一講的內(nèi)容給大家介紹。

3-2 不能在帶有 yield 語(yǔ)句的方法體里用 return 語(yǔ)句

yield 語(yǔ)句帶有固定的執(zhí)行語(yǔ)義,如果一個(gè)方法里帶有了這樣的語(yǔ)句,它就相當(dāng)于是改寫了方法體的執(zhí)行邏輯和規(guī)則。如果你混用 yield 語(yǔ)句和 return 語(yǔ)句的話,就會(huì)改變這個(gè)執(zhí)行規(guī)則:yield 語(yǔ)句表示帶出一個(gè)新的迭代元素和退出迭代過程;而 return 語(yǔ)句直接返回一個(gè) IEnumerableIEnumerator 接口的實(shí)例。

你想想看,我直接在方法體里寫了 yield 語(yǔ)句,原本方法體里應(yīng)該用 return 來(lái)返回一個(gè)實(shí)現(xiàn) IEnumeratorIEnumerable 接口類型的實(shí)例,可現(xiàn)在我 yield return 可以按元素為單位帶出每一個(gè)迭代的東西,它們顯然不屬于同一種思維方式。

所以,yield 語(yǔ)句使用就直接改變了迭代的邏輯和方案,這意味著你用 yield returnyield break 語(yǔ)句就不能用 return 語(yǔ)句;你用了 return 語(yǔ)句也就不能用 yield returnyield break 語(yǔ)句:它們的流程控制是不同的。

3-3 不能往用了 yield 的方法參數(shù)上用 refout 修飾符

想想看,下面的這個(gè)方法有啥意義?

這個(gè) yield return 1 是我象征性返回一個(gè)元素的代碼,實(shí)際上可以不帶只有一個(gè)元素的集合。可參數(shù)這次用了 ref 修飾。這有意義嗎?

我們可以制作一個(gè)方法,傳入一個(gè)變量,表示我從 1 開始迭代到多少:

所以這個(gè)參數(shù) s 是有意義的??扇绻覀魅肓?ref int 而不是 int,意義就完全不一樣了:ref int 參數(shù)的 ref 修飾符意味著這個(gè)數(shù)據(jù)是可以通過方法體里的修改,直接影響到方法的調(diào)用方的。

你看看,要真的是在帶 yield 語(yǔ)句的方法上,參數(shù)用了 ref,你就完全沒辦法掌控這個(gè) ref 參數(shù)的具體變化的情況了。因?yàn)?yield 的迭代會(huì)被底層看待是一個(gè)象征性的元素返回過程,并整體整理為一個(gè)集合,提供 foreach 操作。在此期間,我們使用 ref 修飾的參數(shù)如果出現(xiàn)在代碼里,產(chǎn)生了變更,那么變量的數(shù)值到底是每一個(gè)迭代改一下,還是底層機(jī)制自己控制改變情況呢?這個(gè)我們根本說不清楚。

同理,out 也是一樣。out 參數(shù)意味著方法體內(nèi)賦值傳出方法給調(diào)用方使用的過程,即從內(nèi)往外的單向賦值過程。可問題就在于,ref 雙向的都不允許,out 難道就會(huì)允許了?它們都是會(huì)同時(shí)影響調(diào)用方和方法本身里的代碼執(zhí)行的修飾符,我們必須限制 ref 后也要限制 out 才行,不然傳出的參數(shù)是多少也都是不穩(wěn)定的。

可能你還是對(duì) yield 不熟悉,下面我們會(huì)通過另外一講的方式,給大家詳盡闡述一下 yield 的底層原理,即編譯器到底是怎么看待和理解 yield 這個(gè)連方法體的流程控制的思維都改變了的關(guān)鍵字用法。


第 82 講:C# 2 之迭代器語(yǔ)句(一):yield 語(yǔ)句的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
皋兰县| 和静县| 云和县| 南川市| 井研县| 珠海市| 荆州市| 阜新市| 上高县| 黄平县| 马山县| 高唐县| 九江市| 文登市| 双柏县| 光山县| 滦平县| 金沙县| 庆云县| 齐河县| 芜湖市| 昌平区| 昆明市| 西乌珠穆沁旗| 新昌县| 灵台县| 华坪县| 淮北市| 鄢陵县| 蓝田县| 长乐市| 巫溪县| 德昌县| 兰溪市| 淮滨县| 尉氏县| 邵武市| 通州市| 同江市| 桃园县| 察雅县|