五月天青色头像情侣网名,国产亚洲av片在线观看18女人,黑人巨茎大战俄罗斯美女,扒下她的小内裤打屁股

歡迎光臨散文網 會員登陸 & 注冊

第 83 講:C# 2 之迭代器語句(二):`yield` 的底層

2022-01-28 12:08 作者:SunnieShine  | 我要投稿

上一回我們帶著大家學習了 yield 關鍵字的用法。下面我們來看一下,yield 到底編譯器是怎么看待和理解的。

坐穩(wěn)了。本節(jié)內容較多,也比較復雜,所以如果前一節(jié)的內容沒有完全掌握的話,請等到完全掌握后再來學習。講解原理的內容往往都比正常的東西都難,所以我希望能夠讓大家看懂的前提是必須掌握前面介紹到的、關于 yield 關鍵字的用法。

Part 1 廢話不多說,先看完整代碼

你始終要記住,新版的語法,不管是 C# 2 還是 C# 10,它都跟語言特性有關。你實現的代碼最終都會被自動轉換為 C# 早期能夠寫出的代碼;當然,確實有些也寫不出來。比如你無法用 C# 代碼表達一個委托類型的實例的底層實現,因為它用到了函數指針,這個 C# 語法是沒有的,所以你完全寫不出 C# 的完美的委托類型的底層代碼實現。不過我們大多數時候在講解原理的時候,都講的是能翻譯成 C# 原生代碼可以表達的完整版代碼,這樣你才能明確了解和明白這些新的語言特性在編譯器的眼中,到底是怎么去看待的。

我們來看一個例子。有例子會好說一些??紤]一種情況,假設我輸入一個數字進去(假設叫 limit),然后通過 yield 來不斷迭代從 0 到 limit 兩倍之間的所有偶數(不含 0)。那么,代碼可以這么寫:

假設第 1 到 2 行代碼是 Main 里的,而第 4 行到第 8 行則是 Program 類型里帶的別的方法。

我故意設計了一個這個例子,是為了讓你明白和故意體現出實現代碼的“編譯器魔法”:我故意給 yield return 后跟的表達式上乘以了 2,而故意傳入了參數。這就是為了體現待會我們看完整版代碼的時候,能夠發(fā)現到這些地方到底都對應到哪里。

為了簡單寫代碼我就這么寫了,我也面得寫一個 Program 然后再寫 Main 然后才是執(zhí)行代碼。實際上你要知道,Program 類型里包裹 Main 方法,里面才是第 1 到 2 行的內容;然后你還需要在 Program 類型的別處放上 GenerateEventSeries 這個方法。這才是完整的代碼實現。

下面我們來看一下,這個代碼的完整版代碼。由于內容有點多,所以我將其分為三個代碼塊給大家介紹。先來看 yield return 這個方法會導致產生的迭代器完整類型:

雖然很抱歉,但確實就這么長。接著是 Main 方法:

最后是 GenerateEvenSeries 方法,就是執(zhí)行 yield return 的那個方法。

當然,這些代碼并不是完全意義上的等價,為了簡化我接下來讓你理解的內容,我稍微對完整版翻譯的代碼做了部分改動。比如刪除了一些沒必要在這里說明的接口實現、一些沒有必要在這里說明的特性標記、內聯了部分變量的使用等等。然而,事實的真相是,我簡化了代碼之后代碼仍然很長。沒有關系,下面我們逐個理解。先來看迭代器類型 Iterator

很多人到現在仍然并不知道,完整版的代碼的“完整版”到底是怎么一種概念。我簡要概括一下:你用到的新的語法特性,都是編譯器按照等價翻譯,改寫成 C# 1 原生語法后才執(zhí)行的。而我這里介紹的,就是編譯器改寫后的代碼。它和原來新語法特性的格式不同,但執(zhí)行的語義是等價的。

在編程里,我們把“一種語言特性可以簡化原本書寫的代碼,但這種語言特性和原本書寫的代碼是等價關系(即執(zhí)行起來效果完全相同,甚至潛藏的 bug 或者觸發(fā) bug 等等的行為全都是一樣的)”的這種語言特性叫做原本書寫的代碼的語法糖(Syntactic Sugar)。換句話說,新語言特性多數情況下都是舊版語法的語法糖,即簡化了代碼使用和書寫,但效果仍然一樣。這里的 yield 語句,就是典型的迭代器模式的語法糖——原本你要自己獨立寫一個迭代器的類型出來,而現在你只需要一句 yield 就可以讓編譯器按照你給的操作自動生成和反推出完整的迭代器類型的實現,而這樣就不用你手寫了。你說,是不是很方便?

