迭代器模式的一種應用場景以及C#對於迭代器的內置支持

迭代器模式

先放上gof中對於迭代器模式的介紹鎮樓面試

  1. 意圖
    提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示。
  2. 別名
    遊標(Cursor)。
  3. 動機
    一個聚合對象, 如列表(list), 應該提供一種方法來讓別人能夠訪問它的元素,而又不需暴露它的內部結構. 此外,針對不一樣的須要,可能要以不一樣的方式遍歷這個列表。可是即便能夠預見到所需的那些遍歷操做,你可能也不但願列表的接口中充斥着各類不一樣遍歷的操做。有時還可能須要在同一個表列上同時進行多個遍歷。迭代器模式均可幫你解決全部這些問題。這一模式的關鍵思想是將對列表的訪問和遍歷從列表對象中分離出來並放入一個迭代器(iterator)對象中。迭代器類定義了一個訪問該列表元素的接口。迭代器對象負責跟蹤當前的元素; 即, 它知道哪些元素已經遍歷過了。

類圖以下sql

6941baebjw1el54eldhl4j20al07v0t2

工做中遇到的問題

在平常工做中,咱們組負責的系統會常常與外部系統進行大量數據交互,大量數據交互的載體是純文本文件,咱們須要解析文件每一行的數據,處理後入庫,因此在咱們系統中就有了以下的代碼了。數據庫

        public void ParseFile(string filePath, Encoding fileEncoding)
        {
            FileStream fs = null;
            try
            {
                fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
                using (var sr = new StreamReader(fs, fileEncoding))
                {
                    fs = null;
                    string line = null;
                    while ( (line = sr.ReadLine()) != null )
                    {
                        //解析改行數據
                    }
                }
            }
            finally
            {
                if (fs != null)
                {
                    fs.Close();
                }
            }
        }

這樣子的代碼存在兩個問題:1-沒法進行單元測試 2-沒法擴展。c#

來析一下問題的根源

實際上這兩個問題的根源都是由於直接依賴了文件系統。在咱們的業務處理邏輯中,咱們實際關心的是內容,而不是內容從何而來。若是內容格式不發生更改,業務邏輯代碼就應該保持不變。文件做爲內容的載體,可能會變爲socket或者nosql數據庫,若是這種狀況一旦發生,難道把業務代碼copy一份出來,而後把從文件讀取數據改成從socket或者nosql讀取?在進行單元測試時,我但願能夠提供一個字符串數組就能對個人業務邏輯進行測試,而不是要提供一個文件。那麼好了,咱們要作的事情是將具體的數據來源隱藏掉,給業務代碼提供一組API,讓業務代碼使用這組API能夠獲取到它所關心的內容。換句話說,我要提供一種方法來讓人訪問數據載體的元素,可是我並不像把數據載體暴露出來,這個目的簡直跟迭代器模式的動機一毛同樣呀。數組

開始動手改造

在文件解析場景中,文件就是迭代器模式中提到的聚合對象,文件中的每一行就是聚合對象的內部元素。這樣咱們先定義出迭代器接口和具體的文件迭代器nosql

  1 public interface IIterator
  2     {
  3         void First();
  4         void Next();
  5         bool IsDone();
  6         string GetCurrentItem();
  7     }
  1 class FileIterator : IIterator
  2     {
  3         private readonly StreamReader _reader = null;
  4         private string _current = null;
  5         public FileIterator(string filePath, Encoding encoding)
  6         {
  7             _reader = new StreamReader(new FileStream(filePath, FileMode.Open, FileAccess.Read), encoding);
  8         }
  9 
 10         public void First()
 11         {
 12             Next();
 13         }
 14 
 15         public void Next()
 16         {
 17             _current = _reader.ReadToEnd();
 18         }
 19 
 20         public bool IsDone()
 21         {
 22             return _current == null;
 23         }
 24 
 25         public string GetCurrentItem()
 26         {
 27             return _current;
 28         }
 29     }

而此時咱們的業務代碼變成了這樣socket

  1 public void ParseFile(IIterator iterator)
  2         {
  3             for (iterator.First(); !iterator.IsDone(); iterator.Next())
  4             {
  5                 var current = iterator.GetCurrentItem();
  6                 Console.WriteLine(current);
  7                 //對數據進行處理
  8             }
  9         }

經過迭代器模式,業務代碼對數據載體一無所知,按照給定的一組API,獲取想要的數據便可,當進行單元測試時,咱們能夠提供一個基於數組的迭代器,對業務代碼進行UT函數

