为什么IEnumerable没有被消费?/C#中的生成器与Python比较如何?
我原以为C#中的yield return和Python中的yield差不多,我觉得我理解了这两者。我认为编译器会把一个函数变成一个对象,这个对象里有一个指针指向执行应该从哪里继续。当请求下一个值时,这个对象会运行到下一个yield的位置,更新指针,然后返回一个值。
在Python中,这种方式有点像懒惰求值,它会根据需要生成值,但一旦这些值被使用过,如果没有保存到其他变量中,就可以被垃圾回收了。如果你尝试两次遍历这样的函数的结果,除非把结果转成列表,否则会得到一个空的可迭代对象。
举个例子:
def y():
list = [1,2,3,4]
for i in list:
yield str(i)
ys = y()
print "first ys:"
print ",".join(ys)
print "second ys:"
print ",".join(ys)
输出结果:
first ys:
1,2,3,4
second ys:
直到最近,我还以为C#也是这样,但在dotnetfiddle上试了一下却失败了。
http://dotnetfiddle.net/W5Cbv6
using System;
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static IEnumerable<string> Y()
{
var list = new List<string> {"1","2","3","4","5"};
foreach(var i in list)
{
yield return i;
}
}
public static void Main()
{
var ys = Y();
Console.WriteLine("first ys");
Console.WriteLine(string.Join(",", ys));
Console.WriteLine("second ys");
Console.WriteLine(string.Join(",", ys));
}
}
输出结果:
first ys
1,2,3,4,5
second ys
1,2,3,4,5
这里发生了什么?它是在缓存结果吗?这不对吧,否则File.ReadLines在处理大文件时会出问题吧?难道它只是第二次从头开始运行这个函数吗?
注意:我对生成器和协程的一些术语有点不确定,所以我尽量避免使用这些标签。
6 个回答
编译器会创建一个对象,这个对象实现了你在Y方法中定义的IEnumerable接口。
这个对象其实就像一个状态机,它会记录当前的状态,当你在遍历时,它会向前移动。你可以看看从你的Y方法返回的IEnumerable所创建的Enumerator的MoveNext方法的IL代码:
IL_0000: ldarg.0
IL_0001: ldfld int32 Program/'<Y>d__1'::'<>1__state'
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: switch (IL_001e, IL_00e8, IL_00ce)
IL_0019: br IL_00e8
IL_001e: ldarg.0
IL_001f: ldc.i4.m1
IL_0020: stfld int32 Program/'<Y>d__1'::'<>1__state'
IL_0025: ldarg.0
IL_0026: ldarg.0
IL_0027: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
IL_002c: stfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0031: ldarg.0
IL_0032: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0037: ldstr "1"
IL_003c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
IL_0041: ldarg.0
IL_0042: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0047: ldstr "2"
IL_004c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
IL_0051: ldarg.0
IL_0052: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0057: ldstr "3"
IL_005c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
IL_0061: ldarg.0
IL_0062: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0067: ldstr "4"
IL_006c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
IL_0071: ldarg.0
IL_0072: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0077: ldstr "5"
IL_007c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
IL_0081: ldarg.0
IL_0082: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<>g__initLocal0'
IL_0087: stfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<list>5__2'
IL_008c: ldarg.0
IL_008d: ldarg.0
IL_008e: ldfld class [mscorlib]System.Collections.Generic.List`1<string> Program/'<Y>d__1'::'<list>5__2'
IL_0093: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<string>::GetEnumerator()
IL_0098: stfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string> Program/'<Y>d__1'::'<>7__wrap4'
IL_009d: ldarg.0
IL_009e: ldc.i4.1
IL_009f: stfld int32 Program/'<Y>d__1'::'<>1__state'
IL_00a4: br.s IL_00d5
IL_00a6: ldarg.0
IL_00a7: ldarg.0
IL_00a8: ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string> Program/'<Y>d__1'::'<>7__wrap4'
IL_00ad: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
IL_00b2: stfld string Program/'<Y>d__1'::'<i>5__3'
IL_00b7: ldarg.0
IL_00b8: ldarg.0
IL_00b9: ldfld string Program/'<Y>d__1'::'<i>5__3'
IL_00be: stfld string Program/'<Y>d__1'::'<>2__current'
IL_00c3: ldarg.0
IL_00c4: ldc.i4.2
IL_00c5: stfld int32 Program/'<Y>d__1'::'<>1__state'
IL_00ca: ldc.i4.1
IL_00cb: stloc.0
IL_00cc: leave.s IL_00f3
IL_00ce: ldarg.0
IL_00cf: ldc.i4.1
IL_00d0: stfld int32 Program/'<Y>d__1'::'<>1__state'
IL_00d5: ldarg.0
IL_00d6: ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string> Program/'<Y>d__1'::'<>7__wrap4'
IL_00db: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
IL_00e0: brtrue.s IL_00a6
IL_00e2: ldarg.0
IL_00e3: call instance void Program/'<Y>d__1'::'<>m__Finally5'()
IL_00e8: ldc.i4.0
IL_00e9: stloc.0
IL_00ea: leave.s IL_00f3
当Enumerator对象处于初始状态时(也就是刚通过GetEnumerator调用创建出来),这个方法会生成一个内部列表,里面包含了所有的返回值。之后每次调用MoveNext时,都是在操作这个内部列表,直到它被用完为止。这就意味着每次有人开始遍历返回的IEnumerable时,都会创建一个新的Enumerator,重新开始。
File.ReadLines也是一样。每次你开始遍历文件时,都会创建一个新的文件句柄,每调用一次MoveNext或Current,就会从底层流中返回一行。
当编译器看到“yield”这个关键词时,它会在程序类里面创建一个嵌套的私有类,这个类会实现一个叫做IEnumerator的东西。(在C#有“yield”之前,我们得自己手动实现这个功能)
这里有一个稍微简化、易读的版本:
private sealed class EnumeratorWithSomeWeirdName : IEnumerator<string>, IEnumerable<string>
{
private string _current;
private int _state = 0;
private List<string> list_;
private List<string>.Enumerator _wrap;
public string Current
{
get { return _current; }
}
object IEnumerator.Current
{
get { return _current; }
}
public bool MoveNext()
{
switch (_state) {
case 0:
_state = -1;
list_ = new List<string>();
list_.Add("1");
list_.Add("2");
list_.Add("3");
list_.Add("4");
list_.Add("5");
_wrap = list_.GetEnumerator();
_state = 1;
break;
case 1:
return false;
case 2:
_state = 1;
break;
default:
return false;
}
if (_wrap.MoveNext()) {
_current = _wrap.Current;
_state = 2;
return true;
}
_state = -1;
return false;
}
IEnumerator<string> GetEnumerator()
{
return new EnumeratorWithSomeWeirdName();
}
IEnumerator IEnumerator.GetEnumerator()
{
return new EnumeratorWithSomeWeirdName();
}
void IDisposable.Dispose()
{
_wrap.Dispose();
}
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
Y()方法也会有所改变。它会简单地返回这个嵌套类的一个实例:
public static IEnumerable<string> Y()
{
return new EnumeratorWithSomeWeirdName();
}
注意,这个时候什么都还没发生。你只是得到了这个类的一个实例。只有当你开始遍历(用foreach循环)时,实例上的MoveNext()方法才会被调用。这个方法会一次返回一个项目。(这一点很重要)
foreach循环其实是个语法糖;它实际上是调用GetEnumerator()方法:
using(IEnumerator<string> enumerator = list.GetEnumerator()) {
while (enumerator.MoveNext()) yield return enumerator.Current;
}
如果你调用ys.GetEnumerator(),你会看到它有一个MoveNext()方法和一个Current属性,这正是IEnumerator应该有的。
如果你的Main方法里有一行像这样的代码:
foreach (string s in ys) Console.WriteLine(s);
然后你用调试器逐步执行,你会看到调试器在Main和Y方法之间来回跳动。通常情况下,像这样进出一个方法是不可能的,但因为实际上它是一个类,所以这样是可行的。(因为string.Join会遍历整个内容,所以你的例子不会显示这个。)
现在,每次你调用
Console.WriteLine(string.Join(",", ys));
时,都会调用另一个foreach循环,因此又会创建一个新的Enumerator。这是可能的,因为这个内部类也实现了IEnumerable(他们在实现yield关键词时考虑得非常周到)。所以这里有很多编译器的魔法在运作。一行带有yield return的代码变成了一个完整的类。
代码在不同情况下表现不一样的原因是因为在Python中,你使用了同一个 IEnumerator
实例两次,但第二次的时候它已经被遍历过了(它不能重复使用,所以就不再用了)。而在C#中,每次调用 GetEnumerator()
都会返回一个新的 IEnumerator
,这个新的实例会从头开始遍历集合。每个枚举器实例之间互不影响。枚举器不会自动锁定集合,所以两个枚举器都可以遍历整个集合。然而,你的Python例子只使用了一个枚举器,所以如果不重置,它只能遍历一次。
yield 操作符是一个方便的工具,用来更轻松地返回 IEnumerable
或 IEnumerator
实例。它实现了接口,每次调用 yield return
时都会向返回的迭代器中添加一个元素。每次调用 Y()
时,都会构建一个新的可枚举对象,但每个可枚举对象可以有多个枚举器。 每次调用 String.Join
时,内部会调用 GetEnumerator
,这会为每次调用创建一个新的枚举器。因此,每次调用 String.Join
时,你都会从头到尾遍历整个集合。
经过仔细查看代码的每个部分,我觉得问题可能和IEnumerable<>有关。如果我们去看一下MSDN,会发现IEnumerable本身并不是一个枚举器,而是每次调用GetEnumerator()时都会创建一个新的枚举器。如果我们再看看GetEnumerator,我们会发现foreach(我想string.Join也是这样)会调用GetEnumerator(),每次调用时都会创建一个新的状态。举个例子,这里有一段代码使用了枚举器:
using System;
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static IEnumerable<string> Y()
{
var list = new List<string> {"1","2","3","4","5"};
foreach(var i in list)
{
yield return i;
}
}
public static void Main()
{
var ys = Y();
Console.WriteLine("first ys");
Console.WriteLine(string.Join(",", ys));
IEnumerator<string> i = ys.GetEnumerator();
Console.WriteLine(""+i.MoveNext()+": "+i.Current);
Console.WriteLine(""+i.MoveNext()+": "+i.Current);
Console.WriteLine(""+i.MoveNext()+": "+i.Current);
Console.WriteLine(""+i.MoveNext()+": "+i.Current);
Console.WriteLine(""+i.MoveNext()+": "+i.Current);
Console.WriteLine(""+i.MoveNext()+": "+i.Current);
}
}
当MoveNext到达末尾时,它的行为就像Python那样,正如预期的那样。
你已经非常接近了。IEnumerable
是一种可以创建迭代器(IEnumerator
)的对象。IEnumerator
的行为正如你所描述的那样。
所以,IEnumerable
就是 生成生成器。
除非你特别去做一些事情,让生成的迭代器之间共享某种状态,否则 IEnumerator
对象不会相互影响,不管它们是来自不同的迭代器块调用,还是由同一个 IEnumerable
生成的另一个 IEnumerator
。