为什么不是';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消费了吗/与python相比,生成器在c#中是如何工作的

您非常接近。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操作符是一个用于更容易地返回IEnumerableIEnumerator实例的实用程序。它实现了接口,每次调用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的新实例,并且这些实例中的每个实例都有自己的状态,因此对它们的枚举是相互独立的。

我希望我写的是有意义的,并为你澄清了这个问题。