Ruby 2.0有一个新的特性是惰性枚举器,举了一个例子:可以将下面的代码
File.open(path) {|fp| fp.each_line. \ select {|line| # 生成了临时数组 /regexp/ =~ line }. \ each_with_index.map {|line, no| # 生成了临时数组 sprintf("%d: %s\n", no, line) }. \ first(10).each {|str| # 生成了临时数组 puts(str) }}
转换为
File.open(path) {|fp| fp.each_line.lazy \ select {|line| # 没有临时数组产生 /regexp/ =~ line }. \ each_with_index.map {|line, no| # 没有临时数组产生 sprintf("%d: %s\n", no, line) }. \ first(10).each {|str| # 没有临时数组产生 puts(str) }} # 甚至在到达EOF之前都不读取数据
这样来避免产生多余的临时对象。这里谈到了惰性枚举,其实这个概念并不算太新鲜,在.NET引以为傲的Linq中,惰性枚举其实越来越重要。
初学C#的时候其实并不容易搞清楚所谓的IEnumerable和IEnumerator,有个时候就糊弄一下觉得大多数情况很少手工操作迭代器和枚举器,用一个foreach
就巧妙的解决并自鸣得意。但是看了《CLR via C#》以及一些关于C#的案例和图书似乎都很少出现foreach
,有时候还纳闷特么这些人是蠢的么...当然,后来发现foreach
的实现方式导致其本身效率是不高的所以...。
回头看.NET的IEnumerable接口:
public interface IEnumerable{ // // Methods // [DispId (-4)] IEnumerator GetEnumerator ();}
这个接口只需要实现一个GetEnumerator的方法,非常简洁。
IEnumerator接口:
public interface IEnumerator{ // // Properties // object Current { get; } // // Methods // bool MoveNext (); void Reset ();}
于是我们便可以实现一个仅能duang出来一个的“列表”:
class OnlyOne : IEnumerable, IEnumerator{ public IEnumerator GetEnumerator () => this; public object Current => "caocaoda"; public bool MoveNext () => false; public void Reset () {}}
如果把false
改为true
那就可以一直艹艹哒啦。
但是这样的话,还是很麻烦,毕竟要我们手工实现,说好的C#简单呢...所以M$引入了一个迭代器,用以实现IEnumerable/IEnumerator。
class OnlyOne : IEnumerable{ public IEnumerator GetEnumerator() { Int32 value = 0; do { yield return value++; } while (false); }}
省事太多,通过DILASM可以看到其实编译器帮我们实现了前面我们自己写的方法。
通过IL不难看出,其实MoveNext()是一个Switch...
废话那么多回到惰性枚举上来,其实我们发现,IEnumerable和IEnumerator两个接口的实现其实是惰性的,也就是在需要的时候才会获取数据,而不会产生临时的数据,就像前面Ruby一样,使用迭代器不会产生额外的开销。如果我们把false
改成了true
,还没有“惰性”那玩意儿可够呛...
为什么说Linq其实很依赖惰性枚举呢...举个例子:
public static IEnumerable Take (Int32 much, IEnumerable s){ for (int i = 0; i < much; i++) { yield return s [i]; }}
我们就可以实现一个在数据源中抓much
个元素的方法了。
你在说什么?
其实我就是打算复习一下迭代器而已...