程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 匹夫細說C#:庖丁解牛迭代器,那些藏在幕後的秘密,

匹夫細說C#:庖丁解牛迭代器,那些藏在幕後的秘密,

編輯:C#入門知識

匹夫細說C#:庖丁解牛迭代器,那些藏在幕後的秘密,


0x00 前言

在匹夫的上一篇文章《匹夫細說C#:不是“棧類型”的值類型,從生命周期聊存儲位置》的最後,匹夫以總結和後記的方式涉及到一部分迭代器的知識。但是覺得還是不夠過瘾,很多需要說清楚的內容還是含糊不清,所以這周就專門寫一下c#中的迭代器吧。

0x01 你好,迭代器

首先思考一下,在什麼情景下我們需要使用到迭代器?

假設我們有一個數據容器(可能是Array,List,Tree等等),對我們這些使用者來說,我們顯然希望這個數據容器能提供一種無需了解它的內部實現就可以獲取其元素的方法,無論它是Array還是List或者別的什麼,我們希望可以通過相同的方法達到我們的目的。

此時,迭代器模式(iterator pattern)便應運而生,它通過持有迭代狀態,追蹤當前元素並且識別下一個需要被迭代的元素,從而可以讓使用者透過特定的界面巡訪容器中的每一個元素而不用了解底層的實現。

那麼,在c#中,迭代器到底是以一個怎樣的面目出現的呢?

如我們所知,它們被封裝在IEnumerable和IEnumerator這兩個接口中(當然,還有它們的泛型形式,要注意的是泛型形式顯然是強類型的。且IEnumerator<T>實現了IDisposable接口)。

IEnumerable非泛型形式:

