《深入理解C#》整理4-迭代器

迭代器模式是行爲模式的一種範例,行爲模式是一種簡化對象之間通信的設計模式。這是一種非常易於理解和使用的模式。實際上,它允許你訪問一個數據項序列中的所有元素,而無須關心序列是什麼類型——數組、列表、鏈表或任何其他類型。它能非常有效地構建出一個數據管道,經過一系列不同的轉換或過濾後再從管道的另一端出來。

在.NET中,迭代器模式是通過IEnumerator和IEnumerable接口及它們的泛型等價物來封裝的。如果某個類型實現了IEnumerable接口,就意味着它可以被迭代訪問。調用GetEnumerator方法將返回IEnumerator的實現,這就是迭代器本身。C# 1利用foreach語句實現了訪問迭代器的內置支持。foreach語句被編譯後會調用GetEnumerator和MoveNext方法以及Current屬性,假如IDisposable也實現了,程序最後還會自動銷燬迭代器對象。

一、C# 1:手寫迭代器的痛苦

實現一個相對簡單的迭代器首先需要實現IEnumerable接口,以便使用者能夠輕鬆迭代集合中的所有值。其次需要實現GetEnumerator方法,以獲取當前迭代的對象信息。如果將GetEnumerator的實現放在與實現IEnumerable接口相同的類中,會存在一定的問題,比如使用兩個嵌套的foreach語句,那麼就要求兩個迭代器彼此獨立,而放在一個類中會導致分工不明確,代碼也會變得非常混亂,所有通常情況下會將GetEnumertor放在一個單獨實現的類中。

實現如下:

image-20201024153850541

二、C# 2:利用yield語句簡化迭代器

1、迭代器塊和yield return實現

1、C# 2中GetEnumerator方法的完整實現如下:

image-20201024155209604

yield return就是告訴C#編譯器,這個方法不是一個普通的方法,而是實現一個迭代器塊的方法。這個方法被聲明爲返回一個IEnumerator接口,所以就只能使用迭代器塊來實現返回類型爲IEnumerable、IEnumerator或泛型等價物的方法。如果方法聲明的返回類型是非泛型接口,那麼迭代器塊的生成類型(yield type)是object,否則就是泛型接口的類型參數。

yield return的限制:如果存在任何catch代碼塊,則不能在try代碼塊中使用yield return,並且在finally代碼塊中也不能使用yield return或yield break。這並非意味着不能在迭代器內部使用try/catch或try/finally代碼塊,只是說使用它們時有一些限制而已。

2、編寫迭代器塊時,儘管你編寫了一個似乎是順序執行的方法,但實際上是請求編譯器爲你創建了一個狀態機。這樣做的原因,和我們在C# 1的迭代器實現中塞入那麼多代碼的原因完全一樣——調用者每次只想獲取一個元素,所以在返回上一個值時需要跟蹤當前的工作狀態。

當編譯器看到迭代器塊時,會爲狀態機創建一個嵌套類型,來正確記錄塊中的位置以及局部變量(包括參數)的值。所創建的類類似於我們之前用普通方法實現的類,用實例變量來保存所有必要的狀態。要實現迭代器,狀態機需要以下事情:

  • 它必須具有某個初始狀態;
  • 每次調用MoveNext時,在提供下一個值之前(換句話說,就是執行到yield return語句之前),它需要執行GetEnumerator方法中的代碼;
  • 使用Current屬性時,它必須返回我們生成的上一個值;
  • 它必須知道何時完成生成值的操作,以便MoveNext返回false。

2、觀察迭代器的工作流程

image-20201024162831212

這個結果中有幾個重要的事情需要牢記:

  • 在第一次調用MoveNext之前,CreateEnumerable中的代碼不會被調用;
  • 所有工作在調用MoveNext時就完成了,獲取Current的值不會執行任何代碼;
  • 在yield return的位置,代碼就停止執行,在下一次調用MoveNext時又繼續執行;
  • 在一個方法中的不同地方可以編寫多個yield return語句;
  • 代碼不會在最後的yield return處結束,而是通過返回false的MoveNext調用來結束方法的執行。

第一點尤爲重要,因爲它意味着如果在方法調用時需要立即執行代碼,就不能使用迭代器塊,如參數驗證。如果你將普通檢查放入用迭代器塊實現的方法中,將不能很好地工作。

3、進一步瞭解迭代器執行流程

在常規的方法中,return語句具有兩個作用:第一,給調用者提供返回值;第二,終止方法的執行,在退出時執行合適的finally代碼塊。我們看到yield return語句臨時退出了方法,直到再次調用MoveNext後又繼續執行,我們根本沒有檢查finally代碼塊的行爲。

1、使用yield break結束迭代器的執行

yield break語句會終止迭代器的運行,讓當前對MoveNext的調用返回false。yield break語句的行爲非常類似於普通方法中的return語句。現在的問題是finally代碼塊如何執行及何時執行?

2、finally代碼塊的執行

在要離開相關作用域時,我們習慣執行finally代碼塊。迭代器塊行爲方式和普通方法不太一樣,儘管我們也看到,yield return語句暫時停止了方法,但並沒有退出該方法。按照這樣的邏輯,在這裏我們不要期望任何finally代碼塊能夠正確執行——實際也確實如此。不過,在遇到yield break語句時,適當的finally代碼塊還是能夠執行的。

finally在迭代器塊中常用於釋放資源,通常與using語句配合使用。foreach會在它自己的finally代碼塊中調用IEnumerator所提供的Dispose方法(就像using語句)。當迭代器完成迭代之前,你如果調用由迭代器代碼塊創建的迭代器上的Dispose,那麼狀態機就會執行在代碼當前“暫停”位置範圍內的任何finally代碼。簡單來說,只要調用者使用了foreach循環,迭代器塊中的finally將按照你期望的方式工作。

在迭代器完成之前終止它的執行是相當少見的,並且不用foreach語句而手動使用迭代器也是不多見的,不過如果你這樣做,記得把迭代器包含在using語句中使用

4、體實現中的奇特之處

微軟C#2編譯器對迭代器的實現有以下奇特之處值得了解:

  • 在第一次調用MoveNext之前,Current屬性總是返回迭代器產生類型的默認值;
  • 在MoveNext返回false之後,Current屬性總是返回最後的生成值;
  • Reset總是拋出異常,而不像我們手動實現的重置過程那樣,爲了遵循語言規範,這是必要的行爲;
  • 嵌套類總是實現IEnumerator的泛型形式和非泛型形式(提供給泛型和非泛型的IEnumerable所用)。

不實現Reset是完全合理的——編譯器無法合理地解決在重置迭代器時需要完成的一些事情,甚至不能判斷解決方法是否可行。可以認爲,Reset在IEnumerator接口中一開始就不存在,很多集合都不支持Reset,通常,調用者不能依賴它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章