class ArrayIterator:IIterator
    {
        private int _currentIndex = -1;
        private readonly string[] _array = null;

        public ArrayIterator(string[] array)
        {
            _array = array;
        }

        public void First()
        {
            Next();
        }

        public void Next()
        {
            _currentIndex++;
        }

        public bool IsDone()
        {
            return _currentIndex >= _array.Length;
        }

        public string GetCurrentItem()
        {
            return _array[_currentIndex];
        }
    }

問題並未徹底解決

細心的讀者已經發現了,在我上面實現的文件迭代器是存在問題的,由於我在構造函數裏打開了文件流,可是並無關閉它,因此按照C#裏的標準作法,文件迭代器要實現 IDisposable接口,咱們還要實現一個標準的Dispose模式,咱們的文件迭代器就變成了這樣。單元測試

  1 class FileIterator : IIterator,IDisposable
  2     {
  3         private StreamReader _reader = null;
  4         private string _current = null;
  5         private bool _disposed = false;
  6         private FileStream _fileStream = null;
  7         private readonly string _filePath = null;
  8         private readonly Encoding _encoding = null;
  9         public FileIterator(string filePath, Encoding encoding)
 10         {
 11             _filePath = filePath;
 12             _encoding = encoding;
 13         }
 14 
 15         public void First()
 16         {
 17             //原先在構造函數裏實例化StreamReader不太合適,轉移到First方法裏
 18             _fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
 19             _reader = new StreamReader(_fileStream, _encoding);
 20             _fileStream = null;
 21             Next();
 22         }
 23 
 24         public void Next()
 25         {
 26             _current = _reader.ReadToEnd();
 27         }
 28 
 29         public bool IsDone()
 30         {
 31             return _current == null;
 32         }
 33 
 34         public string GetCurrentItem()
 35         {
 36             return _current;
 37         }
 38 
 39         public void Dispose()
 40         {
 41             Dispose(true);
 42             GC.SuppressFinalize(this);
 43         }
 44 
 45         protected virtual void Dispose(bool disposing)
 46         {
 47             if (_disposed)
 48             {
 49                 return;
 50             }
 51             if (disposing)
 52             {
 53                 if (_reader != null)
 54                 {
 55                     _reader.Dispose();
 56                 }
 57                 if (_fileStream != null)
 58                 {
 59                     _fileStream.Dispose();
 60                 }
 61             }
 62             _disposed = true;
 63         }
 64 
 65         ~FileIterator()
 66         {
 67             Dispose(false);
 68         }
 69     }

配合此次改造,業務代碼也要作一些改變測試

  1 public void ParseFile(IIterator iterator)
  2         {
  3             try
  4             {
  5                 for (iterator.First(); !iterator.IsDone(); iterator.Next())
  6                 {
  7                     var current = iterator.GetCurrentItem();
  8                     Console.WriteLine(current);
  9                     //對數據進行處理
 10                 }
 11             }
 12             finally
 13             {
 14                 var disposable = iterator as IDisposable;
 15                 if (disposable != null)
 16                 {
 17                     disposable.Dispose();
 18                 }
 19             }
 20         }

使用迭代器模式,成功解耦了對文件系統的依賴,咱們能夠爲所欲爲地進行單元測試,數據載體的變更再也影響不到業務代碼。

C#早就看穿了一切

上面的章節,我實現了經典gof迭代器模式,實際上,迭代器模式的應用是如此的廣泛,以致於有些語言已經提供了內置支持,在C#中,與迭代器有關的有foreach關鍵字,IEnumerable,IEnumerable<T>,IEnumerator,IEnumerator<T>四個接口,看起來有四個接口,其實是2個,只是由於在 C#2.0版本以前未提供泛型支持,在這裏僅對兩個泛型接口進行討論。

在C#中,接口IEnumerator<T>就是迭代器,對應上面的Iterator,而IEnumerable<T>接口就是聚合對象,對應上面的Aggregate。在IEnumerable<T>中只定義了一個方法

public Interface IEnumerable<T>
{
    IEnumerator<T> GetEnumerator();
}

而foreach關鍵字c#專門爲了遍歷迭代器纔出現的,我面試別人的時候,特別喜歡問這樣一個問題:「知足什麼條件的類型實例才能夠被foreach遍歷?"看起來正確答案應該是實現了IEnumerable<T>接口的類型,實際上C#並不要求類型實現IEnumerable<T>接口,只要類型中定義了public IEnumerator<T> GetEnumerator()接口便可。

對於IEnumerator<T>接口,微軟已經想到了迭代器中可能會用到非託管對象(實際上微軟剛開始忽略了這個事情,因此最初的非泛型接口IEnumerator並無繼承IDisposable接口,直到2.0後才讓泛型接口IEnumerator<T>繼承了IDisposable),因此它的定義是這樣子的。