//IEnumerable非泛型形式
[ComVisibleAttribute(True)]
[GuidAttribute("496B0ABE-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

IEnumerator非泛型形式:

//IEnumerator非泛型形式
[ComVisibleAttribute(true)]
[GuidAttribute("496B0ABF-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerator
{
    Object Current {get;}
    bool MoveNext();
    void Reset();
}

IEnumerable泛型形式:

//IEnumerable泛型形式
public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
    IEnumerator GetEnumerator(); 
}

IEnumerator泛型形式:

//IEnumerator泛型形式
public interface IEnumerator<out T> : IDisposable, IEnumerator
{

    void Dispose(); 
    Object Current {get;} 
    T Current {get;}
    bool MoveNext(); 
    void Reset(); 
}

[ComVisibleAttribute(true)]
public interface IDisposable
{
    void Dispose();
}

IEnumerable接口定義了一個可以獲取IEnumerator的方法——GetEnumerator()。

而IEnumerator則在目標序列上實現循環迭代(使用MoveNext()方法,以及Current屬性來實現),直到你不再需要任何數據或者沒有數據可以被返回。使用這個接口,可以保證我們能夠實現常見的foreach循環。

為什麼會有2個接口?

到此,各位看官是否和曾經的匹夫有相同的疑惑呢?那就是為何IEnumerable自己不直接實現MoveNext()方法、提供Current屬性呢?為何還需要額外的一個接口IEnumerator來專門做這個工作?

OK,假設有兩個不同的迭代器要對同一個序列進行迭代。當然,這種情況很常見,比如我們使用兩個嵌套的foreach語句。我們自然希望兩者相安無事,不要互相影響彼此。所以自然而然的,我們需要保證這兩個獨立的迭代狀態能夠被正確的保存、處理。這也正是IEnumerator要做的工作。而為了不違背單一職責原則,不使IEnumerable擁有過多職責從而陷入分工不明的窘境,所以IEnumerable自己並沒有實現MoveNext()方法。

迭代器的執行步驟

為了更直觀的了解一個迭代器,匹夫這裡提供一個小例子。

using System;
using System.Collections.Generic;

class Class1
{ 
    static void Main()
    {
        foreach (string s in GetEnumerableTest())
        {
            Console.WriteLine(s);
        }
    }

     static IEnumerable<string> GetEnumerableTest()
    {
        yield return "begin";
        
        for (int i=0; i < 10; i++)
        {
            yield return i.ToString();
        }
        
        yield return "end";
    }
}

輸出結果如圖:

OK,那麼匹夫就給各位捋一下這段代碼的執行過程。

這個例子中迭代器的執行過程,匹夫已經給各位看官簡單的描述了一下。但是還有幾點需要關注的,匹夫也想提醒各位注意一下。

  • 在第一次調用MoveNext()方法之前,我們自己在GetEnumerableTest中的代碼不會執行
  • 之後調用MoveNext()方法時,會從上次暫停(yield return)的地方開始。
  • 編譯器會保證GetEnumerableTest方法中的局部變量能夠被保留,換句話說,雖然本例中的i是值類型實例,但是它的值其實是被迭代器保存在堆上的,這樣才能保證每次調用MoveNext時,它是可用的。這也是匹夫上一篇文章中說迭代器塊中的局部變量會被分配在堆上的原因。

好啦,簡單總結了一下C#中的迭代器的外觀。那麼接下來,我們繼續向內部前進,來看看迭代器究竟是如何實現的。

0x02 原來是狀態機呀

上一節我們已經從外部看到了IEnumerable和IEnumerator這兩個接口的用法了,但是它們的內部到底是如何實現的呢?兩者之間又有何區別呢?

既然要深入迭代器的內部,這就是一個不得不面對的問題。

那麼匹夫就寫一個小程序,之後再通過反編譯的方式,看看在我們自己手動寫的代碼背後,編譯器究竟又給我們做了哪些工作吧。

為了簡便起見,這個小程序僅僅實現一個按順序返回0-9這10個數字的功能。

IEnumerator的內部實現

首先,我們定義一個返回IEnumerator<T>的方法TestIterator()。

//IEnumerator<T>測試
using System;
using System.Collections;

class Test
{
    static IEnumerator<int> TestIterator()
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

接下來,我們看看反編譯之後的代碼,探查一下編譯器到底為我們做了什麼吧。

internal class Test
{
    // Methods 注,此時還沒有執行任何我們寫的代碼
    private static IEnumerator<int> TestIterator()
    {
        return new <TestIterator>d__0(0);
    }

    // Nested Types 編譯器生成的類,用來實現迭代器。
    [CompilerGenerated]
    private sealed class <TestIterator>d__0 : IEnumerator<int>, IEnumerator, IDisposable
    {
        // Fields 字段:state和current是默認出現的
        private int <>1__state;
        private int <>2__current;
        public int <i>5__1;//<i>5__1來自我們迭代器塊中的局部變量,匹夫上一篇文章中提到過

        // Methods 構造函數,初始化狀態
        [DebuggerHidden]
        public <TestIterator>d__0(int <>1__state)
        {
            this.<>1__state = <>1__state;
        }
        // 幾乎所有的邏輯在這裡
        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<i>5__1 = 0;
                    while (this.<i>5__1 < 10)
                    {
                        this.<>2__current = this.<i>5__1;
                        this.<>1__state = 1;
                        return true;
                    Label_0046:
                        this.<>1__state = -1;
                        this.<i>5__1++;
                    }
                    break;

                case 1:
                    goto Label_0046;
            }
            return false;
        }

        [DebuggerHidden]
        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }

        void IDisposable.Dispose()
        {
        }

        // Properties
        int IEnumerator<int>.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }

        object IEnumerator.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }
    }
}

我們先全面的看一下反編譯之後的代碼,可以發現幾乎所有的邏輯都發生在MoveNext()方法中。那麼之後我們再詳細介紹下它,現在我們先從上到下把代碼捋一遍。

OK,IEnumerator接口我們看完了。下面再來看看另一個接口IEnumerable吧。

IEnumerator VS IEnumerable

依樣畫葫蘆,這次我們仍然是寫一個實現按順序返回0-9這10個數字的功能的小程序,只不過返回類型變為IEnumerable<T>。

using System;
using System.Collections.Generic;

class Test
{
    static IEnumerable<int> TestIterator()
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

之後,我們同樣通過反編譯,看看編譯器又背著我們做了什麼。

internal class Test
{
    private static IEnumerable<int> TestIterator()
    {
        return new <TestIterator>d__0(-2);
    }

