農林漁牧網

您現在的位置是:首頁 > 林業

C 核心-迭代器揭秘

2022-09-26由 張猿外 發表于 林業

比爾蓋茨的拼音怎麼拼

迭代器,我想大家都聽說過,但是我想這塊內容很容易讓人忽視,或許你每天都在用,但是你不自知。我想你肯定知道什麼是foreach,因為這個你每天都在用。你肯定看到過IEnumerable,IEnumerator這兩個介面,看到過關鍵字yield,但是或許你只是在一些微軟提供的資料結構裡面看到過,你自己並沒有去實現過。

迭代器是一種設計模式,並不是語言層面的。我想說的意思是迭代器不侷限程式語言,也就是他不是C#專有,其他很多語言都可以實現。他的目的是“構建出一個數據管道,把源頭資料透過一系列的轉換和過濾變成你想要的資料”,為什麼我要用引號,因為這句話並不好理解。首先在C#裡面,只要實現了IEnumerable的類就是實現了迭代器設計模式的類,就能夠在foreach裡面遍歷。反過來說,能放在foreach遍歷的物件都是實現了IEnumerable的迭代器類,而IEnumerable實現類裡面利用了IEnumerator這個實現體,所以IEnumerable 和 IEnumerator是密不可分的。但是C#裡面一定是隻能實現了IEnumerable,IEnumerator才是實現了迭代器嗎,當然不是,我說了迭代器是設計模式,不侷限於C#,js都可以。

接下來我通過幾個例子說明怎麼使用,請看下圖。

C 核心-迭代器揭秘

圖1

C 核心-迭代器揭秘

圖2

C 核心-迭代器揭秘

圖3

C 核心-迭代器揭秘

圖4

C 核心-迭代器揭秘

圖5

不忘初心,方得始終,我接下來舉的所有例子的目的都是在說明迭代器的作用,就是那句話。“構建出一個數據管道,把源頭資料透過一系列的轉換和過濾變成你想要的資料”。

請看上圖3,4,我想要把“a”,“b”,“c”,“d”,“e”這個陣列按照我想要的順序顯示出來,比如按照“c”開頭的,“c”,“d”,“e”,“a”,“b”,前者的資料是“源頭資料”,後者是“你想要的資料”。我構建了兩個類

InterationSample,InterationSampleIterator,

前者實現了IEnumerable,後者實現了IEnumerator。我們先看看這兩個介面定義的方法,請看圖1,圖2。 兩個介面都很簡單,

IEnumerable定義了一個方法GetEnumerator,返回IEnumerator介面。。

IEnumerator介面定義了兩個方法和一個屬性,我們看這兩個實現類。

InterationSample實現了GetEnumerator,返回的是IEnumerator介面,

也就是IterationSampleIterator這個類,

InterationSample建構函式兩個引數,一個是原始資料陣列,

另外一個是輸出資料的起始標記位。

IterationSample很簡單,foreach迴圈開始,

首先會呼叫GetEnumerator方法獲取IEnumerator物件,然後執行MoveNext方法,判斷是否需要繼續執行下去。

“position++”之後position等於4,

“var ret = position <= parent。startingPoint + parent。values。Length;”

“parent。startingPoint”是3,

“parent。values。Length”是5,

那麼4<8成立,說明迭代器繼續。為什麼這麼判斷,我們先看後面的程式碼。

當然MoveNext方法返回true,那麼foreach就會取Current這個屬性的值。

C 核心-迭代器揭秘

圖6

可以看出來,“index=position”,等於4,然後

“index=(index-2)%parent。values。Length”,

“index=(4-2)%5”,

取餘數等結果等於2。

然後返回“parent。values[index]”也就是第三個資料“c”。看結果圖5,顯示出來了“c”,

也就是Current的屬性值給了foreach裡面變數x,所以顯示出來“c”。

這個也是我們預期的,因為我們正想從“c”開始輸出。接著繼續執行MoveNext方法,

“position++”之後position等於5,

“var ret = position <= parent。startingPoint + parent。values。Length;”

“parent。startingPoint”是3,

“parent。values。Length”是5,

那麼5<=8成立。

說明迭代器繼續,然後呼叫Current get屬性,

“index=(index-2)%parent。values。Length”,

“index=(5-2)%5”,

取餘數等結果等於3,

然後返回“parent。values[index]”也就是第三個資料“d”。