Part 2 迭代器類型 Iterator

前面我們介紹了完整代碼。如果你覺得自己看得懂的話,可以自己去看前面的內容。如果看懂了的話,后面從這里開始的所有內容都可以不再看了,因為反正我只還是介紹一下這個底層到底都是一些什么,到底都有什么用,以及為什么這么寫。

先來說迭代器類型 Iterator 類型。我們把代碼照搬過來。

2-1 private sealed class 類型修飾符

可以從 Iterator 類型上看到,修飾符是 private sealed class。可問題是,類型不是不能用 private 么?實際上,這個類型是編譯器自動生成的,它被丟進了 Program 類型里,作為嵌套類存儲和存在。所以,為了防止從外部別處訪問它,以及暴露出去讓別人知道等等安全問題,C# 考慮用 private 修飾符。

sealed 呢?很明顯也是為了避免重寫這個類型。因為這個類型是編譯器自動生成的,沒有必要讓別的用戶從一個自動生成的類型里去派生別的類型。而且這么做也沒有意義:因為我這個迭代器的使用和構建代碼全部是編譯器一條龍服務搞定的,你自己去派生出來別的類型,不論你哪里用都沒有意義,因為反正你也改變不了原本編譯器生成的代碼。所以,sealed 修飾符預防用戶從它派生。

class 也就不多說了。別問我為啥不用 struct。

2-2 [CompilerGenerated] 特性

這個老早就說了。這個特性的存在是為了輔助編譯器區(qū)分和辨識哪些代碼是編譯器生成的,哪些是我們自己寫的。編譯器代碼最終在運行期間,是和你自己的代碼混在一起的,所以必須需要一種機制去區(qū)分開代碼的實現機制和實現方,為了輔助用戶在以后某個時刻區(qū)分它們(通過反射機制)。

2-3 _limitlimit 字段

這個類型里我們可以發(fā)現,它自帶 _limitlimit 兩個字段,不過帶下劃線的 _limitpublic 修飾的,反倒是沒有下劃線的 limitprivate 的。

我們仔細觀察代碼可以發(fā)現,_limit 這個 public 字段貌似只在 GetEnumerator 方法里出現,而 limit 則在 MoveNextGetEnumerator 方法里都使用到了。這個 limit 字段和 _limit 字段實際上就是前面 GenerateEvenSeries 方法的那個參數賦值過來的。至于怎么賦值的,我們目前只能在 GetEnumerator 里看到過程:iterator.limit = _limit;。而至于哪里賦過來的,我們目前還沒辦法知道,因為代碼沒有在這個類型里面(在 GenerateEvenSeries 方法里)。

由于 limit 是只在類型里使用,所以修飾的是 private;反而是這個 _limit,由于它在外部用到(Program 類型下我們實現的 GenerateEvenSeries 方法里),所以被修飾為了 public。為什么不是 internal?因為沒必要:整個 Iterator 類型都是私有的,你沒有必要給它設置嚴格的字段訪問級別,即使你寫 public,和你寫 internal 在當前程序環(huán)境下也都是沒有區(qū)別的。所以這種嵌套是一種習慣用法——給 private 里面的東西用 public。

2-4 _state 字段

這個是一個全新的字段信息。在我們之前實現迭代器類型的時候,并沒有實現這個字段。它在這個類型里體現的作用是表示當前迭代過程的狀態(tài)??梢园l(fā)現,_state 最開始是 -2(由 GenerateEvenSeries 方法里看到,實例化的時候傳入的是這個 -2,而構造器里是給出了 _state = state; 的過程,所以可以確定,最開始對象是 -2 為初始數值的)。

這個字段稍后還會在 MoveNext 方法里用到,它甚至可以變?yōu)?-1、0、1、2 這些數值。這個我們稍后說。但可以知道一點的是,因為它只在這個類型里有所使用,因此它修飾的是 private,防止外部訪問。

2-5 _current 字段

這個不用多廢話,它就是用來提供返回 Current 屬性的結果的。而且我們之前就是用 private 修飾的,所以這里也沒有變化。

2-6 _initialThreadId 字段