    private sealed class <TestIterator>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
    {
        // Fields
        private int <>1__state;
        private int <>2__current;
        private int <>l__initialThreadId;
        public int <count>5__1;

        public <TestIterator>d__0(int <>1__state)
        {
            this.<>1__state = <>1__state;
            this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
        }

        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<count>5__1 = 0;
                    while (this.<count>5__1 < 10)
                    {
                        this.<>2__current = this.<count>5__1;
                        this.<>1__state = 1;
                        return true;
                    Label_0046:
                        this.<>1__state = -1;
                        this.<count>5__1++;
                    }
                    break;

                case 1:
                    goto Label_0046;
            }
            return false;
        }

        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
            if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
            {
                this.<>1__state = 0;
                return this;
            }
            return new Test.<TestIterator>d__0(0);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<Int32>) this).GetEnumerator();
        }

        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }

        void IDisposable.Dispose()
        {
        }

        int IEnumerator<int>.Current
        {
            get
            {
                return this.<>2__current;
            }
        }

        object IEnumerator.Current
        {
            get
            {
                return this.<>2__current;
            }
        }
    }
}

看到反編譯出的代碼,我們就很容易能對比出區別。

所以,從這些對比中我們能發現些什麼嗎?思考一下我們經常使用的一些用法,包括匹夫在上一節中提供的小例子。不錯,我們會創建一個IEnumerable<T>的實例,之後一些語句(例如foreach)會去調用GetEnumerator方法獲取一個Enumerator<T>的實例,之後迭代數據,最終結束後釋放掉迭代器的實例(這一步foreach會幫我們做)。(而最初我們得到的IEnumerable<T>實例,在第一次調用GetEnumerator方法獲得了一個Enumerator<T>實例之後就再沒有用到了。

而分析IEnumerable的GetEnumerator方法:

        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
            if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
            {
                this.<>1__state = 0;
                return this;
            }
            return new Test.<TestIterator>d__0(0);
        }

我們可以發現,-2這個狀態,也就是此時的初始狀態,表明了GetEnumerator()方法還沒有執行。而0這個狀態,則表明已經准備好了迭代,但是MoveNext()尚未調用過。

當在不同的線程上調用GetEnumerator方法或者是狀態不是-2(證明已經不是初始狀態了),則GetEnumerator方法會返回一個<TestIterator>d__0類的新實例用來保存不同的狀態。

0x03 狀態管理

OK,我們深入了迭代器的內部,發現了原來它的實現主要依靠的是一個狀態機。那麼,下面就讓匹夫繼續和大伙聊聊這個狀態機是如何管理狀態的。

狀態切換

根據Ecma-334標准,也就是c#語言標准的第26.2 Enumerator objects小節,我們可以知道迭代器有4種可能狀態:

而其中before狀態是作為初始狀態出現的。

在我們討論狀態如何切換之前,匹夫還要帶領大家回想一下上面提到的,也就是在調用一個使用了迭代器塊,返回類型為一個IEnumerator或IEnumerable接口的方法時,這個方法並非立刻執行我們自己寫的代碼的。而是會創建一個編譯器生成的類的實例,之後當調用MoveNext()方法時(當然如果方法的返回類型是IEnumerable,則要先調用GetEnumerator()方法),我們的代碼才會開始執行,直到遇到第一個yield return語句或yield break語句,此時會返回一個布爾值來判斷迭代是否結束。當下次再調用MoveNext()方法時,我們的方法會繼續從上一個yield return語句處開始執行。

為了能夠直觀的觀察狀態的切換,下面小匹夫提供一個類似於《深入理解C#》這本書中的例子:

class Test
{
    
    static IEnumerable<int> TestStateChange()
    {
        Console.WriteLine("----我TestStateChange是第一行代碼");
        Console.WriteLine("----我是第一個yield return前的代碼");
        yield return 1;
        Console.WriteLine("----我是第一個yield return後的代碼");

        Console.WriteLine("----我是第二個yield return前的代碼");
        yield return 2;
        Console.WriteLine("----我是第二個yield return前的代碼");
    }
    