因為Current獲取陣列的資料是按照索引來的,所以透過取餘方式獲取索引比較合適,這也 說明為什麼MoveNext方法要用

“var ret = position <= parent。startingPoint + parent。values。Length;”

這句話進行判斷了。大家想一下這個邏輯就知道了。

大家可能對於foreach怎麼執行程式碼搞不清楚,才導致理解上面的例子需要有點時間。那麼我換另外一種方式遍歷迭代器。請看下圖。

C 核心-迭代器揭秘

圖7

這個程式碼就清晰多了,而且和我們上面描述的過程是一樣的,結果也是一樣的。我想這個程式碼非常清楚就不再描述了。

看到這裡你有沒有感到疑問?要實現從“a”,“b”,“c”,“d”,“e”到“c”,“d”,“e”,“a”,“b”的轉換,或者“d”,“e”,“a”,“b”,“c”,我tm需要搞得這麼複雜麼,我使用for迴圈也就幾行程式碼就搞定了。我想和你說是的,我們確實第一念頭就是直接透過for迴圈,寫的程式碼也少。但是我想和你說的是,這裡我只是想要循序漸進地往下說,慢慢你就明白用迭代器的好處在哪裡了。我們回過頭再看看這個程式碼。

C 核心-迭代器揭秘

圖8

確實程式碼有點多,現在我要引入yield關鍵字了。請看下圖。

C 核心-迭代器揭秘

圖8

C 核心-迭代器揭秘

圖9

先說明一下,foreach C#1。0就有了,也就是上面的程式碼C#1。0大體就能執行。yield是C#2。0才引入的。請看下面的C#各個版本升級的內容

C 核心-迭代器揭秘

圖10

讓你明白yield關鍵字的作用,我還得和你說另外一個話題,就是C#程式碼執行的過程,這裡我想長話短說。首先C#編譯器會將C#程式碼編譯成IL(中間語言),元資料。dll大家都知道,他裡面大體就是這兩樣東西。我想只介紹IL語言,因為這個話題應該另外起一個文章介紹才行。

IL到底是個啥,有什麼用呢?原因就是因為。NET平臺不止C#一種語言,還有vb,f#等很多種語言,你開心的話你也可以搞一個語言出來。那麼這些語言語法會有不同,微軟就想要透過一箇中間語言統一這些語言,讓這些語言透過各自編譯器生成一樣的程式碼,也就是IL中間語言。那麼後面這些程式碼就統一處理了。也就是微軟說我支援你發明一個新語言,我不管你發明的語言是中文還是漢語我都不在乎,只要你的編譯器生成的程式碼是IL程式碼,那麼就可以在我。NET平臺執行。執行的過程會用到JIT編譯器,會將IL程式碼生成機器語言,也就是組合語言,這樣cpu就可以執行了。

為什麼我要扯開話題聊一下C#程式碼執行過程,就是因為在我們學習過程中會發現C#一直在升級,目前。NET6 已經到了C# 10,各種語法糖讓我們更加方便地使用,但是也增加了複雜度(都是一些什麼玩意每天升級讓人一直得去學習)。但是我想告訴大家的是,IL語言改變不大,IL歸根到底還是高階語言,有類,物件,方法,欄位,屬性,事件,委託等,也就是說不管C#再怎麼升級,都是編譯器變的魔術,最終生成的IL語言和前面版本的C#是差不多的。當然非同步函式也是編譯器變的一種魔術。

IL語言我們可以透過工具生成,這裡我用的是ilspy,也就是dll生成IL,然後也有工具可以從IL生成C#程式碼,可以使用IL dasm。我們接下來把這個程式碼,生成IL。

C 核心-迭代器揭秘

圖11

這裡我不解釋IL語言了,這是另外一個章節需要講的事情了。

現在我們想一下,既然各種版本的C#生成的IL語言差不多,那麼我們直接把這份程式碼降級到C#1。0,ILSpy工具這個功能。看看C#1。0程式碼是怎麼樣的。

C 核心-迭代器揭秘

圖12

C 核心-迭代器揭秘

圖13

C 核心-迭代器揭秘

圖14

