1. 程式人生 > >我與C# yield不能說的秘密

我與C# yield不能說的秘密

ext not 執行 throw 朋友 text t權限 鏈表 處理異常

那一次的邂逅:

第一次見到yield的時候,內心中充滿了各種聲音,這是個啥子鬼扯扯的東西?C#有這個破玩意嗎?這是一個關鍵字?按捺不住內心的疑惑,熟練的打開了宇宙第一IDE ------ VS2015.

技術分享圖片

臥槽,還真有這個關鍵字.. 看一下解釋 "yield 關鍵字" ,可以,不和我多逼逼! 微軟大佬不愧是微軟大佬,就是這麽高冷.

魂牽夢繞:

面對大佬如此愛答不理的態度,勾引起了我單純內心的無限遐想,先從字面意思yield理解:產量,產出。難道這個是一個集合應當具有的屬性? 找到我們的老朋友List<T>:

技術分享圖片

從yield的字面意思上入手,我們把關註點放在IEnumerable接口上,微軟給出的解釋是:公開的枚舉數,該枚舉數支持在非泛型集合上進行簡單的叠代.

在C#要對一個集合進行遍歷有兩種方式:

1.For循環

2.Foreach循環

對於Foreach循環,我們要為這個集合定義一個叠代器才能使用微軟大佬給我們提供的福利(Foreach).

眉目傳情

控制不住自己的情緒,事不宜遲,先建立一個Test<T>類來實現IEnumerable來試一試:

class Test<T> : IEnumerable
        {

            public IEnumerator GetEnumerator()
            {
                throw
new NotImplementedException(); } }

從代碼中可以看出,要實現IEnumerable接口,必須要實現GetEnumerator()方法, 並且返回的類型是IEnumerator, 所以現在我們要先實現IEnumerator這個接口

class Test<T> : IEnumerable
        {

            public IEnumerator GetEnumerator()
            {
                throw new NotImplementedException();
            }

            
class TestEnumerator : IEnumerator { public object Current { get { throw new NotImplementedException(); } } public bool MoveNext() { throw new NotImplementedException(); } public void Reset() { throw new NotImplementedException(); } } }

實現了IEnumerator接口,必須要實現 Current屬性,以及MoveNext() 和 Reset()方法。 並且使用了嵌套類TestEnumerator,為什麽要使用嵌套類呢,客官莫急莫急,且聽臣娓娓道來.

Current : 簡單的說就是保存集合當前的值,並且只有get權限.

MoveNext():返回的是個bool類型的值,用來判斷集合是否還有下一個元素。 如果集合還有元素則返回true,反之亦然。

Rest():對集合進行重置操作.

為什麽要使用嵌套類呢? 因為我們要對Test<T>類的實例進行操作,如果我們使用了一個頂級類,那麽會得不到相應的數據(沒有權限查看).

添加實現得到的代碼如下:

class Test<T> : IEnumerable
        {
            private T[] values; //用於接收傳遞的數組
            public Test()
            {

            }
            public Test(T[] values) //構造函數初始化
            {
                this.values = values;
            }

            public IEnumerator GetEnumerator()
            {
                return new TestEnumerator(this); // 因為需要得到Test 實列的values.(所以使用嵌套類 有權限能訪問到values的值)
            }

            class TestEnumerator : IEnumerator
            {
                private Test<T> parent;
                private int index;//定義索引
                public TestEnumerator(Test<T> parent)
                {
                    this.parent = parent;
                    index = -1;// 在foreach的時候 會先去調用 GetEnumerator()方法, 所以先給定一個index=-1 初始化。
                }
                public object Current  //得到當前集合的值
                {
                    get
                    {
                        if (index == -1 || index == parent.values.Length)
                        {
                            throw new InvalidOperationException();
                        }
                        return this.parent.values[index];
                    }
                }

                public bool MoveNext() //是否還有剩余元素
                {
                    if (index != parent.values.Length)
                    {
                        index++;
                    }
                    return index < parent.values.Length;
                }

                public void Reset()
                {
                    index = -1; // 初始化索引值;
                }
            }
        }

在下比較懶,對於Test<T>的內部values只用了數組來存,有時間 我會更新成用鏈表的數據結構來實現這個方法,但是這個不是今天的重點..

測試下代碼:

