http://flier_lu.blogone.net/?id=1511638
C#语言从VB中吸取了一个非常实用的foreach语句。对所有支持IEnumerable接口的类的实例,foreach语句使用统一的接口遍历其子项,使得以前冗长的for循环中繁琐的薄记工作完全由编译器自动完成。支持IEnumerable接口的类通常用一个内嵌类实现IEnumerator接口,并通过IEnumerable.GetEnumerator函数,允许类的使用者如foreach语句完成遍历工作。
这一特性使用起来非常方便,但需要付出一定的代价。Juval Lowy发表在MSDN杂志2004年第5期上的 Create Elegant Code with Anonymous Methods, Iterators, and Partial Classes 一文中,较为详细地介绍了C# 2.0中迭代支持和其他新特性。
首先,因为IEnumerator.Current属性是一个object类型的值,所以值类型(value type)集合在被foreach语句遍历时,每个值都必须经历一次无用的box和unbox操作;就算是引用类型(reference type)集合,在被foreach语句使用时,也需要有一个冗余的castclass指令,保障枚举出来的值进行类型转换的正确性。
> 以下为引用:
>
>>
> using System.Collections;
>
> public class Tokens : IEnumerable
> {
> ...
> Tokens f = new Tokens(...);
>
> foreach (string item in f)
> {
> Console.WriteLine(item);
> }
> ...
> }
>
>
> ---
上面的简单代码被自动转换为
> 以下为引用:
>
>>
> Tokens f = new Tokens(...);
>
> IEnumerator enum = f.GetEnumerator();
> try
> {
> do {
> string item = (string)enum.get_Current(); // 冗余转换
>
> Console.WriteLine(item);
> } while(enum.MoveNext());
> }
> finally
> {
> if(enum is IDisposable) // 需要验证实现IEnumerator接口的类是否支持IDisposable接口
> {
> ((IDisposable)enum).Dispose();
> }
> }
>
>
> ---
好在C# 2.0中支持了泛型(generic)的概念,提供了强类型的泛型版本IEnumerable定义,伪代码如下:
> 以下为引用:
>
>>
> namespace System.Collections.Generic
> {
> public interface IEnumerable
1<itemtype>
2> {
3> IEnumerator<itemtype> GetEnumerator();
4> }
5> public interface IEnumerator<itemtype> : IDisposable
6> {
7> ItemType Current{get;}
8> bool MoveNext();
9> }
10> }
11>
12>
13> ---
14
15
16
17这样一来即保障了遍历集合时的类型安全,又能够对集合的实际类型直接进行操作,避免冗余转换,提高了效率。
18
19
20> **以下为引用:**
21>
22>>
23> using System.Collections.Generic;
24>
25> public class Tokens : IEnumerable<string>
26> {
27> ... // 实现 IEnumerable<string> 接口
28>
29> Tokens f = new Tokens(...);
30>
31> foreach (string item in f)
32> {
33> Console.WriteLine(item);
34> }
35> }
36>
37>
38> ---
39
40
41
42上面的代码被自动转换为
43
44
45> **以下为引用:**
46>
47>>
48> Tokens f = new Tokens(...);
49>
50> IEnumerator<string> enum = f.GetEnumerator();
51> try
52> {
53> do {
54> string item = enum.get_Current(); // 无需转换
55>
56> Console.WriteLine(item);
57> } while(enum.MoveNext());
58> }
59> finally
60> {
61> if(enum) // 无需验证实现IEnumerator接口的类是否支持IDisposable接口,
62> // 因为所有由编译器自动生成的IEnumerator接口实现类都支持
63> {
64> ((IDisposable)enum).Dispose();
65> }
66> }
67>
68>
69> ---
70
71
72
73
74除了遍历时的冗余转换降低性能外,C#现有版本另一个不爽之处在于实现IEnumerator接口实在太麻烦了。通常都是由一个内嵌类实现IEnumerator接口,而此内嵌类除了get_Current()函数外,其他部分的功能基本上都是相同的,如
75
76
77> **以下为引用:**
78>
79>>
80> public class Tokens : IEnumerable
81> {
82> public string[] elements;
83>
84> Tokens(string source, char[] delimiters)
85> {
86> // Parse the string into tokens:
87> elements = source.Split(delimiters);
88> }
89>
90> public IEnumerator GetEnumerator()
91> {
92> return new TokenEnumerator(this);
93> }
94>
95> // Inner class implements IEnumerator interface:
96> private class TokenEnumerator : IEnumerator
97> {
98> private int position = -1;
99> private Tokens t;
100>
101> public TokenEnumerator(Tokens t)
102> {
103> this.t = t;
104> }
105>
106> // Declare the MoveNext method required by IEnumerator:
107> public bool MoveNext()
108> {
109> if (position < t.elements.Length - 1)
110> {
111> position++;
112> return true;
113> }
114> else
115> {
116> return false;
117> }
118> }
119>
120> // Declare the Reset method required by IEnumerator:
121> public void Reset()
122> {
123> position = -1;
124> }
125>
126> // Declare the Current property required by IEnumerator:
127> public object Current
128> {
129> get // get_Current函数
130> {
131> return t.elements[position];
132> }
133> }
134> }
135> ...
136> }
137>
138>
139> ---
140
141
142
143内嵌类TokenEnumerator的position和Tokens实际上是每个实现IEnumerator接口的类共有的,只是Current属性的get函数有所区别而已。这方面C# 2.0做了很大的改进,增加了yield关键字的支持,允许代码逻辑上的重用。上面冗长的代码在C# 2.0中只需要几行,如
144
145
146> **以下为引用:**
147>
148>>
149> using System.Collections.Generic;
150>
151> public class Tokens : IEnumerable<string>
152> {
153> public IEnumerator<string> GetEnumerator()
154> {
155> for(int i = 0; i<elements.length; i++)=""> yield elements[i];
156> }
157> ...
158> }
159>
160>
161> ---
162
163
164
165GetEnumerator函数是一个C# 2.0支持的迭代块(iterator block),通过yield告诉编译器在什么时候返回什么值,再由编译器自动完成实现IEnumerator<string>接口的薄记工作。而yield break语句支持从迭代块中直接结束,如
166
167
168> **以下为引用:**
169>
170>>
171> public IEnumerator<int> GetEnumerator()
172> {
173> for(int i = 1;i< 5;i++)
174> {
175> yield return i;
176> if(i > 2)
177> yield break; // i > 2 时结束遍历
178> }
179> }
180>
181>
182> ---
183
184
185
186这样一来,很容易就能实现IEnumerator接口,并可以方便地支持在一个类中提供多种枚举方式,如
187
188
189> **以下为引用:**
190>
191>>
192> public class CityCollection
193> {
194> string[] m_Cities = {"New York","Paris","London"};
195> public IEnumerable<string> Reverse
196> {
197> get
198> {
199> for(int i=m_Cities.Length-1; i>= 0; i--)
200> yield m_Cities[i];
201> }
202> }
203> }
204>
205>
206> ---
207
208
209
210
211接下来我们看看如此方便的语言特性背后,编译器为我们做了哪些工作。以上面那个支持IEnumerable<string>接口的Tokens类为例,GetEnumerator函数的代码被编译器用一个类包装起来,伪代码如下
212
213
214> **以下为引用:**
215>
216>>
217> public class Tokens : IEnumerable<string>
218> {
219> private sealed class GetEnumerator$00000000__IEnumeratorImpl
220> : IEnumerator<string>, IEnumerator, IDisposable
221> {
222> private int $PC = 0;
223> private string $_current;
224> private Tokens <this>;
225> public int i$00000001 = 0;
226>
227> // 实现 IEnumerator<string> 接口
228> string IEnumerator<string>.get_Current()
229> {
230> return $_current;
231> }
232>
233> bool IEnumerator<string>.MoveNext()
234> {
235> switch($PC)
236> {
237> case 0:
238> {
239> $PC = -1;
240> i$00000001 = 0;
241> break;
242> }
243> case 1:
244> {
245> $PC = -1;
246> i$00000001++;
247> break;
248> }
249> default:
250> {
251> return false;
252> }
253> }
254>
255> if(i$00000001 < <this>.elements.Length)
256> {
257> $_current = <this>.elements[i$00000001];
258> $PC = 1;
259>
260> return true;
261> }
262> else
263> {
264> return false;
265> }
266> }
267>
268> // 实现 IEnumerator 接口
269> void IEnumerator.Reset()
270> {
271> throw new Exception();
272> }
273>
274> string IEnumerator.get_Current()
275> {
276> return $_current;
277> }
278>
279> bool IEnumerator.MoveNext()
280> {
281> return IEnumerator<string>.MoveNext(); // 调用 IEnumerator<string> 接口的实现
282> }
283>
284> // 实现 IDisposable 接口
285> void Dispose()
286> {
287> }
288> }
289>
290> public IEnumerator<string> GetEnumerator()
291> {
292> GetEnumerator$00000000__IEnumeratorImpl impl = new GetEnumerator$00000000__IEnumeratorImpl();
293>
294> impl.<this> = this;
295>
296> return impl;
297> }
298> }
299>
300>
301> ---
302
303
304
305从上面的伪代码中我们可以看到,C# 2.0编译器实际上维护了一个和我们前面实现IEnumerator接口的TokenEnumerator类非常类似的内部类,用来封装IEnumerator<string>接口的实现。而这个内嵌类的实现逻辑,则根据GetEnumerator定义的yield返回地点决定。
306我们接下来看一个较为复杂的迭代块的实现,支持递归迭代(Recursive Iterations),代码如下:
307
308
309> **以下为引用:**
310>
311>>
312> using System;
313> using System.Collections.Generic;
314>
315> class Node<t>
316> {
317> public Node<t> LeftNode;
318> public Node<t> RightNode;
319> public T Item;
320> }
321>
322> public class BinaryTree<t>
323> {
324> Node<t> m_Root;
325>
326> public void Add(params T[] items)
327> {
328> foreach(T item in items)
329> Add(item);
330> }
331>
332> public void Add(T item)
333> {
334> // ...
335> }
336>
337> public IEnumerable<t> InOrder
338> {
339> get
340> {
341> return ScanInOrder(m_Root);
342> }
343> }
344>
345> IEnumerable<t> ScanInOrder(Node<t> root)
346> {
347> if(root.LeftNode != null)
348> {
349> foreach(T item in ScanInOrder(root.LeftNode))
350> {
351> yield item;
352> }
353> }
354>
355> yield root.Item;
356>
357> if(root.RightNode != null)
358> {
359> foreach(T item in ScanInOrder(root.RightNode))
360> {
361> yield item;
362> }
363> }
364> }
365> }
366>
367>
368> ---
369
370
371
372BinaryTree<t>提供了一个支持IEnumerable<t>接口的InOrder属性,通过ScanInOrder函数遍历整个二叉树。因为实现IEnumerable<t>接口的不是类本身,而是一个属性,所以编译器首先要生成一个内嵌类支持IEnumerable<t>接口。伪代码如下
373
374
375> **以下为引用:**
376>
377>>
378> public class BinaryTree<t>
379> {
380> private sealed class ScanInOrder$00000000__IEnumeratorImpl<t>
381> : IEnumerator<t>, IEnumerator, IDisposable
382> {
383> BinaryTree<t> <this>;
384> Node<t> root;
385>
386> // ...
387> }
388>
389> private sealed class ScanInOrder$00000000__IEnumerableImpl<t>
390> : IEnumerable<t>, IEnumerable
391> {
392> BinaryTree<t> <this>;
393> Node<t> root;
394>
395> IEnumerator<t> IEnumerable<t>.GetEnumerator()
396> {
397> ScanInOrder$00000000__IEnumeratorImpl<t> impl = new ScanInOrder$00000000__IEnumeratorImpl<t>();
398>
399> impl.<this> = this.<this>;
400> impl.root = this.root;
401>
402> return impl;
403> }
404>
405> IEnumerator IEnumerable.GetEnumerator()
406> {
407> ScanInOrder$00000000__IEnumeratorImpl<t> impl = new ScanInOrder$00000000__IEnumeratorImpl<t>();
408>
409> impl.<this> = this.<this>;
410> impl.root = this.root;
411>
412> return impl;
413> }
414> }
415>
416> IEnumerable<t> ScanInOrder(Node<t> root)
417> {
418> ScanInOrder$00000000__IEnumerableImpl<t> impl = new ScanInOrder$00000000__IEnumerableImpl<t>();
419>
420> impl.<this> = this;
421> impl.root = root;
422>
423> return impl;
424> }
425> }
426>
427>
428> ---
429
430
431
432因为ScanInOrder函数内容需要用到root参数,故而IEnumerable<t>和IEnumerator<t>接口的包装类都需要有一个root字段,保存传入ScanInOrder函数的参数,并传递给最终的实现函数。
433实现IEnumerator<t>接口的内嵌包装类ScanInOrder$00000000__IEnumeratorImpl<t>实现原理与前面例子里的大致相同,不同的是程序逻辑大大复杂化,并且需要用到IDisposable接口完成资源的回收。
434
435
436> **以下为引用:**
437>
438>>
439> public class BinaryTree<t>
440> {
441> private sealed class GetEnumerator$00000000__IEnumeratorImpl
442> : IEnumerator<t>, IEnumerator, IDisposable
443> {
444> private int $PC = 0;
445> private string $_current;
446> private Tokens <this>;
447> public int i$00000001 = 0;
448>
449> public IEnumerator<t> __wrap$00000003;
450> public IEnumerator<t> __wrap$00000004;
451> public T item$00000001;
452> public T item$00000002;
453> public Node<t> root;
454>
455> // 实现 IEnumerator<t> 接口
456> string IEnumerator<t>.get_Current()
457> {
458> return $_current;
459> }
460>
461> bool IEnumerator<t>.MoveNext()
462> {
463> switch($PC)
464> {
465> case 0:
466> {
467> $PC = -1;
468> if(root.LeftNode != null)
469> {
470> __wrap$00000003 = <this>.ScanInOrder(root.LeftNode).GetEnumerator();
471>
472> goto ScanLeft;
473> }
474> else
475> {
476> goto GetItem;
477> }
478> }
479> case 1:
480> {
481> return false;
482> }
483> case 2:
484> {
485> goto ScanLeft;
486> }
487> case 3:
488> {
489> $PC = -1;
490> if(root.RightNode != null)
491> {
492> __wrap$00000004 = <this>.ScanInOrder(root.RightNode).GetEnumerator();
493>
494> goto ScanRight;
495> }
496> else
497> {
498> return false;
499> }
500> break;
501> }
502> case 4:
503> {
504> return false;
505> }
506> case 5:
507> {
508> goto ScanRight;
509> }
510> default:
511> {
512> return false;
513> }
514> ScanLeft:
515> $PC = 1;
516>
517> if(__wrap$00000003.MoveNext())
518> {
519> $_current = item$00000001 = __wrap$00000003.get_Current();
520> $PC = 2;
521> return true;
522> }
523>
524> GetItem:
525> $PC = -1;
526> if(__wrap$00000003 != null)
527> {
528> ((IDisposable)__wrap$00000003).Dispose();
529> }
530> $_current = root.Item;
531> $PC = 3;
532> return true;
533>
534> ScanRight:
535> $PC = 4;
536>
537> if(__wrap$00000004.MoveNext())
538> {
539> $_current = $item$00000002 = __wrap$00000004.get_Current();
540> $PC = 5;
541> return true;
542> }
543> else
544> {
545> $PC = -1;
546> if(__wrap$00000004 != null)
547> {
548> ((IDisposable)__wrap$00000004).Dispose();
549> }
550> return false;
551> }
552> }
553> // 实现 IDisposable 接口
554> void Dispose()
555> {
556> switch($PC)
557> {
558> case 1:
559> case 2:
560> {
561> $PC = -1;
562> if(__wrap$00000003 != null)
563> {
564> ((IDisposable)__wrap$00000003).Dispose();
565> }
566> break;
567> }
568> case 4:
569> case 5:
570> {
571> $PC = -1;
572> if(__wrap$00000004 != null)
573> {
574> ((IDisposable)__wrap$00000004).Dispose();
575> }
576> break;
577> }
578> }
579> }
580> }
581> }
582>
583>
584> ---
585
586
587
588通过上面的伪代码,我们可以看到,C# 2.0实际上是通过一个以$PC为自变量的有限状态机完成的递归迭代块,这可能是因为有限状态机可以很方便地通过程序自动生成吧。而Dispose()函数则负责处理状态机的中间变量。
589
590有兴趣进一步了解迭代特性的朋友,可以到 Grant Ri的BLog 上阅读 Iterators相关文章 。
591在了解了Iterators的实现原理后,再看那些讨论就不会被其表象所迷惑了 :D</this></this></t></t></t></t></t></t></this></t></t></t></t></t></t></this></t></t></t></t></this></this></t></t></this></this></t></t></t></t></t></this></t></t></t></t></this></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></string></this></string></string></string></this></this></string></string></string></this></string></string></string></string></int></string></elements.length;></string></string></string></string></string></itemtype></itemtype></itemtype>