大家發現沒,yield程式碼已經消失了,與之出現的是實現了IEnumerator的類。這個和我們上面舉的例子是一樣的。遍歷過程中yield return執行的次數,對應這個實現了IEnumerator的類對應的狀態欄位記錄他,請看這個“<>1_state”欄位。看過我《C#核心- async await》文章的朋友,這篇文章裡面提到的狀態機類和這裡的這個迭代器類兩個類的程式碼是不是有點相似性?相似性就在於都記錄了狀態,狀態機類記錄的是執行了多少次非同步await,這邊迭代器類是記錄了執行了多少次yield return。而且一個是非同步迭代,而這裡是同步迭代,而且都有“MoveNext”方法,我們可以好好體會體會。

上面我想說明的是yield只是編譯器的一個語法糖,目的就是快速構建一個迭代器類。

我們回過頭再看一下用yield實現的迭代器類

C 核心-迭代器揭秘

15

C 核心-迭代器揭秘

16

C 核心-迭代器揭秘

17

這個寫法還能再次簡化。如圖

C 核心-迭代器揭秘

圖18

C 核心-迭代器揭秘

圖19

C 核心-迭代器揭秘

圖20

可以看到InterationSample4這個方法,沒有看見實現IEnumerable的類了,同樣也沒有實現IEnumerator的類了。直接透過yield返回IEnumerable。根據上面所說的一樣,我們把它降級,看看會怎麼樣。請看下圖。

C 核心-迭代器揭秘

圖21

C 核心-迭代器揭秘

圖22

你會發現看不到yield關鍵字了,編譯器幫我們生成實現IEnumerable,IEnumerator的類,也就是和我們之前討論的一樣了。但是現在的寫法更加的簡單了。

我們在之前問了一個問題,就是把“a”,“b”,“c”,“d”,“e”,輸出為“c”,“d”,“e”,“a”,“b”,或者“d”,“e”,“a”,“b”,“c”為什麼需要迭代器這麼複雜,我直接透過一個for迴圈就可以做到。即使你加入一個yield,即使簡化到一個方法的方式實現迭代器,也沒有for迴圈實現得那麼快,或者說看不出來差距。我們看下for怎麼實現這個程式碼

C 核心-迭代器揭秘

圖23

異常簡單。不過我在上面也說了我只是想要循序漸進。接下來,我在提出一個需求,就是除了讓能讓這個“a”,“b”,“c”,“d”,“e”原始資料能夠按照我想要的順序的基礎上,將順序變化之後的陣列的每個成員成為一個型別物件的欄位值,比如物件是Class1,欄位是Property1。請看下圖。

C 核心-迭代器揭秘

圖24

C 核心-迭代器揭秘

圖25

我又建了一個InterationSample5方法,用於將之前改變順序的資料透過迭代器的方式組裝成物件並輸出出來。我們看一下C#裡面是怎麼樣子的。

C 核心-迭代器揭秘

圖26

可以看到生成了兩個迭代器類。我們看一下外層迭代器的MoveNext 方法。

C 核心-迭代器揭秘

圖27

C 核心-迭代器揭秘

圖28

C 核心-迭代器揭秘

圖29

檢視圖12程式碼“<>

1

__state=0”,moveNext方法裡面會遞迴“d__3”這個迭代器,而這個迭代器的作用就是我們之前的例子,拿到變更順序之後的成員,然後在“d__2”這個迭代器裡面把結果拿到組裝成物件返回。這裡迭代器的巢狀,其實就是外層迭代器呼叫MoveNext方法,然後MoveNext方法呼叫內部迭代器的MoveNext方法,拿到資料處理,然後返回。就像我們開篇說的那句話一樣。“構建出一個數據管道,把源頭資料透過一系列的轉換和過濾變成你想要的資料”。這個過程就像把很多蘋果放到機器貓的口袋裡面,伸手進去,一個一個拿出來,經過了幾道工序,拿出來的時候發現,蘋果變成削去了皮的蘋果,然後又變成了蘋果泥,最終拿出來的時候已經是包裝好的蘋果醬了。

重點是一個一個的順序,直到口袋裡面沒有蘋果了,我們可以看到外層迭代器,foreach的是IEnumerable,我們需要知道他成員數量嗎,這裡我們知道成員數量,因為陣列是我們傳進去的。但是如果這個陣列不是傳進去的,而是和下圖這樣。

C 核心-迭代器揭秘

圖30

C 核心-迭代器揭秘

圖31