static void Main(string[] args)
        {
            int[] values = { 1, 2, 3, 4, 5, 6 };
            Test<int> t = new Test<int>(values);

            // 在遍歷實列的時候
            // 1.會先去找到GetEnumerator()方法
            // 2.在去初始化 實現IEnumerator接口的類,並且初始化
            // 3. 在去MoveNext()判斷是否還有下一項
            // 4.輸出Current屬性, 此時代表的就是item
            foreach (var item in t)
            {
                Console.WriteLine(item);  // 1,2,3,4,5,6
            }
            Console.ReadKey();
        }

初次相識:

說了半天,怎麽看不到yield的影子,故弄玄虛,博主你是不是個睿智啊??? 不好意思,讓各位大爺久等了,現在閃亮的請出我們今天的主角 ”yield“。

在C#1.0要需要手動實現叠代器,洋洋灑灑幾十行的代碼,微軟大佬你就不能對小弟好一些,使用寫短小幹練的方式嗎? 於是乎,大佬決定相應小弟們的號召,使用關鍵字”yield“ 對叠代器進行封裝.. (微軟大佬就是好,沒事就給小弟們吃糖(語法糖),牙齒都給我吃的曲黑!!)

不多逼逼,簡單粗暴點,直接上代碼:

重新實現GetEnumerator()方法:

   public IEnumerator GetEnumerator()
            {
                for (var index = 0; index < values.Length; index++)
                {
                    yield return values[index];
                }
            }

簡單幾行代碼就能夠完全實現Test<T>類所需要的功能。方法看起來很普通,除了使用了yield return。這條語句告訴編譯器這不是一個普通的方法,而是一個需要執行的叠代塊(yield block),他返回一個IEnumerator對象你能夠使用叠代塊來執行叠代方法並返回一個IEnumerable需要實現的類型,IEnumerator或者對應的泛型。如果實現的是非泛型版本的接口,叠代塊返的yield type是Object類型,否則返回的是相應的泛型類型。例如,如果方法實現IEnumerable<String>接口,那麽yield返回的類型就是String類型。 在叠代塊中除了yield return外,不允許出現普通的return語句。塊中的所有yield return 語句必須返回和塊的最後返回類型兼容的類型。舉個例子,如果方法定義需要返回IEnumeratble<String>類型的話,不能yield return 1 。 需要強調的一點是,對於叠代塊,雖然我們寫的方法看起來像是在順序執行,實際上我們是讓編譯器來為我們創建了一個狀態機。這就是在C#1中我們書寫的那部分代碼---調用者每次調用只需要返回一個值,因此我們需要記住最後一次返回值時,在集合中位置。 當編譯器遇到叠代塊是,它創建了一個實現了狀態機的內部類。這個類記住了我們叠代器的準確當前位置以及本地變量,包括參數。這個類有點類似與我們之前手寫的那段代碼,他將所有需要記錄的狀態保存為實例變量。

春宵一刻:

利用yield,可以簡單的實現Linq中的where條件過濾(惰性過濾)。

在Test<T>中添加where方法:

public IEnumerable<T> where(Predicate<T> predicate)
            {
                foreach (T item in values)
                {
                    if (predicate(item))
                    {
                        yield return item;
                    }
                }
            }

測試:

static void Main(string[] args)
        {
            int[] values = { 1, 2, 3, 4, 5, 6 };
            Test<int> t = new Test<int>(values);

            foreach (var item in t.where(x => x < 5))
            {
                Console.WriteLine(item);  // 1,2,3,4
            }
            Console.ReadKey();
        }

賢者時間:

C#對許多設計模式進行了間接的實現,使得實現這些模式變得很容易。相對來針對某一特定的設計模式直接實現的的特性比較少。從foreach代碼中看出,C#1對叠代器模式進行了直接的支持,但是沒有對進行叠代的集合進行有效的支持。對集合實現一個正確的IEnumerable很耗時,容易出錯也很枯燥。在C#2中,編譯器為我們做了很多工作,為我們實現了一個狀態機來實現叠代。

代碼中還有些瑕疵的地方,比如沒有處理異常,沒有使用泛型的接口等。還有yield break的時候,需要釋放資源的問題,具體的細節,有時間在寫把!

微軟大佬,法力無邊,使我螺旋升天。--------- by 沒有對象的野指針

我與C# yield不能說的秘密