這個也是一個全新的字段。我們可以看到它只在構造器里和 GetEnumerator 方法里有。它的賦值和判斷用到的都是一個叫做 Environment.CurrentManagedThreadId 的靜態(tài)屬性。它是什么呢?它表示你當前運行環(huán)境下的線程編號。整個操作系統(tǒng)是由很多進程一起工作的,而我們這個屬性,獲取的是當前線程的編號。也就是整個運行程序的時候的編號。

這做什么呢?有沒有想過一個問題,就是別的線程破壞性更改了你的這個 Iterator 類型的東西,會如何?如果我現在用了別的線程,然后更改了這里的數據(或者改到一半,時間片切換),你的里面的數值就變得異常奇怪。是的,這個標識是記錄你生成和產生迭代器的當前線程用的。為了避免從別的地方更改你的內容,我們使用了這個機制來防御。

請注意 GetEnumerator 方法里,我們用了 _state == -2 && _initialThreadId == Environment.CurrentManagedThreadId 兩個條件判斷。如果你的運行線程已經切換,這條將不會成立,于是會走 else 部分,創(chuàng)建和生成一個新的迭代器實例;否則,我們選用自身作為返回結果。

看到 _iterator = this; 這句話。this 作為當前對象,賦值給 _iterator 的目的就是將自己作為返回結果。如果已經跨線程更改了 this 里包含的數據,顯然編程我們是無法防御這種變更的,因此有這么一個機制可以避免將更改后的錯誤對象返回出來使用。

同樣地,這個字段也只在當前類型里用,所以用 private 修飾。

2-7 _index 字段

_index 記錄的是序列在 for 循環(huán)的迭代次數。這個也不必多說,因為改成迭代器類型后,我們無法再次合理記錄迭代的過程的那個變量 i。因此,我們這里需要一個臨時的變量記錄它,于是創(chuàng)建了這個 _index 字段。

2-8 Iterator(int state) 構造器

這個類型創(chuàng)建了唯一一個構造器,傳入的是 state 的數值,并且可以在代碼里看到,它給 _state 賦值;并且,它還給 _initialThreadId 字段賦值,記錄了當前的線程。

2-9 Current 屬性

這個屬性沒有多說的必要,它就是原本我們實現的迭代器類型的 Current 屬性。

2-10 MoveNext 方法

我們學習了很多迭代器的知識點了,我們多多少少對 MoveNext 方法的基本作用有了一個合理的認知。MoveNext 方法用在 foreach 的等價替換里,用于 while 循環(huán)的條件判斷。foreach 循環(huán)的迭代遍歷過程都是針對于集合的,而 MoveNext 方法的執(zhí)行邏輯相當于給這個操作“降了個維度”——只判斷能否進行下一次迭代。

這次我們可以看到操作一直在變更 _state_current 的數值,不過還帶了一些別的判斷邏輯。

首先,方法進來我們會先判斷一下 _state 是否是 0。為什么判斷是不是 0 呢?不是最開始賦值的是 -2 嗎?是的,這就要說一下 _state 的基本規(guī)則和處理機制了。

_state 在整個迭代器里稱為狀態(tài),狀態(tài)一共是有如下一些情況:

  • 迭代前:-2;

  • 迭代中:-1;

  • 迭代預備:0;

  • ???:1。

我們對比代碼可以發(fā)現,我們在 GenerateEvenSeries 方法里實例化的 _state 恰好為 -2,對應了這里的“迭代前”狀態(tài);而在 GetEnumerator 方法里,_state 從 -2 變?yōu)?0,即改為“迭代預備”狀態(tài)。然后在 MoveNext 方法里,_state 分為 0 和不是 0 兩種情況。如果 _state == 0,說明此時是“迭代預備”狀態(tài),所以我們這是我們第一次調用 MoveNext 方法,于是執(zhí)行的代碼是如下兩行:

即變更迭代器的狀態(tài)和初始賦值。因為 MoveNext 僅提供的是判斷是否可以進行下一次迭代,因此無法確定是不是第一次迭代。因此,此時變更 _state 為 -1,將此時迭代狀態(tài)變?yōu)椤暗小?,表示開始迭代了;接著,_index 字段改為 1,表示初始迭代數值。它對應了我們 for (int i = 1; i <= limit; i++) yield return i * 2;i = 1 的初始數值賦值,意味著迭代的開端。

