迭代器模式的一种应用场景以及C#对于迭代器的内置支持

迭代器模式

先放上gof中对于迭代器模式的介绍镇楼

  1. 意图
    提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
  2. 别名
    游标(Cursor)。
  3. 动机
    一个聚合对象, 如列表(list), 应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构. 此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个表列上同时进行多个遍历。迭代器模式都可帮你解决所有这些问题。这一模式的关键思想是将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器(iterator)对象中。迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素; 即, 它知道哪些元素已经遍历过了。

类图如下

工作中遇到的问题

在日常工作中,我们组负责的系统会经常与外部系统进行大量数据交互,大量数据交互的载体是纯文本文件,我们需要解析文件每一行的数据,处理后入库,所以在我们系统中就有了如下的代码了。

        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-无法扩展。

来析一下问题的根源

实际上这两个问题的根源都是因为直接依赖了文件系统。在我们的业务处理逻辑中,我们实际关心的是内容,而不是内容从何而来。如果内容格式不发生更改,业务逻辑代码就应该保持不变。文件作为内容的载体,可能会变为socket或者nosql数据库,如果这种情况一旦发生,难道把业务代码copy一份出来,然后把从文件读取数据改为从socket或者nosql读取?在进行单元测试时,我希望可以提供一个字符串数组就能对我的业务逻辑进行测试,而不是要提供一个文件。那么好了,我们要做的事情是将具体的数据来源隐藏掉,给业务代码提供一组API,让业务代码使用这组API可以获取到它所关心的内容。换句话说,我要提供一种方法来让人访问数据载体的元素,但是我并不像把数据载体暴露出来,这个目的简直跟迭代器模式的动机一毛一样呀。

开始动手改造

在文件解析场景中,文件就是迭代器模式中提到的聚合对象,文件中的每一行就是聚合对象的内部元素。这样我们先定义出迭代器接口和具体的文件迭代器

  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     }

而此时我们的业务代码变成了这样

  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会告诉你

时间: 2024-10-15 00:19:04

迭代器模式的一种应用场景以及C#对于迭代器的内置支持的相关文章

23种设计模式之迭代器模式(Iterator)

迭代器模式是一种对象的行为型模式,提供了一种方法来访问聚合对象,而不用暴露这个对象的内部表示.迭代器模式支持以不同的方式遍历一个聚合对象,复杂的聚合可用多种方法来进行遍历:允许在同一个聚合上可以有多个遍历,每个迭代器保持它自己的遍历状态,因此,可以同时进行多个遍历操作. 优点: 1)支持集合的不同遍历. 2)简化了集合的接口. 使用场景: 1)在不开发集合对象内部表示的前提下,访问集合对象内容. 2)支持集合对象的多重遍历. 3)为遍历集合中的不同结构提供了统一的接口. Iterator 模式

23种设计模式(13):迭代器模式

定义:提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节. 类型:行为类模式. 类图: 如果要问java中使用最多的一种模式,答案不是单例模式,也不是工厂模式,更不是策略模式,而是迭代器模式,先来看一段代码吧: ```java view plaincopy public static void print(Collection coll){ Iterator it = coll.iterator(); while(it.hasNext()){ String str = (Stri

【设计模式】迭代器模式

迭代器模式(Iterator Pattern)是 Java 和 .Net 编程环境中非常常用的设计模式.这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示. 迭代器模式属于行为型模式. 介绍 意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示. 主要解决:不同的方式来遍历整个整合对象. 何时使用:遍历一个聚合对象. 如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象. 关键代码:定义接口:hasNext, next. 应用实例:JAVA 中的

行为型模式之迭代器模式

概述 在软件开发中,我们经常需要使用聚合对象来存储一系列数据.聚合对象拥有两个职责:一是存储数据:二是遍历数据.从依赖性来看,前者是聚合对象的基本职责:而后者既是可变化的,又是可分离的.因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求. 定义 迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cu

C#设计模式(16)——迭代器模式(Iterator Pattern)

一.引言 在上篇博文中分享了我对命令模式的理解,命令模式主要是把行为进行抽象成命令,使得请求者的行为和接受者的行为形成低耦合.在一章中,将介绍一下迭代器模式.下面废话不多说了,直接进入本博文的主题. 二.迭代器模式的介绍 迭代器是针对集合对象而生的,对于集合对象而言,必然涉及到集合元素的添加删除操作,同时也肯定支持遍历集合元素的操作,我们此时可以把遍历操作也放在集合对象中,但这样的话,集合对象就承担太多的责任了,面向对象设计原则中有一条是单一职责原则,所以我们要尽可能地分离这些职责,用不同的类去

设计模式(十一):迭代器模式

一.概述 迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示. 二.解决问题 迭代器模式就是提供一种遍历元素的统一接口,用一致的方法遍历聚合元素.试想,如果我们的聚合元素是用不同的方式实现的,有些用了数组,有些用了java的集合类,或者还有其他方式,当客户端要遍历这些元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构. 三.应用实例 下面我用一个在页面展示一个学校院系结构的例子来讲解迭代器的用法,讲完例子后我们再来看看迭代器的结构类图.现在我们的需求是这样,要

16.设计模式_迭代器模式

一.引言 在上篇博文中分享了我对命令模式的理解,命令模式主要是把行为进行抽象成命令,使得请求者的行为和接受者的行为形成低耦合.在一章中,将介绍一下迭代器模式.下面废话不多说了,直接进入本博文的主题. 二.迭代器模式的介绍 迭代器是针对集合对象而生的,对于集合对象而言,必然涉及到集合元素的添加删除操作,同时也肯定支持遍历集合元素的操作,我们此时可以把遍历操作也放在集合对象中,但这样的话,集合对象就承担太多的责任了,面向对象设计原则中有一条是单一职责原则,所以我们要尽可能地分离这些职责,用不同的类去

迭代器模式(Iterator.hasNaxt())

迭代器模式(Iterator Pattern)是 Java 和 .Net 编程环境中非常常用的设计模式.这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示. 迭代器模式属于行为型模式. 介绍 意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示. 主要解决:不同的方式来遍历整个整合对象. 何时使用:遍历一个聚合对象. 如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象. 关键代码:定义接口:hasNext, next. 应用实例:JAVA 中的

Java源代码-迭代器模式

Java无疑是最成功的项目之一了,而在其中学习设计模式和架构设计,无疑是最好不过了. 概念: 提供一种方法访问容器中的各个元素,而又不暴露该对象的内部细节. 使用场景: 和容器经常在一起,我们定义了一个容器,还要提供外部访问的方法,迭代器模式无疑是最好不过了. 迭代器模式的UML类图: 下面的代码是Java集合框架内部实现迭代器模式的精简版: public interface Iterator<E> {//迭代器接口精简版 boolean hasNext(); E next(); } publ