1. 程式人生 > 實用技巧 >C# 迭代器、列舉器、IEnumerable和IEnumerator

C# 迭代器、列舉器、IEnumerable和IEnumerator

開始之前先思考幾個問題:

  • 為什麼集合可以使用foreach來遍歷
  • 不用foreach能不能遍歷各元素
  • 為什麼在foreach中不能修改item的值?
  • 要實現foreach需要滿足什麼條件?
  • 為什麼Linq to Object中要返回IEnumerable?

一、列舉器和可列舉型別

1、什麼是可列舉型別?

可列舉類是指實現了IEnumerable介面的類,比如陣列就是可列舉型別;下面展示了一個可列舉類的完整示例:

namespace ConsoleApplication4
 {
     /// <summary>
     /// 自定義一個列舉物件
     /// </summary>
class ColorEnumerator : IEnumerator { private string[] _colors; private int _position = -1; public ColorEnumerator(string[] arr) { _colors = arr; for (int i = 0; i < arr.Length; i++) { _colors[i]
= arr[i]; } } public object Current { get { if (_position == -1) { throw new InvalidOperationException(); } if (_position >= _colors.Length) {
throw new InvalidOperationException(); } return _colors[_position]; } } public bool MoveNext() { if (_position < _colors.Length - 1) { _position++; return true; } else { return false; } } public void Reset() { _position = -1; } } /// <summary> /// 建立一個實現IEnumerable介面的列舉類 /// </summary> class Spectrum : IEnumerable { private string[] Colors = { "red", "yellow", "blue" }; public IEnumerator GetEnumerator() { return new ColorEnumerator(Colors); } } class Program { static void Main(string[] args) { Spectrum spectrum = new Spectrum(); foreach (string color in spectrum) { Console.WriteLine(color); } Console.ReadKey(); } } }
View Code

2、什麼是列舉器?

IEnumerable介面只有一個成員GetEnumerator方法,它返回的物件就是列舉器;實現了IEnumerator介面的列舉器包含三個函式成員:Current,MoveNext,Reset

  • Current是隻讀屬性,它返回object型別的引用;
  • MoveNext是把列舉器位置前進到集合的下一項的方法,它返回布林值,指示新的位置是否有效位置還是已經超過了序列的尾部;
  • Reset是把位置重置為原始狀態的方法;

3、為什麼集合可以使用foreach來遍歷

我們知道當我們使用foreach語句的時候,這個語句為我們依次取出了陣列中的每一個元素。

例如下面的程式碼:

int[] arr = { 1, 2, 3, 4, 5, 6 };
foreach( int arry in arr )
{
    Console.WriteLine("Array Value::{0}",arry);
}

輸出效果為

為什麼陣列可以使用foreach來遍歷?原因是陣列可以按需提供一個叫做列舉器(enumerator)的物件,列舉器可以依次返回請求的陣列中的元素,列舉器知道項的次序並且跟蹤它在序列中的位置。依次返回請求的當前項。

對於有列舉器的型別而言,必須有一個方法來獲取它這個型別。獲取一個物件列舉器的方法是呼叫物件的GetEnumrator方法,實現GetEnumrator方法的型別叫做可列舉型別。那麼陣列就是可列舉型別。

總結來說,實現GetEnumrator方法的型別叫做可列舉型別,GetEnumrator方法返回的物件就是列舉器,列舉器可以依次返回請求的陣列中的元素,列舉器知道項的次序並且跟蹤它在序列中的位置。依次返回請求的當前項。

下圖演示一下可列舉型別和列舉器之間的關係

foreach結構設計用來和可列舉型別一起使用,只要給它的遍歷物件是可列舉型別,比如陣列。基本邏輯如下:

  • 通過呼叫GetEnumrator方法獲取物件的列舉器。
  • 從列舉器中請求每一項並且把它作為迭代器,程式碼可以讀取該變數,但不可以改變
foreach(Type VarName in EnumrableObject )
{
}

EnumrableObjec必須是可列舉型別。

4、不用foreach能不能遍歷各元素?

當然是可以的,看下面程式碼:

二、迭代器

設計模式中有個迭代器模式,其實這裡說的迭代器就是利用迭代器設計模式實現的一個功能,返回的是列舉器。

1、自定義迭代器

.net中迭代器是通過IEnumerable和IEnumerator介面來實現的,換句話說,使用迭代器設計模式實現了IEnumerable和IEnumerator,返回的是列舉器。今天我們也來依葫蘆畫瓢。首先來看看這兩個介面的定義:

並沒有想象的那麼複雜。其中IEnumerable只有一個返回IEnumerator的GetEnumerator方法。而IEnumerator中有兩個方法加一個屬性。接下來開發畫瓢,我們繼承IEnumerable介面並實現:

下面使用原始的方式呼叫:

有朋友開始說了,我們平時都是通過foreache來取值的,沒有這樣使用過啊。好吧,我們來使用foreach迴圈:

為什麼說基本上是等效的呢?我們先看列印結果,在看反編譯程式碼。

由此可見,兩者有這麼個關係:

現在我們可以回答為什麼在foreach中不能修改item的值?

我們還記得IEnumerator的定義嗎

介面的定義就只有get沒有set。所以我們在foreach中不能修改item的值。

我們再來回答另一個問題:“要實現foreach需要滿足什麼條件?”:

必須實現IEnumerable介面?NO

我們自己寫的MyIEnumerable刪掉後面的IEnumerable介面一樣可以foreach(不信?自己去測試)。

所以要可以foreach只需要物件定義了GetEnumerator無參方法,並且返回值是IEnumerator或其對應的泛型。細看下圖:

也就是說,只要可以滿足這三步呼叫即可。不一定要繼承於IEnumerable。有意思吧!下次面試官問你的時候一定要爭個死去活來啊,哈哈!

2、yield的使用

你肯定發現了我們自己去實現IEnumerator介面還是有些許麻煩,並且上面的程式碼肯定是不夠健壯。對的,.net給我們提供了更好的方式。

你會發現我們連MyIEnumerator都沒要了,也可以正常執行。太神奇了。yield到底為我們做了什麼呢?

好傢伙,我們之前寫的那一大坨。你一個yield關鍵字就搞定了。最妙的是這塊程式碼:

這就是所謂的狀態機吧!

我們繼續來看GetEnumerator的定義和呼叫:

我們呼叫GetEnumerator的時候,看似裡面for迴圈了一次,其實這個時候沒有做任何操作。只有呼叫MoveNext的時候才會對應呼叫for迴圈:

現在我想可以回答你“為什麼Linq to Object中要返回IEnumerable?”:

因為IEnumerable是延遲載入的,每次訪問的時候才取值。也就是我們在Lambda裡面寫的where、select並沒有迴圈遍歷(只是在組裝條件),只有在ToList或foreache的時候才真正去集合取值了。這樣大大提高了效能。

如:

這個時候得到了就是IEnumerable物件,但是沒有去任何遍歷的操作。(對照上面的gif動圖看)

什麼,你還是不信?那我們再來做個實驗,自己實現MyWhere:

現在看到了吧。執行到MyWhere的時候什麼動作都沒有(返回的就是IEnumerable),只有執行到ToList的時候才程式碼才真正的去遍歷篩選。

這裡的MyWhere其實可以用擴充套件方法來實現,提升逼格。(Linq的那些查詢操作符就是以擴充套件的形式實現的)

3、怎樣高效能的隨機取IEnumerable中的值

三、IEnumrator介面

IEnumrator介面包含了3個函式成員:Current、MoveNext以及Reset;

.Current是返回序列中當前位置項的屬性。(注意:Current它是隻讀屬性,它返回Object型別的引用,所以可以返回任意型別)

.MoveNext是把列舉器位置前進到集合中下一項的方法。它也但會布林值,指示新的位置是否是有效位置。

注:如果返回的位置是有效的,方法返回true;

  如果新的位置是無效的,方法返回false;

  列舉器的原始位置在序列中的第一項之前,因此MoveNext必須在第一次使用Current之前呼叫。

.Reset是把位置重置為原始狀態的方法。

下面我們用圖表示一下他們之間的關係

有了集合的列舉器,我們就可以使用MoveNext和Current成員來模仿foreach迴圈遍歷集合中的項,例如,我們已經知道陣列是可列舉型別,所以下面的程式碼手動做foreach語句

自動做的事情。

程式碼如下:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 using System.Collections;
 7 
 8 namespace ConsoleApplication1
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14             int[] arr = { 1, 2, 3, 4, 5, 6 };
15             IEnumerator ie = arr.GetEnumerator();
16             while( ie.MoveNext() )
17             {
18                 int i = (int)ie.Current;
19                 Console.WriteLine("{0}", i);
20             }
21         }
22     }
23 }