那么 _state 如果調用 MoveNext 方法不是第一次的話,_state 肯定就不會是 0,因此執(zhí)行的代碼則是這樣的:

_state 此時已經有 -2、-1 和 0 兩種數值已經用過和講過了,也都出現過了;那么僅剩下的就只有 1 沒有說了。MoveNext 是不斷被調用的過程,因此它有可能不是第一次使用到,因此 _state 此時可能已經改為了別的數值情況,而此時 1 也可能會出現。我們仔細觀察 MoveNext 方法,我們可以發(fā)現在后面有一個 if 判斷和一句 return false;,是這么一段內容:

我們發(fā)現,_index <= limit 實際上就等價于正常的循環(huán)判斷過程,就是原來 for (int i = 1; i <= limit; i++) yield return i * 2;i <= limit 部分。接著,如果條件成立,循環(huán)需要執(zhí)行,因此里面我們認定迭代是成功的,可以正常進行 MoveNext 操作,因此里面的代碼是給 _current 正常賦值,并給 _state = 1 作為狀態(tài)數值的更新,然后返回 true 作為 MoveNext 正確迭代的返回結果;如果我們遇到 return false; 了,說明此時早已經不滿足 _index <= limit 條件,不然就不會走到 return false; 這里來。所以,這段代碼的思路就是正常的模擬循環(huán)迭代的中間步驟,判斷是否循環(huán)可以向下執(zhí)行。

那么 1 呢?前文不是留下了一個 1 這個狀態(tài)碼還沒有介紹和解釋嗎?這個 1 是什么呢?我們再次返回到前面的代碼:

我們前面有這樣一段代碼??蓡栴}就在于,我這個代碼的外層被 _state != 0 包裹起來了,也就是說它前提是 _state 已經不是 0 的時候,才會判斷是不是為 1。仔細觀察整個迭代器和賦值的過程,_state 一共出現了 -2、-1、0、1 四種可能狀態(tài),而非 0 的情況只有 -2 和 -1 還有 1;而 -2 和 -1 都意味著迭代還沒開始和已經開始,因此它們跟 return false; 沒有半毛錢的關系。你肯定不能說 -2 這個狀態(tài)(還沒開始迭代)就 return false; 來終止迭代器迭代過程,也不能說 -1 這個狀態(tài)(都在迭代之中了,你都不確定是不是迭代完畢了)就貿然 return false; 來終止迭代器的迭代過程吧?所以,很明顯這里的 _state != 1 跟 -2 和 -1 狀態(tài)對應的操作過程是沒有任何關系的。那么 1 是什么呢?

狀態(tài)碼為 1 編譯器生成代碼期間,跟 yield return 操作綁定起來的一個編號。它可以是任何的正整數,不一定非得是 1,也不一定只使用 1 這一種情況。綁定 yield return 的迭代數值其實是為了表達 MoveNext 在將來不斷迭代的時候,到底應該迭代哪一個數字、迭代到哪里去。

這句話非常不好理解。我們可以想象一下,假設你現在在醫(yī)院。你是醫(yī)生掌管這一層樓的病人。早上你需要查房去問候病人的身體狀況信息。這個例子里,醫(yī)生可以類比于迭代器里的“游標”,表示我到底訪問到哪里了。你站在辦公室就說明你還沒開始查房(迭代器還沒有開始迭代);你站在某某病房就說明你已經在查房期間了,只不過游標現在指向這個迭代的數值信息。當你完整走過整條病房的走廊,就說明你已經完成了對一個樓層的查房(即完成了迭代器的迭代過程)。在查房期間,你可能不會“老實地”按次序去查房,可能你會因為危重病人會優(yōu)先去距離你辦公室更遠的病房,然后折返回來看前面的病房。這個正整數的狀態(tài)碼就是編譯器解決這種“自定義迭代序列”的一個標識碼。只不過我們都知道,剛才我們整個 yield return 語句外層只有一個很普通的 for 循環(huán),因此狀態(tài)碼在這個例子里只有也只需要 1 這一種情況的標識。而使用正整數標識碼的這個思路很巧妙,巧妙在于它可以解決你對于代碼實現的任何情況。為什么說是“任何情況”呢?因為你在書寫代碼使用 yield return 語句的時候,你可以平凡迭代(一大堆連著的 yield return 語句),也可以帶有 if 語句的 yield return,也可以是 forforeach 循環(huán)的 yield return,也可以是嵌套循環(huán)、嵌套循環(huán)和 ifswitch 語句的 yield return,等等等等。這種實現機制是我們隨意給定和規(guī)定的,而編譯器處理起來就很頭疼了:我怎么靠著一句和幾句 yield return 來反推實現完整的 MoveNext 操作呢?最簡單的辦法,就是利用一個臨時編碼記錄一下迭代的次序、迭代的規(guī)則以及迭代的過程,再配合 goto 語句和標簽,便可以完美完成任何情況的跳轉。這也是編譯器的“慣用伎倆”:為了避免各種各樣復雜的迭代操作和語法,我干脆就用 goto 和標簽來完成跳轉。至于跳轉的順序和跳轉的規(guī)則,配合編號和次序信息就可以完成擬合。因此,這個編號和次序就是我們這里的 _state 了。

