概念
迭代器模式提供了一种方法顺序访问一个聚合对象(理解为集合对象)中各个元素,而又无需暴露该对象的内部表示。
迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是简化对象间通讯的模式。
在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接口,那么就能够被迭代;调用GetEnumerator方法将返回IEnumerator接口的实现,
内建了对迭代器的支持,编译器会将foreach编译来调用GetEnumerator和MoveNext方法以及Current属性,如果对象实现了IDisposable接口,在迭代完成之后会释放迭代器。
迭代器是实现IEnumerator接口的对象,类似数据库中的游标,他是数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。
迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。(百度百科)
迭代器的执行过程
为了让大家更好的理解迭代器,下面列出迭代器的执行流程:
C#2中实现GetEnumerator方法的完整代码:
public IEnumerator GetEnumerator()
{
for (int index = 0; index < this.values.Length; index++)
{
yield return values[(index + startingPoint) % values.Length];
}
}
使用了yield return。这条语句告诉编译器不是普通的方法,而是一个需要执行的迭代块(yield block),他返回一个IEnumerator对象
创建了一个实现了状态机的内部类。这个类记住了我们迭代器的准确当前位置以及本地变量,包括参数。他将所有需要记录的状态保存为实例变量。下面来看看,为了实现一个迭代器,这个状态机需要按顺序执行的操作:
- 它需要一些初始的状态
- 当MoveNext被调用时,他需要执行GetEnumerator方法中的代码来准备下一个待返回的数据。
- 当调用Current属性是,需要返回yielded的值。
- 需要知道什么时候迭代结束是,MoveNext会返回false
注意:
- 直到第一次调用MoveNext,GetEnumerator()方法才被调用。
- 在调用MoveNext的时候,已经做好了所有操作,返回Current属性并没有执行任何代码。
- 代码在yield return之后就停止执行,等待下一次调用MoveNext方法的时候继续执行。
- 在方法中可以有多个yield return语句。
- 在最后一个yield return执行完成后,代码并没有终止。调用MoveNext返回false使得方法结束。
迭代器执行中的特殊行为:
- 在MoveNext方法第一次执行之前,Current属性总是返回迭代器返回类型的默认的值。例如IEnumeratble返回的是Int32类型,那么默认初始值是0,所以在调用MoveNext方法之前调用Current属性就会返回0。
- MoveNext方法返回false后,Current属性总是返回最后迭代的那个值。
- Reset方法一般会抛出异常,而在本文开始代码中,我们手动实现一个迭代器时在Reset中能够正确执行逻辑。
- 编译器为我们产生的嵌套类会同时实现IEnumerator的泛型和非泛型版本(恰当的时候还会实现IEnumerable的泛型和非泛型版本).
类型:
编译器遇到迭代块(yield block),他返回一个IEnumerator对象,你能够使用迭代块来执行迭代方法并返回一个IEnumerable需要实现的类型,IEnumerator或者对应的泛型。如果实现的是非泛型版本的接口,迭代块返的yield type是Object类型,否则返回的是相应的泛型类型。例如,如果方法实现IEnumerable<String>接口,那么yield返回的类型就是String类型。 在迭代块中除了yield return外,不允许出现普通的return语句。块中的所有yield return 语句必须返回和块的最后返回类型兼容的类型。
Finally语句块的执行
通常,finally语句块在当方法执行退出特定区域时就会执行。迭代块中的finally语句和普通方法中的finally语句块不一样。就像我们看到的,yield return语句停止了方法的执行,而不是退出方法,根据这一逻辑,在这种情况下,finally语句块中的语句不会执行。
但当碰到yield break语句的时候,就会执行finally 语句块,这根普通方法中的return一样。一般在迭代块中使用finally语句来释放资源,就像使用using语句一样。
其他
C#2.0 利用迭代器可以简单实现 GetEnumerator() 函数。迭代器是用于返回相同类型的值的有序集合的一段代码。利用 yield 关键字,实现控制权的传递和循环变量的暂存,使类或结构支持 foreach 迭代,而不必显式实现 IEnumerable 或 IEnumerator 接口,由 JIT 编译器辅助编译成实现了 IEnumerable 或 IEnumerator 接口的对象。yield return 提供了迭代器一个重要功能,即取到一个数据后马上返回该数据,不需要全部数据加载完毕,有效提高遍历效率(延迟加载)。
- yield 关键字用于指定返回的值,yield return 语句依次返回每个元素,yield break 语句终止迭代;
- 到达 yield return 语句时,保存当前位置,下次调用迭代器时直接从当前位置继续执行;
- 迭代器可以用作方法、运算符或get访问器的主体实现,yield 语句不能出现在匿名方法中;
- 迭代器返回类型必须为 IEnumerable、IEnumerator、IEnumerable<T> 或 IEnumerator<T>;
总结:
使用foreach必须实现IEnumerable,因为要获得迭代器,获得迭代器就必须实现IEnumerable接口中的GetEnumerator()方法
C#1.0 实现迭代器就必须实现IEnumerator接口中的bool MoveNext()和void Reset()方法
C#2.0 使用yield关键字 实质:编译器生成中间代码时为我们生成了一个IEnumerator接口的对象,
实质:foreach被编译后会调用GetEnumerator来返回一个迭代器,也就是一个集合中的初始位置。
因为迭代的主体在MoveNext()中实现(因为在MoveNext()方法中访问了集合中的当前位置的元素),Foreach中每次遍历执行到in的时候才会调用MoveNext()方法,所以迭代器可以延迟计算
1 编译器遇到yield return会生成IEnumerator接口迭代器对象 ,所以迭代块的代码在迭代器对象里面,延迟到第一次moveNext()。
2 Yield Return关键字的作用就是退出当前函数,并且会保存当前函数执行到什么地方,也就上下文。 (实质)编译器会生成一个状态机来维护迭代器的状态。
return 作用 1调用返回结果 2终止方法执行
yield return 短暂退出方法 等待下一次nextMove()执行
yield break 能够马上终止迭代 下次调用MoveNext()时直接返回false
yield return 后 finally方法不会执行
实现:
C#1.0迭代器的实现
class Program
{
static void Main(string[] args)
{
Friends friendcollection = new Friends();
foreach (Friend f in friendcollection)
{
Console.WriteLine(f.Name);
}
Console.Read();
}
}
/// <summary>
/// 朋友类
/// </summary>
public class Friend
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public Friend(string name)
{
this.name = name;
}
}
/// <summary>
/// 朋友集合
/// </summary>
public class Friends : IEnumerable
{
private Friend[] friendarray;
public Friends()
{
friendarray = new Friend[]
{
new Friend("张三"),
new Friend("李四"),
new Friend("王五")
};
}
// 索引器
public Friend this[int index]
{
get { return friendarray[index]; }
}
public int Count
{
get { return friendarray.Length; }
}
// 实现IEnumerable<T>接口方法
public IEnumerator GetEnumerator()
{
return new FriendIterator(this);
}
}
/// <summary>
/// 自定义迭代器,必须实现 IEnumerator接口
/// </summary>
public class FriendIterator : IEnumerator
{
private readonly Friends friends;
private int index;
private Friend current;
internal FriendIterator(Friends friendcollection)
{
this.friends = friendcollection;
index = 0;
}
#region 实现IEnumerator接口中的方法
public object Current
{
get
{
return this.current;
}
}
public bool MoveNext()
{
if (index + 1 > friends.Count)
{
return false;
}
else
{
this.current = friends[index];
index++;
return true;
}
}
public void Reset()
{
index = 0;
}
#endregion
}
C#1.0 当前代码实现步骤:
聚合对象,1 实现IEnumerable的GetEnumerator()方法返回IEnumerator接口的实现,提供迭代器对象
2 创建私用数组存储数据 3 提供索引器获取数组元素 4 提供数组长度
迭代器对象,1 私用变量3个,聚合对象,索引下标,当前聚合元素 2构造函数,初始化数据
3 MoveNext() ,返回是否含有数据,并把当前当前聚合元素赋值,下标加1
4 提供Current属性返回当前元素,Reset方法重置。
C#2.0 使用yield return语句简化了迭代器的实现
public IEnumerator GetEnumerator()
{
for (int index = 0; index < friendarray.Length; index++)
{
// 这样就不需要额外定义一个FriendIterator迭代器来实现IEnumerator
// 在C# 2.0中只需要使用下面语句就可以实现一个迭代器
yield return friendarray[index];
}
}
1 实现IEnumerable的GetEnumerator()方法返回IEnumerator接口的实现,提供迭代器对象
2 使用yield return直接返回迭代器对象
情景
1 编译器遇到yield return的迭代块(yield block)生成包含状态机的内部类,延迟到第一次执行moveNext()执行
public static IEnumerable<int> WithIterator()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("在WithIterator方法中的, 当前i的值为:{0}", i);
if (i > 1)
{
yield return i;
}
}
}
static void Main(string[] args)
{
WithIterator();
}
运行结果:
无输出。
2 Yield Return关键字的作用就是退出当前函数,并且会保存当前函数执行到什么地方。
static void Main(string[] args)
{
// 测试三
foreach (int j in WithIterator())
{
Console.WriteLine("在main输出语句中,当前i的值为:{0}", j);
}
}
运行结果:
应用:
1遍历日期
public IEnumerable<DateTime> DateRange
{
get
{
for (DateTime day=StartDate ; day < =EndDate; day=day.AddDays(1))
{
yield return day;
}
}
}
2 遍历文件行
static IEnumerable<String> ReadLines(String fileName)
{
using (TextReader reader = File.OpenText(fileName))
{
String line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
3
public delegate TResult Func<TResult>()
代理没有参数,返回和类型参数相同的类型。我们想获得TextReader对象,所以可以使用Func<TextReader>,代码如下:
using (TextReader reader=provider())
{
String line;
while ((line=reader.ReadLine())!=null)
{
yield return line;
}
}
4 实现where
异常判断:将方法分为两部分,一部分像普通方法那样对参数进行验证,另一部分代码使用迭代块对主体逻辑数据进行惰性处理。
每一次只在内存中请求一行并对其进行处理。
public static IEnumerable<T> Where<T>(IEnumerable<T> source, Predicate<T> predicate)
{
if (source == null || predicate == null)
throw new ArgumentNullException();
return WhereImpl(source, predicate);
}
private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Predicate<T> predicate)
{
foreach (T item in source)
{
if (predicate(item))
yield return item;
}
}
扩展:
Yield Return是可以让当前函数的进程状态切换到阻塞状态,然后去选择了把cpu交给当前的出进程,这样就转而执行调用方函数。 (补充个小知识点其实我们写的程序加入到内存中,并不定就是一个进程,我们会根据情况分成几个子进程去干活,方便操作系统去管理以及多道程序运行在内存,提高计算机资源的利用率)
编译器会生成一个状态机来维护迭代器的状态。
当希望获取一个IEnumerable<T>类型的集合,而不想把数据一次性加载到内存,就可以考虑使用yield return实现"按需供给"。
参考文献:
https://www.cnblogs.com/zhili/archive/2012/12/02/Interator.html
https://www.cnblogs.com/yangecnu/archive/2012/03/17/2402432.html