关键词:迭代模式、foreach、迭代器、IEnumerable、IEnumerator、泛型
导言:这两天看迭代模式,一边学习,一边联系到.NET Framework的有关设计,小有收获,写下此文以供日后查看。这个关联学习的过程包括以下几个阶段:
学习迭代模式
à
想到了对应.NET中的foreach
à
在深入了解foreach的时候碰到了泛型
à
然后回归到.NET集合内的迭代,认识了到了“迭代器”的说法
一、 迭代模式
定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其中内部的表示。
关于迭代模式的正统解释和示例说明,不想多说,参考《HeadFirst》或《设计模式》。谈谈个人的理解:
首先,迭代模式出现在对集合遍历的需求上。
集合(C)有很多形式:数组、ArrayList、HashTable等等,如果要对这些集合的实例变量做遍历,大的方式和流程是一致的,但是具体实现代码不尽相同;
为了尽量减少代码数量和尽可能的实现逻辑抽象,就有人想出了Iterator(迭代接口),让这些集合都实现这个接口,以方便对接口的统一调用;
但是如此就违反了OO设计的“职责单一”原则,所以需要另外设置一个类(E)来实现Iterator接口,当然这个接口需要以集合为参数。
获取某个集合的迭代接口对象,可以放在E内实现,也可以放在C内实现。从用户角度考虑,面向用户的信息量要尽量精简、屏蔽实现细节,所以最好放在C内,这也不怎么违背集合C的职责单一性。
二、 Foreach
迭代模式不就是实现集合遍历嘛,这让我想到了.NET2.0新增的语法foreach,foreach就是用来对集合实现遍历的,所以我猜想能被foreach的集合可定实现了迭代,所以我马上参看了《C#语言规范》关于foreach的说明:
5.3.3.16 foreach 语句(因为没有参看上下文,没太看懂,抽时间再看)
8.8.4 foreach 语句(看懂了,说两句)
《C#语言规范》的相关全文不做全部转载,自行参考。我关心的有几句话:
首先是foreach的使用限制:在 foreach 语句执行期间,迭代变量表示当前正在为其执行迭代的集合元素。如果嵌入语句试图修改迭代变量(通过赋值或 ++ 和 -- 运算符)或将迭代变量作为 ref 或 out 参数传递,则将发生编译时错误。
为什么会这样呢?这是我的一个疑问,我后边会解释。
其次,《C#语言规范》明确说明了什么叫集合:如果类型 C 实现了 System.Collections.IEnumerable 接口,或者能够满足下列(有关实现集合模式的)条件,就称它是集合类型:
- C 包含一个 public 实例方法,它带有签名 GetEnumerator(),且返回值属于结构类型、类类型或接口类型(后文中用 E 表示该返回值的类型)。
- E 包含一个 public 实例方法,此方法具有签名 MoveNext() 和返回类型 bool。
- E 包含一个名为 Current 的 public 实例属性,此属性允许读取当前值。此属性的类型称为该集合类型的元素类型。
我为什么会留意这一点呢,因为规范上说:foreach 语句的表达式的类型必须是集合类型。
老实说,之前我对foreach的实现原理总有种神秘感,现在了解了其原理之后,手痒痒的厉害,就试着做了个测试:自己定义一个集合,但不实现IEnumerable接口,代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace 迭代
{
class Program
{
static void Main(string[] args)
{
MyIEnumerator mi = new MyIEnumerator();
foreach (int i in mi)
{
Console.WriteLine(i);
}
Console.Read();
}
}
class MyIEnumerator //: IEnumerator
{
int i;
public MyIEnumerator GetEnumerator()
{
return this;
}
public MyIEnumerator()
{
i = -1;
}
public object Current
{
get { return i; }
}
public bool MoveNext()
{
if (i < 3)
{
i++;
return true;
}
else
{
return false;
}
}
//public void Reset()
//{
// i = -1;
//}
}
}
这段程序测试通过,兴奋!不过兴奋归兴奋,以后实际使用还是继承IEnumerable来的规范一点。
最后,《规范》给出了foreach的背后扩展形式
E enumerator = (collection).GetEnumerator();
try {
while (enumerator.MoveNext()) {
ElementType element = (ElementType)enumerator.Current;
statement;
}
}
finally {
IDisposable disposable = enumerator as System.IDisposable;
if (disposable != null) disposable.Dispose();
}
并说,enumerator 变量是一个临时变量,它在嵌入 statement 中既是不可访问的,也是不可见的,元素变量在嵌入 statement 中是只读的。
这倒解释了,前边使用了foreach的限制,那enumerator为什么又是个临时变量呢?这又是个疑问留在后边解释。
三、 泛型
可爱的泛型,自从我了解到什么是泛型之后,我就有一种说不的快乐,哈哈,原来泛型这么好使,这里我不打算详细说我对泛型的理解,我对它的喜爱足以让我另立专题,这里我只想说说,我怎么从foreach找到了泛型。
我们一般新建一个cs文件(类文件)时,VS2005默认就给出了三个引用项
using System;
using System.Collections.Generic;
using System.Text;
我在认识IEnumerator接口的时候,尝试写这么一句话
IEnumerator E = list. GetEnumerator(),却发现编辑环境居然不自动提示IEnumerator这个单词,而且我写出来也不变色(是类或接口就应该变成绿色嘛),奇怪了(IEnumerator是声明在System.Collections命名空间内的,我忘了引用,要是出来才奇怪!)
,却出来了个IEnumerator<T>的提示,泛型?不了解,决定翻翻书……
关于泛型,我还想说的是,三个默认的引用文件就有其一个System.Collections.Generic,说明其何等的重要。
另外一句话,是刚刚看设计模式得到的“使用泛型来确保foreach的类型安全”,深感认同。(不过,原话不是这么说了,原话是说的java的for/in语法,我自己翻译了一下)
四、.NET中的迭代器
好了,再回到迭代模式。.NET Framework 已经将迭代模式做进了框架内所有定义的集合类型内,MSDN中称这个所有集合都能返回的接口IEnumerator为迭代器(和java的Iterator对应)。从单一职责的角度考虑,GetEnumerator()方法所返回的接口实例,应该是实现集合遍历的另外一个类,这个类呢?我通过Reflector查看源码没有看到,上网查了查,原来微软将迭代器做进了编译器,真绝!不仅如此,为了方便用户实现自己的迭代器,微软还为迭代器的快捷生成设置了 yield return 这样的语法,如下例。关于yield是什么,请自行参看MSDN帮助,我的白话理解是:能够允许连续返回多个值,而且返回的这多个值就构成了一个可以遍历的迭代器,这是编译器自己干的。
public class Persons : IEnumerable
{
string[] m_Names;
public Persons(params string[] Names)
{
m_Names = new string[Names.Length];
Names.CopyTo(m_Names,0);
}
public IEnumerator GetEnumerator()
{
foreach (string s in m_Names)
{
yield return s;
}
}
}
class Program
{
static void Main(string[] args)
{
Persons arrPersons = new Persons("Michel","Christine","Mathieu","Julien");
foreach (string s in arrPersons)
{
Console.WriteLine(s);
}
Console.ReadLine();
}
}
最后,顺便解释一下,foreach的约束问题(还记得吗?)。正因为迭代器是编译器默认生成的,而且是用户不可见的,而且是临时的,所以尝试对它的修改和引用注定是不被允许的,当然了,遍历看一看,读取一下总是可以的。
学习参考
生活TMD需要激情,做事需冷静,说话需冷静!
遇事记着:办法总比困难多,困难和问题说不定就是机遇和转折!
历史证明:哪个环节没照顾到,哪个环节就会出问题!能自己来,就不要让别人来。