1. 程式人生 > 實用技巧 >foreach原理分析

foreach原理分析

  我們知道通常foreach可以實現對型別的遍歷,但是foreach並不是針對所有型別都可以實現遍歷的功能,那麼我們可以思考這樣的一個問題:foreach對型別實施遍歷的依據條件是什麼?它是通過什麼方式來實現遍歷的?

  下面我們自定義一個型別來嘗試使用foreach進行遍歷,看會發生什麼樣的現象,並且以此作為出發點來一點點分析foreach的原理。

1.自定義型別並使用foreach遍歷

執行VS後編譯器提示了錯誤,根據錯誤描述可以推斷出foreach需要呼叫Person型別中的”GetEnumerator”方法。

2.在自定義型別中新增”GetEnumerator”方法

下一步我們根據編譯器的要求在Person型別中新增”GetEnumerator”方法,新增該方法後又出現如下的錯誤提示:

根據編譯器的錯誤提示,可以推斷出Person類的GetEnumerator方法的返回值型別必須要有MoveNext方法和Current屬性,示例中的object型別並沒有MoveNext方法和Current屬性。

3.新增MoveNext方法和Current屬性

  編譯器的錯誤提示要有MoveNext方法和Current屬性,但是我們無法搞清楚這兩個東西的具體實現形式,比如說MoveNext方法的返回值是什麼樣的?Current屬性是隻讀還是可讀可寫的?

  此時我們可以去已經支援foreach的型別中查詢GetEnumerator方法返回值型別,並對該型別的實現進行仿寫並用於我們自定義型別當中。string型別作為常用型別並支援foreach遍歷,下面我們檢視該string的GetEnumerator方法返回值型別進行仿寫。

仿寫後編譯通過:


以“數數”的思想來真正實現foreach遍歷的功能

  在生活中小朋友們往往通過數數的形式來完成對數學的啟蒙,foreach遍歷的思想就類似於“數數”。小朋友數數往往需要通過雙手來對事物進行數數,這就相當於GetEnumerator方法:其作用實際上是需要foreach當前遍歷的型別呼叫該方法提供一個用於遍歷當前的型別的“迭代計數器物件”,GetEnumerator方法的返回值型別:該型別其實就是用於foreach遍歷當前型別的“迭代計數器物件”,類似於計數的雙手(工具);Current屬性:就相當於當前數到的物件(foreach遍歷到的當前元素);MoveNext方法:就相當於“數數”中對元素不斷的移動的動作,促使讓“迭代計數器物件”的計數移至到下一位。

  到目前為止已經提供了編譯器錯誤提示的所有成員,但實際執行是毫無意義的,因為通常程式設計中實現遍歷的物件都是一個序列,但是目前示例中Person型別並不是且成員未包含序列。我們將為示例中Person型別新增一個序列,作為foreach“遍歷的物件”。


實現“數數的功能”

1.遍歷的資料物件

  新增foreach“遍歷的資料物件”後又有問題來了,foreach如何訪問到這個資料呢。我們可以將資料作為“迭代計數器物件”建構函式的引數在GetEnumerator方法中傳遞過去,然後“迭代計數器物件”中提供一個欄位進行儲存。

2.設定foreach遍歷到的當前元素(Current屬性)

  目前我們遍歷的“物件”是一個string陣列,所以要讀取這個數組裡的元素,我們需要一個下標索引來讀取遍歷到的當前元素,並作為Current屬性值。下面我們將設定一個int型別值為-1的欄位作為“讀取下標”,然後在Current屬性的get方法中通過下標索引器讀取“當前遍歷到的資料物件”。

3.MoveNext方法

  因為我們是通過下標去訪問元素的,所以需要對下標進行遞增進行變化,從而不斷指向下一個元素從而到達累計。MoveNext方法是一個布林的返回值型別,其主要目的是告知foreach當前遍歷的資料物件是否還存在沒有遍歷到的元素,如果存在元素則不斷遞增下標索引並反會true,反之返回false並結束遍歷,MoveNext方法的返回值相當於foreach遍歷的前提條件。

  下面我們通過程式碼來實現MoveNext方法:

到目前為止我們已經將一個不具備foreach條件的自定義型別,通過自編碼實現了foreach的基本要求。下面我們通過程式碼執行看看執行結果:

結果執行成功,並列印輸出遍歷的資料物件(Person中的陣列)。

注意:遍歷該陣列我們並沒有通過foreach直接對其進行的遍歷,而是結合foreach的流程本質和基本要求來實現的遍歷功能。


通過除錯來分析foreach的遍歷的原理

通過除錯結果我們可以總結下foreach遍歷主要依靠三個流程:

  1. foreach呼叫當前遍歷型別的GetEnumerator方法建立一個“迭代計數器物件”,並將遍歷的資料作為引數傳遞到物件建構函式中。(獲取迭代計數器物件)
  2. “迭代計數器物件”呼叫MoveNext方法將索引下標遞增(第一次遞增為0),如果遞增下標大於陣列長度則代表已經遍歷完。(呼叫MoveNext方法)
  3. MoveNext方法返回true,代表還有元素需要遍歷,使用當前下標在資料中獲取元素並設定為Current屬性值。(獲取Current屬性)

基於foreach的原理思想,我們還可以將遍歷寫成如下形式:

foreach的寫法其實就是呼叫上面的程式碼片段,從而實現的一種“語法糖”。


C#中基於原理的實現方式

  在上文中我們借鑑string型別中的GetEnumerator方法來參照實現“迭代計數器物件”當中的MoveNext方法和Current屬性。

  在.net中看某個型別是否支援使用foreach進行遍歷,其實接可以看該型別和該型別的“迭代計數器”是否都實現了IEnumerable介面。IEnumerable介面中的成員就包含了foreach實現的原理和需要呼叫的成員。

圖一:

圖二:


示例原始碼

 1   class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5           
 6             Person person = new Person();
 7             foreach (var item in person)
 8             {
 9                 Console.WriteLine(item);
10             }
11 
12             var Enumerator = person.GetEnumerator();
13             while (Enumerator.MoveNext())
14             {
15                 Console.WriteLine(Enumerator.Current);
16             }
17 
18         }  // END Main()
19 
20     }
Main

 1    class Person
 2     {
 3         string[] Datas = new string[] { "張三","李四","王五"};
 4 
 5         public PersonEnumerator GetEnumerator()
 6         {
 7             return new PersonEnumerator(Datas);
 8         }
 9     }
10 
11     /// <summary>
12     /// 迭代計數器
13     /// </summary>
14     class PersonEnumerator
15     {
16         public PersonEnumerator(string[] datas) { this.Datas = datas; }
17 
18         /// <summary>
19         /// 遍歷的資料物件
20         /// </summary>
21         private string[] Datas;
22 
23         private int index = -1;
24 
25         /// <summary>
26         /// 當前遍歷到的元素
27         /// </summary>
28         public string Current {
29             get { return Datas[index]; }
30         }
31 
32         /// <summary>
33         /// 將記錄指標移至下一條
34         /// </summary>
35         /// <returns>是否存在尚未遍歷的元素</returns>
36         public bool MoveNext()
37         {
38             index++;
39             return index < Datas.Length;
40         }
41 
42     }
Model