不過,由于 _state 在前文用過了 -2 和 -1 還有 0 了,因此這次為了避免使用數據和編號上的沖突,在正常迭代期間,我們都采用的是正整數進行的完整的迭代過程。而例子非常巧的地方是,因為它只有一個 for 循環(huán)包裹了 yield return,因此我們只用到了 1 這一種額外的狀態(tài)碼信息??赡苣氵€是不太清楚,2、3、4 甚至更大數值的狀態(tài)碼出現在什么時候。你可以試試看,自己寫一個自定義各種各樣奇奇怪怪的跳轉的、并帶有 yield return 的迭代方法,然后看看底層的代碼,就可以發(fā)現,這些時候 2、3、4 甚至別的數值也都會跟著出現了。至此我們就說明了 _state 的情況。下面總結一下 _state 的情況:

  • -2:表示迭代操作還未開始;

  • -1:表示迭代操作已經開始;

  • 0:表示迭代操作是處于還沒有開始和開始之間,作為預備開始的情況存在;

  • 其它正整數(本例里只有 1 一種情況):標識和標記自定義跳轉過程的跳轉位置和編號。

2-11 GetEnumerator 方法

可能你會有一個問題,這個 GetEnumerator 方法也挺奇奇怪怪的:一個我們自己實現的迭代器類型怎么自己還帶 GetEnumerator 方法?下面我們來說說這個 GetEnumerator 方法。

前文其實簡單提到了 if 判斷,因此我們這里簡單說一下就行了。

請注意代碼。Iterator iterator; 變量定義放在 if 的最外側,因為 ifelse 里都會給 iterator 賦值,雖然數值不同。

如果 _state == -2 并且 _initialThreadId == Environment.CurrentManagedThreadId,就說明我們當前迭代操作還未開始,并且線程是沒有變化的話,說明此時當前對象可以用于迭代,因此 iterator = this; 就表示迭代器實例就是它自己。當然,這里需要改成 0,是因為 -2 只是表示我在迭代前才會用。因為馬上要開始迭代了,因此要進入“預備”階段,所以要改成 0。

但凡 _state 不是 -2,或者 _initialThreadId 不是當前線程的編號,都說明此時迭代器的數據信息可能已經被篡改或更改過。但此時顯然我們還沒開始迭代,數據就有更改就是不合適的。因此,我們在這種極端情況下會走 else,將 iterator 重新實例化一個對象,作為返回結果。

在返回 iterator 對象之前,我們還需要更新必備數據。因為我們要開始迭代,自然需要直接將對象初始化。因此:

這兩句話就意味著,對 limit 這個私有字段賦值用于迭代,_limit 僅用于外部功能的交互。而現在開始迭代了就得更新 limit 私有字段的數值了。更新完畢(iterator.limit = _limit; 操作完成)后,直接返回 iterator 就可以了。

那么現在我們就可以說說,為什么 GetEnumerator 要存在了。實際上,這個 GetEnumerator 方法的取名有些“造成歧義”,它的目的是為了能夠在 GenerateEvenSeries 方法里使用上這個 Iterator 類型,因此我們需要合理地對對象進行實例化和初始化,才能正常在 GenerateEvenSeries 方法里完成合適的操作,達到和 yield return 等價的執(zhí)行過程。

而本身 Iterator 類型是迭代器類型,它自身并沒有合理、合適的初始化位置,所以 GetEnumerator 方法就起到作用了:它封裝了迭代器初始化的邏輯,避免誤用和非規(guī)范使用迭代器實例化進行迭代過程。因此這個 GetEnumerator 方法,更合理的名字應該叫 GetIterator。不過……顯然還有一個別的原因,不過這個原因,我們稍后說。

