为什么不是';IEnumerable消费了吗/与python相比,生成器在c#中是如何工作的
本文关键字:工作 何工作 相比 IEnumerable python 为什么不 | 更新日期: 2023-09-27 18:01:03
所以我认为我理解c#yield return在很大程度上与pythons yield相同,我认为我已经理解了。我认为编译器会将函数转换为一个对象,该对象带有一个指针,指向应该恢复执行的位置,当对下一个值的请求出现时,该对象会运行到下一个yield,在那里它会更新恢复执行的指针并返回一个值。
在python中,这与惰性求值有点类似,因为它根据需要生成值,但一旦使用了值,如果不保存在另一个变量中,则可以对其进行gc'ed。尝试对这样一个函数的结果进行两次迭代会返回一个空的可迭代项,除非您将其转换为列表。
前任。
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会在巨大的文件上爆炸吗?它只是第二次从顶部重新启动功能吗?
注意:我对生成器和协同程序的一些术语有点不确定,所以我尽量避免贴标签。
您非常接近。IEnumerable
是能够创建迭代器(IEnumerator
)的对象。IEnumerator
的行为与您所描述的完全相同。
因此IEnumerable
生成生成器。
除非您尽力在生成的迭代器之间生成某种共享状态,否则IEnumerator
对象不会相互影响,无论它们是来自对迭代器块的单独调用,还是来自同一IEnumerable
生成的另一个IEnumerator
。
看完代码的每一部分后,我相信它与IEnumerable<gt;。如果我们看看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);
}
}
(dotnetfiddle)
当MoveNext到达末尾时,它具有预期的python行为。
代码在每种情况下表现不同的原因是,在python中,您使用同一个IEnumerator
实例两次,但第二次已经枚举(它不能重复,所以不会)。但是,在C#中,每次对GetEnumerator()
的调用都返回一个新的IEnumerator
,它将从头开始在集合中重复。每个枚举器实例不影响其他枚举器。枚举器不会隐式锁定集合,因此这两个枚举器都可以在整个集合中循环。然而,您的python示例只使用一个枚举器,因此在没有重置的情况下,它只能迭代
yield操作符是一个用于更容易地返回IEnumerable
或IEnumerator
实例的实用程序。它实现了接口,每次调用yield return
时都会向返回的迭代器添加一个元素每次调用Y()
,都会构造一个新的可枚举对象,但每个可枚举对象可以有多个枚举器每次对String.Join
的调用都会在内部调用GetEnumerator
,这会为每次调用创建一个新枚举器。因此,每次调用String.Join
时,都会从头到尾循环整个集合。
当编译器看到yield关键字时,它将在Program类内的嵌套私有类中实现状态机。这个嵌套类将实现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应该有的那样。
如果你的主方法有一行像:
foreach (string s in ys) Console.WriteLine(s);
如果您使用调试器逐步完成,您会看到调试器在Main和Y方法之间来回跳跃。通常不可能进出这样的方法,但因为实际上它是一个类,所以这是有效的。(因为string.Join只是枚举了整件事,所以你的例子不会显示这一点。)
现在,每次你打电话给
Console.WriteLine(string.Join(",", ys));
调用另一个foreach循环,因此创建了另一个枚举器。这是可能的,因为内部类也实现了IEnumerable(他们在实现yield关键字时只考虑了所有内容)所以有很多编译器的魔力。一行yield返回就变成了整个类。
编译器创建一个对象,实现Y方法的IEnumerable。
这个对象基本上是一个状态机,它在枚举器向前移动时跟踪对象的当前状态。查看您的Y方法返回的IEnumerable创建的枚举器的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时,都会创建一个新的枚举器,然后重新开始。
File.ReadLines也是如此。每次开始迭代时,都会创建一个新的文件句柄,为每次调用MoveNext/Current
从底层流返回一行。我不知道Python,但在C#中,yield
关键字本质上是一个自动实现的迭代器对象,使用"环绕"yield语句的代码作为迭代器逻辑。
编译器发出实现IEnumerable<T>
和IEnumerator<T>
接口的对象。
IEnumerable
表示可以枚举对象,并提供GetEnumerator()
方法。任何使用IEnumerable
对象的代码都会在某个时刻调用GetEnumerator()
方法。
对GetEnumerator()
方法的调用返回一个实现IEnumerator
接口的对象。IEnumerator
是C#/CLR中迭代器模式的实现,是这个迭代器对象(而不是IEnumerable
对象)保持枚举的状态,即实现IEnumerator
接口的对象是有限状态机(FSM,有限状态自动机)。CCD_ 30和CCD_ 31关键字表示该FSM内的状态转移。
因此,在示例代码中发生的事情是这样的——对Y()方法的多次调用返回包含逻辑的IEnumerator
的新实例,并且这些实例中的每个实例都有自己的状态,因此对它们的枚举是相互独立的。
我希望我写的是有意义的,并为你澄清了这个问题。