程式執行的結果為

我們來用圖解釋一下程式碼中的陣列結構

IEnumerable介面

陣列是可列舉型別,是因為實現了IEnumerable介面的類,所以可列舉類都是因為實現了IEnumerable介面的類。

IEnumerable介面只有一個成員——GetEnumerator()方法,它返回物件的列舉器。

如圖所示:

下面我們舉一個使用IEnumerator和IEnumerable的例子

下面的程式碼展示了一個可列舉類的完整示例,該類叫Component(球形)。它的列舉器類為Shape(形狀)。

程式碼如下:

 1 using System;
 2 using System.Collections;
 3 
 4 namespace ConsoleApplication1
 5 {
 6     class Shape : IEnumerator
 7     {
 8         string[] _Shapes;
 9         int _Position = -1;
10 
11         public Shape(string[] _theShapes)
12         {
13             _Shapes = new string[_theShapes.Length];
14             for( int i = 0; i < _theShapes.Length; i++ )
15             {
16                 _Shapes[i] = _theShapes[i];
17             }
18         }
19 
20         public Object Current
21         {
22             get
23             {
24                 if ( _Position == -1 )
25                     throw new InvalidOperationException();
26                 if (_Position >= _Shapes.Length)
27                     throw new InvalidOperationException();
28                 return _Shapes[_Position];
29             }
30         }
31 
32         public bool MoveNext()
33         {
34             if (_Position < _Shapes.Length - 1)
35             {
36                 _Position++;
37                 return true;
38             }
39             else
40                 return false;
41         }
42 
43         public void Reset()
44         {
45             _Position = -1;
46         }
47     }
48 
49     class Component : IEnumerable
50     {
51         string[] shapes = { "Circular", "spherical", "Quadrilateral", "Label" };
52         public IEnumerator GetEnumerator()
53         {
54             return new Shape( shapes );
55         }
56     }
57 
58     class Program
59     {
60         static void Main(string[] args)
61         {
62             Component comp = new Component();
63             foreach ( string oshape in comp )
64                 Console.WriteLine(oshape);
65         }
66        
67     }
68 }

執行結果:

C#圖解教程 第十八章 列舉器和迭代器

https://www.cnblogs.com/qtiger/p/13571909.html