Ruby 2.0有一个新的特性是惰性枚举器,Soi Mort 的博客举了一个例子:可以将下面的代码
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
个元素的方法了。
你在说什么?
其实我就是打算复习一下迭代器而已...