外層並不知道成員數量,所以不能把IEnumerable當做一個數據結構,因為資料結構往往我們可以知道有多少個成員,有Count,或者Length屬性,而這個只能看做序列,不知道成員數量,只能透過foreach一個一個獲取資料。上面這個例子我們也沒有拿到排序後的資料“d”,“e”,“a”,“b”,“c”,這只是一箇中間狀態。

我們再來看一下InterationSample5的返回值,IEnumerable。這個是泛型迭代器,這個也很簡單,我們看下實現就行了。

C 核心-迭代器揭秘

圖32

C 核心-迭代器揭秘

圖33

也就是如果要返回泛型迭代器,那麼迭代器除了要實現IEnumerable,還要實現IEnumerable,這兩個接口裡面都有Current和MoveNext()方法。

我們上面兩個方法巢狀的迭代器實現起來依然不方便,而且透過一個for迴圈也能完成我們想要做的事情。我們只要在for迴圈裡面呼叫資料處理的方法(自己封裝一下) 不就行了。

但是我們回想一下,每次迭代器都是返回一個IEnumerable,這個是共性,我們能不能封裝一個方法,讓迭代器遞迴呼叫更加的方便,肯定可以!請看下圖。

C 核心-迭代器揭秘

圖34

為什麼這麼寫,我就是想著迭代器的目的,還是那句話“構建出一個數據管道,把源頭資料透過一系列的轉換和過濾變成你想要的資料”。那我們想一下其實就是一個數據,多次透過一道工序,轉成另外一個數據。請看圖18,輸入IEnumerable enumerable,就是工序的初始序列,Func func,就是工序。我們迭代原始資料,用我們傳入的委託執行這道工序,返回我們要的目標序列,也就是IEnumerable。好了,我們改一下我們之前的程式碼。

C 核心-迭代器揭秘

圖35

C 核心-迭代器揭秘

圖36

C 核心-迭代器揭秘

圖37

C 核心-迭代器揭秘

圖38

我們將原始資料,第一步改變順序,第二步封裝到Class1類,第三步,給Class1類Level欄位賦值,第四步,把Class1類分裝到Class2類裡面。

按照我們之前的寫的方式,需要寫4個方法,然後迭代,而且先呼叫最後的工序,然後裡面呼叫前一個工序,看起來非常不直觀。而現在呢。

C 核心-迭代器揭秘

圖39

這樣我們是先拿到原始資料,然後透過封裝的擴充套件方法InterationSample4和Select方法進行一步一步的呼叫,最終得到我們想要的序列,這個過程非常直觀,工序是正序執行而非倒序,程式碼也很少。這種呼叫方式叫做“鏈式程式設計思想”,大家有沒有用過前端的jquery,也是這種程式設計。完全符合人的思維方式,就是每一步都看得懂,換一個人來看也很快能看懂。

再擴散想一想,如果有10道工序,都是處理這些資料的,那麼只要一直使用Select的方法就行了。用for迴圈去做有那麼直觀嗎?再擴散想一下,如果還要從圖23裡面的第二步也需要走別的工序而不是走3,4工序,走別的工序,是不是隻要將2的結果IEnumerable放在一個變數裡面,然後一邊走3,4工序,另外一個走5,6工序就行了。如下圖所示。

C 核心-迭代器揭秘

圖40

如果for迴圈的話是不是得寫很多程式碼,很多重複的程式碼,而且沒有圖24清楚。

不知道大家有沒有用過ef或者sqlsugar,他們用的時候就是和上面說的一樣的思想,“鏈式程式設計思想”。給大家看一下ef使用的程式碼。

C 核心-迭代器揭秘

圖41

C 核心-迭代器揭秘

這是一個查詢一段時間內的相關資料報表介面,都是這種目的很清楚的鏈式程式設計,看圖26,我就寫了一個迭代器,返回一段時間的序列,很多報表都有這個一段時間內的資料報表。很多人偏向每個報表裡面先寫for迴圈,拿到日期,然後再做後面處理,我這裡直接返回序列,後面就非常清楚了。看看ef寫法,其實內部都是擴充套件方法,迭代器,委託。這些也是linq的重要實現手段。

最重要的是我們得了解清楚他的內部是怎麼執行的,寥寥幾句程式碼,但是背後編譯器為我們生成了非常多的迭代器相關的程式碼。現在大家會體現到我開篇說的那個話了吧,就是迭代器可能我們每天都在用,只是我們不自知。