Part 3 GenerateEvenSeries 方法

說完了完整的迭代器的成員,以及它們各自的作用后,我們來說說,如何交互這個迭代器類型。我們此時把注意力轉移到 GenerateEvenSeries 方法里去。

我們把代碼搬過來。

3-1 private static 修飾符

這個沒啥好說的。因為我們最開始創(chuàng)建的方法就是這樣的修飾符,它是默認抄過來也沒有變化的。

3-2 [IteratorStateMachine(typeof(Iterator))] 特性

請注意,這個方法的改動有三處:

  • 返回值類型本來是 IEumerable<int>,現在改成了 Iterator 類型;

  • 代碼從 for 循環(huán)配 yield return 語句,完全改寫為了 Iterator 的實例化和初始化過程;

  • 多了一個 IteratorStateMachineAttribute 類型的特性標記。

我們先來說這個特性。這個 IteratorStateMachineAttribute,是 C# 2 有了 yield 關鍵字后就立馬就有的特性。這個特性標記和標識我們用的是什么類型來完成迭代機制的。當然,這個特性不標記好像也無所謂,但有了標記,編譯器才知道我這個方法是轉譯過后的代碼。因為原本這里是 yield return,現在改成了實例化和初始化。沒有這個特性就很難知道這段代碼是不是編譯器重寫的。這是最通俗也是最簡單理解的一個標記該特性的原因。

3-3 方法體的變動

下面來看整個方法的方法體都執(zhí)行了什么。好吧也沒啥好說的,就是一個 iterator 通過 new 實例化的過程。請注意此時我們初始化的時候,狀態(tài)碼是賦的 -2 而不是別的數值。這是最開始的情況:因為這還沒有開始迭代,只是標識和產生一個實例的過程。迭代器都還沒開始自然不能亂動這個狀態(tài)碼。我們要按規(guī)定來。

然后,再給 _limit 字段賦值上 limit 參數的數值進去,就是為了交互的時候知道,循環(huán)到哪里結束。

最后,返回對象本身即可。

3-4 返回值類型的變動

可以發(fā)現,返回值類型由 IEnumerable<int> 改成了 Iterator 類型。如果你對迭代很熟悉了的話,顯然就會發(fā)現詭異的地方:IEnumerable<int> 自身拿到就可以 foreach,但 Iterator 類型是一個迭代器類型,自身是提供 foreach 循環(huán)里的其中的一個小的步驟的,它們根本不是同一個東西。那么,Iterator 類型顯然就不可能自身用于 foreach 了,對吧?

不對。這里就要說明 GetEnumerator 方法的特效了。這個方法之所以不叫 GetIterator,有一個非常重要的原因:告訴編譯器,Iterator 自身的可 foreach 迭代的鴨子類型。還記得一個對象類型支持 foreach 循環(huán)需要滿足什么條件嗎?是的,有一個可以返回迭代器類型的 GetEnumerator 方法即可。而區(qū)分和辨識一個類型是迭代器類型的滿足條件,只要要求自帶 Current 屬性,以及bool 返回值的 MoveNext 方法。而這個 Iterator 類型神奇就神奇在,本應該是 IEnumerable<int>IEnumerator<int> 兩個不同類型的操作,它自己一個人都做完了。因此,按照鴨子類型的指定規(guī)則,它確實滿足可 foreach 的條件,因此編譯器不會禁止它不能用 foreach。

這便是為什么,這個 Iterator 里自帶的這個 GetEnumerator 方法,奇奇怪怪的真實原因——它“一類雙用”。兩棲動物了屬于是。

Part 4 最后看看主方法

最后我們來看看主方法。

主方法做得相當簡單:一個 foreach 就完成了。原本主方法里就是一個 foreach,現在還是這個 foreach,壓根沒咋動。是的,這就是神奇的 Iterator 類型,它底層完美還原了 yield return 的迭代過程,因此根本影響不到 Main 方法的變動。只不過,底層變化了,這個 GenerateEvenSeries 方法的調用的底層也跟著變了。原本是 IEnumerable<int> 表示的類型實例的可迭代性;而現在類型換為了 Iterator 類型,它則是由于底層的復雜處理機制來完成迭代的。

