2021年了,`IEnumerator`、`IEnumerable`還傻傻分不清楚?

_小碼甲發表於2021-01-13

IEnumeratorIEnumerable這兩個介面單詞相近、含義相關,傻傻分不清楚。
入行多年,一直沒有系統性梳理這對李逵李鬼。

最近本人在懟著why神的《其實吧,LRU也就那麼回事》,方案1使用陣列實現LUR,手寫演算法涉及這一對介面,藉此機會本次覆蓋這一對難纏的冤家。

IEnumerator

IEnumerator、IEnumerable介面有相似的名稱,這兩個介面通常也在一起使用,它們有不同的用途。

IEnumerator介面為類內部的集合提供了迭代功能, IEnumerator 要求你實現三個方法:

  • MoveNext方法: 該方法將集合索引加1,並返回一個bool值,指示是否已到達集合的末尾。
  • Reset方法: 它將集合索引重置為其初始值-1,這會使列舉數無效。
  • Current方法: 返回position位置的當前物件

IEnumerable

IEnumerable介面為foreach迭代提供了支援,IEnumerable要求你實現GetEnumerator方法。

public IEnumerator GetEnumerator()
{
    return (IEnumerator)this;
}

該用哪一個介面?

僅憑以上辭藻,很難區分兩個介面的使用場景。

IEnumerator介面提供了對類中的集合型別物件的迭代

IEnumerable介面允許使用foreach迴圈進行列舉。

但是,IEnumerable介面的GetEnumerator方法會返回一個IEnumerator介面。要實現IEnumerable,你還必須實現IEnumerator。如果你沒有實現IEnumerator,你就不能將IEnumerableGetEnumerator方法的返回值轉換為IEnumerator介面。

從英文詞根上講:
IEnumerator介面代表了列舉器,裡面定義了列舉方式;
IEnumerable介面代表該物件具備了可被列舉的性質。

總之,IEnumerable的使用要求類實現IEnumerator。如果您想提供對foreach的支援,那麼就實現這兩個介面。

最佳實踐

  • 在巢狀類中實現IEnumerator,這樣你可以建立多個列舉器。
  • 為IEnumerator的Current方法提供異常處理。
    為什麼要這麼做?
    如果集合的內容發生變化,則reset方法將被呼叫,緊接著當前列舉數無效,您將收到一個IndexOutOfRangeException異常(其他情況也可能導致此異常)。所以執行一個Try…Catch塊來捕獲這個異常並引發InvalidOperationException異常, 提示在迭代時不允許修改集合內容。

這也正是我們常見的在foreach 裡面嘗試修改迭代物件會報InvalidOperationException異常的原因。

下面以汽車列表為例實現IEnumerator IEnumerable介面

using System;
using System.Collections;
namespace ConsoleEnum
{
    public class cars : IEnumerable
    {
        private car[] carlist;
  
        //Create internal array in constructor.
        public cars()
        {
            carlist= new car[6]
            {
                new car("Ford",1992),
                new car("Fiat",1988),
                new car("Buick",1932),
                new car("Ford",1932),
                new car("Dodge",1999),
                new car("Honda",1977)
            };
        }
        //private enumerator class
        private class  MyEnumerator:IEnumerator
        {
            public car[] carlist;
            int position = -1;

            //constructor
            public MyEnumerator(car[] list)
            {
                carlist=list;
            }
            private IEnumerator getEnumerator()
            {
                return (IEnumerator)this;
            }
            //IEnumerator
            public bool MoveNext()
            {
                position++;
                return (position < carlist.Length);
            }
            //IEnumerator
            public void Reset()
            {
                position = -1;
            }
            //IEnumerator
            public object Current
            {
                get
                {
                    try
                    {
                        return carlist[position];
                    }
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
            }
        }  //end nested class
      public IEnumerator GetEnumerator()
      {
          return new MyEnumerator(carlist);
      }
    }
}

foreach cars的時候,可以明顯看到

  • foreach語法糖初次接觸cars, 實際會進入cars實現的 GetEnumerator()方法
  • foreach每次迭代,實際會進入Current屬性的get訪問器

相關文章