为什么不消耗IEnumerable?/与python相比,c#中的生成器如何工作?

时间:2014-05-01 15:20:58

标签: c# python iteration coroutine

所以我认为我理解c#yield return与pythons yield大致相同,我认为我理解。我认为编译器将一个函数转换为一个对象,该对象带有一个指向执行应该恢复的位置的指针,当对象的下一个值的请求运行到下一个yield时,它会更新指针的恢复执行位置并返回一个值。

在python中,它的工作方式类似于延迟评估,因为它根据需要生成值,但是一旦使用了值,如果不能保存在另一个变量中,则可以使用gc' ed。尝试迭代这样一个函数的结果两次返回一个空的iterable,除非你将它转换为一个列表。

离。

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 个答案:

答案 0 :(得分:11)

非常接近IEnumerable是一个能够创建迭代器(IEnumerator)的对象。 IEnumerator的行为完全如您所述。

因此IEnumerable 会生成生成器

除非你不遗余力地生成在生成的迭代器之间共享的某种状态,否则IEnumerator对象不会相互影响,无论它们来自对迭代器块的单独调用还是另一个{{1由同一IEnumerator生成。

答案 1 :(得分:7)

在查看代码的每个部分后,我认为它与IEnumerable&lt;&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行为。

答案 2 :(得分:2)

当编译器看到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一样。

如果您的Main方法有如下行:

foreach (string s in ys) Console.WriteLine(s);

并且您将使用调试器逐步执行它,您会看到调试器在Main和Y方法之间来回跳转。 通常不可能进出这样的方法,但因为实际上它实际上是一个类,这是有效的。 (因为string.Join只是枚举整个事情,你的例子不会显示这个。)

现在,每次打电话

Console.WriteLine(string.Join(",", ys));

调用另一个foreach循环,因此创建了另一个枚举器。这是可能的,因为内部类也实现了IEnumerable(他们只是在实现yield关键字时想到了所有内容)所以有很多编译器魔术在继续。收益率回报的一行变成了整个类。

答案 3 :(得分:1)

编译器创建一个实现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时,就会创建一个新的枚举器并从头开始。

File.ReadLines也是如此。每次开始迭代时,都会创建一个新的文件句柄,每次调用MoveNext / Current时都会从基础流返回一行

答案 4 :(得分:1)

我不了解Python,但在C#中,yield关键字本质上是一个自动实现的迭代器对象,使用代码&#34;围绕&#34; yield语句作为迭代器逻辑。

编译器发出实现IEnumerable<T>IEnumerator<T>接口的对象。

IEnumerable表示可以枚举对象并提供GetEnumerator()方法。任何使用IEnumerable对象的代码都会在某个时刻调用GetEnumerator()方法。

GetEnumerator()方法的调用返回实现IEnumerator接口的对象。 IEnumerator是C#/ CLR中迭代器模式的实现,它是这个迭代器对象(不是IEnumerable)保存枚举的状态,即实现IEnumerator接口的对象是有限状态机(FSM,有限状态自动机)。 yield returnyield break关键字代表此FSM中的状态转移。

所以在你的示例代码中发生的事情就是这样 - 对你的多次调用Y()方法返回包含你的逻辑的IEnumerator的新实例,并且每个实例都有自己的状态,以便枚举它们是彼此独立的。

我希望我能以一种有意义的方式撰写,并为您澄清问题。

答案 5 :(得分:1)

代码在每种情况下表现不同的原因是因为在python中,您使用相同的IEnumerator实例两次,但第二次它已经被枚举(它不能重复它,所以它不会) 。但是,在C#中,每次调用GetEnumerator()都会返回 IEnumerator,这将从头开始重复收集。每个枚举器实例不会影响其他枚举器。 Enumerators do not implicitly lock the collection,因此两个枚举器都可以遍历整个集合。但是,你的python示例只使用一个枚举器,所以没有重置,它只能迭代

yield运算符是一种可以更轻松地返回IEnumerableIEnumerator个实例的实用程序。它实现了接口,每次调用yield return时都会向返回的迭代器添加一个元素。 每次调用Y()时,都会构建一个新的枚举,但每个枚举可以有多个枚举器。每次调用String.Join都会在内部调用GetEnumerator,为每个调用创建一个新枚举器。因此,每次调用String.Join,您都会从头到尾遍历整个集合。