public interface IEnumerator<out T> : IDisposable, IEnumerator
{    
        new T Current {get;}
}

public interface IEnumerator
{
    bool MoveNext();
      
    Object Current {get;}
      
    void Reset();
}

在C#的IEnumerator<T>中,實際上將gof經典設計中的First(),IsDone()和Next()三個方法全都合併到了MoveNext()方法中,第一次迭代前現調用MoveNext(),並經過返回值判斷迭代是否結束,還額外提供了一個Reset方法來重置迭代器。當咱們使用foreach寫出遍歷一個對象的代碼時,編譯器會將咱們的代碼進行轉換。好比咱們如今要遍歷一個32位整型List

List<int> list = new List<int> {0,1,2,3,4};
foreach (var item in list)
{
   Console.WriteLine(item);
}

編譯時編譯器會將代碼變成相似下面這樣

List<int> list = new List<int> {0,1,2,3,4};
using (var enumerator = list.GetEnumerator())
{
        while (enumerator.MoveNext())
        {
            Console.WriteLine(enumerator.Current);
        }
}

繼續改造咱們的代碼

既然C#中已經內置了迭代器接口,咱們就沒有必要定義本身的IIterator接口了,直接使用IEnumerable<T>和IEnumerator<T>接口便可。

 class FileEnumerable : IEnumerable<string>
    {
        private readonly string _filePath;
        private readonly Encoding _fileEncoding;
        public FileEnumerable(string filePath, Encoding fileEncoding)
        {
            _filePath = filePath;
            _fileEncoding = fileEncoding;
        }
        public IEnumerator<string> GetEnumerator()
        {
            return new FileEnumerator(_filePath,_fileEncoding);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    }

  

public class FileEnumerator : IEnumerator<string>
    {
        private string _current;
        private FileStream _fileStream;
        private StreamReader _reader;
        private readonly string _filePath;
        private readonly Encoding _fileEncoding;
        private bool _disposed = false;
        private bool _isFirstTime = true;
        public FileEnumerator(string filePath, Encoding fileEncoding)
        {
            _filePath = filePath;
            _fileEncoding = fileEncoding;
        }
        public string Current
        {
            get { return _current; }
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            if (disposing)
            {
                if (_reader != null)
                {
                    _reader.Dispose();
                }
                if (_fileStream != null)
                {
                    _fileStream.Dispose();
                }
            }
            _disposed = true;
        }

        public bool MoveNext()
        {
            if (_isFirstTime)
            {
                _fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
                _reader = new StreamReader(_fileStream, _fileEncoding);
                _fileStream = null;
                _isFirstTime = false;
            }
            return (_current = _reader.ReadLine()) != null;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }

        ~FileEnumerator()
        {
            Dispose(false);
        }
    }

  而此時咱們的業務代碼變成了這樣子

public void ParseFile(IEnumerable<string> aggregate)
        {
            foreach (var item in aggregate)
            {
                Console.WriteLine(item);
                // //對數據進行處理
            }
        }

  在進行單元測試時,我能夠直接傳遞一個字符串數組進去了。

最終版本

看起來咱們對於代碼的重構已經完美了,可是實際上C#對於迭代器的內置支持要更完全,在上面,咱們必需要本身寫一個實現了IEnumerator<T>接口的類型,這個工做雖然不難,可是仍是有點繁瑣的,C# 針對迭代器模式,提供了yield return和yield break來幫助咱們更快更好的實現迭代器模式。下面是代碼重構的最終版本,咱們無需本身定義FileEnumerator類了

 class FileEnumerable : IEnumerable<string>
    {
        private readonly string _filePath;
        private readonly Encoding _fileEncoding;
        public FileEnumerable(string filePath, Encoding fileEncoding)
        {
            _filePath = filePath;
            _fileEncoding = fileEncoding;
        }
        public IEnumerator<string> GetEnumerator()
        {
            FileStream fileStream = null;
            try
            {
                fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
                using (var reader = new StreamReader(fileStream, _fileEncoding))
                {
                    fileStream = null;
                    string line = null;
                    while ((line = reader.ReadLine()) != null)
                    {
                        yield return line;
                    }
                    yield break;
                }
            }
            finally
            {
                if (fileStream != null)
                {
                    fileStream.Dispose();
                }
            }

        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    }

  這裏編譯器會根據咱們的代碼,結合yield return和yield break來幫助咱們生存一個實現了IEnumerator<string>接口的類型出來。

關於Dispose模式,和yield return,yield break本篇不作過多展開,有興趣的能夠找下資料,msdn會告訴你

相關文章
相關標籤/搜索