Part 5 完整的迭代器迭代過程

為什么我說它們是等價的呢?我們來想想,foreach 等價代碼是什么。就拿這個題目來說,它的完整代碼應該是這樣的:

是的,通過 GenerateEvenSeries(10) 的調用得到 Iterator 的實例。這個實例里自帶 GetEnumerator 方法用于初始化整個迭代器的狀態(tài)字段、_index 字段、limit 字段等等信息,進行規(guī)范化初始化后,將 Iterator 的新實例得到,用作完整版迭代里 while 循環(huán)的條件:MoveNext 方法的調用。

然后我們反復利用 MoveNext 方法,反復迭代更新迭代器里 _index_current 等等字段的數值;而 _state 則一直保持 1 沒有變動,就只是最開始從 -2 改成了 0,然后從 0 改成了 1,后就沒有任何變化了(一直是 -1、1、-1、1 這么交替來的);另外,此時 limit 沒有變化,因為它初始化后就沒有任何賦值的行為了,只有讀取比較的行為。

反復迭代執(zhí)行 MoveNext 后,_index 最終由于增大而超過 limit 字段,導致條件不成立,于是迭代結束,返回 false 意味著 MoveNext 無法繼續(xù)調用,也意味著 while 循環(huán)執(zhí)行 iterator.MoveNext() 會返回 false 使得循環(huán)無法繼續(xù)執(zhí)行,最終退出 while 循環(huán)。

Part 6 其它問題

下面我們來了解一下,關于迭代器完整代碼產生和帶來的一些我們前文沒解釋到的問題。

6-1 GenerateEvenSeries 返回值是 IEnumerator<int> 的話?

還記得前一節(jié)的內容嗎?我們約定和規(guī)定,yield returnyield break 語句要出現在返回值是 IEnumeratorIEnumerator<>、IEnumerableIEnumerable<> 的接口類型,別的都不行。至于原因我們也已經簡單分析過了,因為別的類型無法也不好控制迭代過程和處理機制,而這四個接口微軟最熟悉:它知道這幾個接口用來干嘛,以及如何使用。

那么,這里有一種情況是我們前面沒說的。前面我們給定的 GenerateEvenSeries 方法返回的是 IEnumerable<int> 類型的,它屬于是 IEnumerableIEnumerable<> 這一個“流派”的。但是,yield returnyield break 語句還可以用于返回值是 IEnumeratorIEnumerator<> 類型的情況。那么,這樣的情況下,生成的代碼是怎么樣的呢?

還記得我們之前 IEnumerable<int> 生成后嗎?生成后,Iterator 這個編譯器生成的類型里會自帶一個 GetEnumerator 方法,用來返回合理、合適的 Iterator 實例,用來迭代?,F在我們如果改成 IEnumerator<int> 的話,你覺得會發(fā)生什么?

對咯!編譯器直接不生成 GetEnumerator 這個方法。因為 IEnumerator<int> 默認就會自動產生這樣的迭代類型對象 IteratorIEnumerable<int> 返回值的迭代器生成代碼會帶有 GetEnumerator 顯然是因為返回值是 IEnumerable<int>,它不帶迭代器獲取的操作和行為,所以需要內嵌一個 GetEnumerator 才行;而此時我們已經更換了返回值類型,已經改成 IEnumerator<int> 了之后,那么在生成的代碼里,就自然而然不必了:因為 Main 方法的原始代碼是這樣的:

現在,Iterator 自己就是 IEnumerator<int> 的,所以,我們完全可以省去這里的 GetEnumerator 方法的調用,改成這樣:

懂了嗎?因為沒有了 GetEnumerator 方法的調用,我們也就不必讓編譯器生成 GetEnumerator 方法了。此時也就不存在什么 _currentThreadId 之類的東西,以及 _state 是 -2 的情況了:因為上來,GenerateEvenSeries 方法就會自動初始化對象,并給 _state 賦值為 0,然后直接開始迭代,根本不需要先 -2 然后判斷是不是當前線程,然后才從 -2 改成 0 開始迭代的復雜行為。所以,綜上所述:

  • 返回 IEnumerableIEnumerable<> 接口的、帶有 yield 語句的方法

    • 翻譯為 Iterator 類型,并帶有 GetEnumerator 方法;

    • _state 包含 -2 這個情況;

    • 包含 _currentThreadId 字段。

  • 返回 IEnumeratorIEnumerator<> 接口的、帶有 yield 語句的方法

    • 翻譯為 Iterator 類型,不帶有 GetEnumerator 方法;

    • _state 不含 -2 這個情況;

    • 不含有 _currentThreadId 字段。