    static void Main()
    {
        Console.WriteLine("調用TestStateChange");
        IEnumerable<int> iteratorable = TestStateChange();
        Console.WriteLine("調用GetEnumerator");
        IEnumerator<int> iterator = iteratorable.GetEnumerator();
        Console.WriteLine("調用MoveNext()");
        bool hasNext = iterator.MoveNext();
        Console.WriteLine("是否有數據={0}; Current={1}", hasNext, iterator.Current);
        
        Console.WriteLine("第二次調用MoveNext");
        hasNext = iterator.MoveNext();
        Console.WriteLine("是否還有數據={0}; Current={1}", hasNext, iterator.Current);

        Console.WriteLine("第三次調用MoveNext");
        hasNext = iterator.MoveNext();
        Console.WriteLine("是否還有數據={0}", hasNext);
    }
}

之後,我們運行這段代碼看看結果如何。

可見,代碼的執行順序就是匹夫剛剛總結的那樣。那麼我們將這段編譯後的代碼再反編譯回C#,看看編譯器到底是如何處理這裡的狀態切換的。

這裡我們只關心兩個方法,首先是GetEnumerator方法。其次是MoveNext方法。

[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
    if ((Environment.CurrentManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
    {
        this.<>1__state = 0;
        return this;
    }
    return new Test.<TestStateChange>d__0(0);
}

看GetEnumerator方法,我們可以發現:

我們再來看看MoveNext方法。

private bool MoveNext()
{
    switch (this.<>1__state)
    {
        case 0:
            this.<>1__state = -1;
            Console.WriteLine("----我TestStateChange是第一行代碼");
            Console.WriteLine("----我是第一個yield return前的代碼");
            this.<>2__current = 1;
            this.<>1__state = 1;
            return true;

        case 1:
            this.<>1__state = -1;
            Console.WriteLine("----我是第一個yield return後的代碼");
            Console.WriteLine("----我是第二個yield return前的代碼");
            this.<>2__current = 2;
            this.<>1__state = 2;
            return true;

        case 2:
            this.<>1__state = -1;
            Console.WriteLine("----我是第二個yield return前的代碼");
            break;
    }
    return false;
}

由於第一次調用MoveNext方法發生在調用GetEnumerator方法之後,所以此時狀態已經變成了0。

可以清晰的看到此時從0——>1——>2——>-1這樣的狀態切換過程。而且還要注意,每個分支中,this.<>1__state都會首先被置為-1:this.<>1__state = -1。之後才會根據不同的階段賦值不同的值。而這些不同的值也就用來標識代碼從哪裡恢復執行。

我們再拿之前實現了按順序返回0-9這10個數字的小程序的狀態管理作為例子,來讓我們更加深刻的理解迭代器除了剛剛的例子,還有什麼手段可以用來實現“當下次再調用MoveNext()方法時,我們的方法會繼續從上一個yield return語句處開始執行。”這一個功能的。

private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<i>5__1 = 0;
                    while (this.<i>5__1 < 10)
                    {
                        this.<>2__current = this.<i>5__1;
                        this.<>1__state = 1;
                        return true;
                    Label_0046:
                        this.<>1__state = -1;
                        this.<i>5__1++;
                    }
                    break;

                case 1:
                    goto Label_0046;
            }
            return false;
        }

如代碼中黃色色帶標出的語句,不錯,此時狀態機是靠著goto語句實現半路插入,進而實現了從yield return處繼續執行的功能。

好吧,讓我們總結一下關於迭代器內部狀態機的狀態切換:

  • -2狀態:只有IEnumerable才有,表明在第一次調用GetEnumerator之前的狀態。
  • -1狀態:即上文中提到的C#語言標准中規定的Running狀態,表明此時迭代器正在執行。當然,也會用於After狀態,例如上例中的case 2中,this.<>1__state被賦值為-1,但是此時迭代結束了。
  • 0狀態:即上文中提到的Before狀態,表明MoveNext()還一次都沒有調用過。
  • 正數(1,2,3...),主要用來標識從遇到yield之後,代碼從哪裡恢復執行。

0x04 總結

通過匹夫上文的分析,可以看出迭代器的實現的確十分復雜。不過值得慶幸的是很多工作都由編譯器在幕後為我們做好了。那麼,本文就到此結束。歡迎大家探討。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved