聲明
本文內(nèi)容來自微軟 MVP solenovex 的視頻教程——真會(huì)C#? - 第4章 進(jìn)階C#其它內(nèi)容过椎,大致和第 3 課—— 4.6 枚舉和迭代器 對(duì)應(yīng)室梅。可在 GitHub 中查看 C# 視頻教程的配套PPT
本文主要包括以下內(nèi)容:
- 枚舉器 Enumerator
- 集合初始化器
- 迭代器 Iterators
- 迭代器的語義
- 組合序列
枚舉器 Enumerator
枚舉器是一個(gè)只讀的潭流,作用于一序列值的竞惋、只能向前的游標(biāo)。枚舉器是一個(gè)實(shí)現(xiàn)了下列任意一個(gè)接口的對(duì)象:
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>
技術(shù)上來說灰嫉,任何一個(gè)含有名為MoveNext
方法和名為Current
的屬性的對(duì)象,都會(huì)被當(dāng)作枚舉器來對(duì)待嗓奢。foreach
語句會(huì)迭代可枚舉的對(duì)象(enumerable object)讼撒。可枚舉的對(duì)象是一序列值的邏輯表示股耽。它本身不是游標(biāo)根盒,它是一個(gè)可以基于本身產(chǎn)生游標(biāo)的對(duì)象。
可枚舉對(duì)象 enumerable object
一個(gè)可枚舉對(duì)象可以是(下列任意一個(gè)):
- 實(shí)現(xiàn)了
IEnumerable
或者IEnumerable<T>
的對(duì)象 - 有一個(gè)名為
GetEnumerator
的方法物蝙,并且該方法返回一個(gè)枚舉器(enumerator)
IEnumerator
和 IEnumerable
是定義在 System.Collections
命名空間下的炎滞。IEnumerator<T>
和IEnumerable<T>
是定義在 System.Collections.Generic
命名空間下的。
枚舉模式 enumeration pattern
class Enumerator // Typically implements IEnumerator or IEnumerator<T>
{
public IteratorVariableType Current { get {...} }
public bool MoveNext() {...}
}
class Enumerable // Typically implements IEnumerable or IEnumerable<T>
{
public Enumerator GetEnumerator() {...}
}
注意:如果枚舉器(enumerator)實(shí)現(xiàn)了IDisposable
接口诬乞,那么foreach
語句就會(huì)像
using
語句那樣册赛,隱式的dispose
掉這個(gè) enumerator 對(duì)象钠导。
foreach (char c in "beer")
Console.WriteLine (c);
using (var enumerator = "beer".GetEnumerator())
while (enumerator.MoveNext())
{
var element = enumerator.Current;
Console.WriteLine (element);
}
集合初始化器
你可以只用一步就把可枚舉對(duì)象進(jìn)行實(shí)例化并且填充里面的元素:
using System.Collections.Generic;
...
List<int> list = new List<int> {1, 2, 3};
但是編譯器會(huì)把它翻譯成:
using System.Collections.Generic;
...
List<int> list = new List<int>();
list.Add (1);
list.Add (2);
list.Add (3);
上例中,要求可枚舉對(duì)象實(shí)現(xiàn)了System.Collections.IEnumerable
接口森瘪,并且他還有一個(gè)可接受適當(dāng)參數(shù)的Add
方法牡属。
var dict = new Dictionary<int, string>()
{
{ 5, "five" },
{ 10, "ten" }
};
// succinctly
var dict = new Dictionary<int, string>()
{
[3] = "three",
[10] = "ten"
};
迭代器 Iterators
foreach
語句是枚舉器(enumerator)的消費(fèi)者,而迭代器(iterator)是枚舉器的生產(chǎn)者扼睬。
yield return
語句表達(dá)的意思是:這是你向我請(qǐng)求從枚舉器產(chǎn)生的下一個(gè)元素逮栅。每逢遇到yield
語句,控制權(quán)都會(huì)回歸到調(diào)用者那里窗宇,但是被調(diào)用這的狀態(tài)還是會(huì)保持的措伐,這樣的話可以保證當(dāng)調(diào)用者列舉出下一個(gè)元素的時(shí)候,方法可以繼續(xù)執(zhí)行军俊。這個(gè)狀態(tài)的生命周期被綁定到了枚舉器上侥加。這樣的話,當(dāng)調(diào)用者完成枚舉動(dòng)作之后蝇完,狀態(tài)可以被釋放官硝。
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
foreach (int fib in Fibs(6))
Console.Write (fib + " ");
}
static IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
}
// OUTPUT: 1 1 2 3 5 8
原理解釋
編譯器把迭代方法轉(zhuǎn)換成私有的、實(shí)現(xiàn)了IEnumerable<T>
或IEnumerator<T>
的類短蜕。迭代器塊內(nèi)部的邏輯被反轉(zhuǎn)并且被切分到編譯器生成的枚舉器類里面的MoveNext
方法和Current
屬性里氢架。這意味著當(dāng)你調(diào)用迭代器方法時(shí),你所做的實(shí)際就是對(duì)編譯器生成的類進(jìn)行實(shí)例化朋魔;運(yùn)行的代碼里沒有一行是你寫的岖研。你寫的代碼僅會(huì)在對(duì)結(jié)果序列進(jìn)行枚舉的時(shí)候才會(huì)運(yùn)行,例如使用foreach
語句警检。
迭代器的語義
迭代器(iterator)是含有一個(gè)或多個(gè)yield
語句的方法孙援、屬性或索引器。迭代器必須返回下面四個(gè)接口中的一個(gè)(否則編譯器會(huì)報(bào)錯(cuò)):
System.Collections.IEnumerable // Enumerable interfaces
System.Collections.Generic.IEnumerable<T> // Enumerable interfaces
System.Collections.IEnumerator // Enumerator interfaces
System.Collections.Generic.IEnumerator<T> // Enumerator interfaces
根據(jù)迭代器返回的是enumerable
接口還是enumerator
接口扇雕,迭代器會(huì)擁有不同的語義拓售。
多個(gè)yield
語句
方法里可以含有多個(gè)yield
語句:
class Test
{
static void Main()
{
foreach (string s in Foo())
Console.WriteLine(s); // Prints "One","Two","Three"
}
static IEnumerable<string> Foo()
{
yield return "One";
yield return "Two";
yield return "Three";
}
}
yield break
yield break
語句表示迭代器塊會(huì)提前退出,不再返回更多的元素镶奉。
static IEnumerable<string> Foo (bool breakEarly)
{
yield return "One";
yield return "Two";
if (breakEarly)
yield break;
yield return "Three";
}
return
語句在迭代器塊里面是非法的础淤,你必須使用yield break
代替。
迭代器和try/catch/finally
塊
yield return
語句不可以出現(xiàn)在含有catch
子句的try
塊里面:
IEnumerable<string> Foo()
{
try { yield return "One"; } // Illegal
catch { ... }
}
yield return
也不能出現(xiàn)在catch
或者finally
塊里面哨苛。但是 yield return
可以出現(xiàn)在只含有finally
塊的try
塊里面:
IEnumerable<string> Foo()
{
try { yield return "One"; } // OK
finally { ... }
}
當(dāng)消費(fèi)的枚舉器到達(dá)序列終點(diǎn)或被disposed
的時(shí)候鸽凶,finally
塊里面的代碼會(huì)執(zhí)行。如果你提前進(jìn)行了break
建峭,那么foreach
語句也會(huì)dispose
掉枚舉器玻侥,所以用起來很安全。
當(dāng)顯式的使用枚舉器的時(shí)候亿蒸,通常會(huì)犯這樣一個(gè)錯(cuò)誤:沒有dispose
掉枚舉就不再用它了凑兰,這就繞開了finally
塊掌桩。針對(duì)這種情況,你可以使用using
語句來規(guī)避風(fēng)險(xiǎn):
string firstElement = null;
var sequence = Foo();
using (var enumerator = sequence.GetEnumerator())
if (enumerator.MoveNext())
firstElement = enumerator.Current;
組合序列
迭代器是高度可組合的票摇。
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
foreach (int fib in EvenNumbersOnly (Fibs(6)))
Console.WriteLine (fib);
}
static IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
static IEnumerable<int> EvenNumbersOnly (IEnumerable<int> sequence)
{
foreach (int x in sequence)
if ((x % 2) == 0)
yield return x;
}
}
上例中的每個(gè)元素直到最后時(shí)刻才會(huì)被計(jì)算拘鞋,也就是被MoveNext()
操作請(qǐng)求的時(shí)候。
參考
Iterators (C#)
System.Collections.Generic Namespace
IEnumerable Interface
IEnumerator Interface
yield (C# Reference)