6-2 惰性迭代與無窮迭代

可以從前面的代碼里看出,迭代器類型實際上就是一個對象的實例化的過程,然后迭代才會執(zhí)行 MoveNext 的操作,才會有后面的循環(huán)迭代結果出來的行為。

如果我們實例化了一個 Iterator 類型的對象(或者之前講的 Enumerator 類型的對象)呢?可以從代碼輕松地看出,實際上實例化對象也就只是一個實例化而已,它沒有干別的事情,一個 Current 屬性自身也不會自動更新換代,所以它是“被動”的機制:你不使用 foreach,它就永遠不會得到執(zhí)行。因此,我們把實現正確的迭代器都稱為是一種惰性迭代(Lazy Iteration)過程。這個惰性(Lazy),可能我們以后還會用到,它就專門表示“被動”的感覺。所以,你可以從此總結出 IEnumerableIEnumerator 的第二個區(qū)別:IEnumerable 是集合,是一組已經有的數據,等待的是你的取值和使用;IEnumerator 是迭代器,是等待迭代過程的一種機制,等待的是你的啟動和執(zhí)行 foreach 循環(huán)的過程。

說到這里了,我們就來看看一種極端現象:無窮迭代(Infinity Iteration)。試一下如下的代碼:

是的,一個 while (true) 的死循環(huán)里帶有 yield return 語句。這個死循環(huán)里,Random.Shared.Next(1, 100) 作為表達式,作為每一個迭代的數值。但明顯可以看出,這個方法是有問題的。由于方法用到了死循環(huán),因此它無法自己終止,一旦執(zhí)行到第 3 行,就再也無法跳出來了。

那么思考一個問題。這樣的代碼是危險的嗎?是不是真的就意味著一定是無窮的迭代下去并且沒辦法終止?

實際上,并不是這樣的。無窮迭代在 .NET 的庫 API 里也或多或少用到過,而這種迭代只需要我們注意使用過程之中注意一下迭代過程,仍然是不會出問題的。你可能會覺得,迭代行為都固定了,怎么可能會改變?

這個方法的確固定了迭代的思路和執(zhí)行的內容,返回無窮無盡的數據;可是,你有沒有想過,我要是中途把序列給截斷呢?我們就拿這個例子來舉例。它不是返回無窮無盡的隨機數序列嗎?它返回值是 IEnumerable<int> 是吧?那么我們假設一種情況,實現代碼讓我們只需要取 10 個元素出來就可以了。怎么做?

這個方法是返回集合序列的是吧。那么:

看看這樣的代碼。我在 foreach 循環(huán)里加了個 if 判斷。如果 ++i > 10 就說明迭代超過 10 個元素了,此時退出 foreach 循環(huán)(break 語句可以跳出循環(huán)的,對吧)。

我們來試試看,運行起來是啥樣的。

是的,它確實生成了 10 個有模有樣的 1 到 100 之間的隨機數。所以,無窮盡的集合看來也不是危險的嘛。只要我們使用得當,這樣的迭代序列就可以完全掌控在我們手中。

另外,這樣的迭代行為,為了規(guī)范化的話,我們可以提取出一個單獨的方法來用:

我們通過迭代集合,然后重新 yield return 的方式來達到這一點。

至此我們就把 yield 語句的相關內容給大家介紹完畢了。好吧……說實話這一講的內容很難。放心,后面是新內容。


第 83 講:C# 2 之迭代器語句(二):`yield` 的底層的評論 (共 條)

分享到微博請遵守國家法律
个旧市| 柳江县| 长岛县| 安陆市| 常山县| 张家口市| 许昌县| 洛阳市| 侯马市| 耒阳市| 林口县| 城固县| 罗源县| 常德市| 郯城县| 蛟河市| 武邑县| 明光市| 广德县| 崇左市| 抚松县| 甘肃省| 敦煌市| 茶陵县| 普格县| 明溪县| 区。| 张家口市| 巩留县| 图们市| 那曲县| 南平市| 皮山县| 申扎县| 永仁县| 盘锦市| 南靖县| 长宁县| 同德县| 泾源县| 房产|