先放上gof中對於迭代器模式的介紹鎮樓面試
類圖以下sql
在平常工做中,咱們組負責的系統會常常與外部系統進行大量數據交互,大量數據交互的載體是純文本文件,咱們須要解析文件每一行的數據,處理後入庫,因此在咱們系統中就有了以下的代碼了。數據庫
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 }
使用迭代器模式,成功解耦了對文件系統的依賴,咱們能夠爲所欲爲地進行單元測試,數據載體的變更再也影響不到業務代碼。
上面的章節,我實現了經典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會告訴你