Почему IEnumerable не потребляется?/ как работают генераторы на c# по сравнению с python

поэтому я думал, что понимаю доходность C# как в значительной степени такую же, как доходность питонов, которую я думал, что понимаю. Я думал, что компилятор преобразует функцию в объект с указателем на то, где выполнение должно быть возобновлено, и когда приходит запрос на следующее значение, объект запускается до следующего выхода, где он обновляет указатель на то, где возобновить выполнение и возвращает значение.

в python это работает аналогично ленивой оценке в этом он производит значения по мере необходимости, но как только значения используются, как только они могут быть gc'ed, если не сохранить в другой переменной. Попытка повторить результат такой функции дважды возвращает пустую итерацию, если вы не преобразуете ее в список.

ex.

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

что здесь происходит? Это кэширование результата? Иначе не может быть!-Файл -23-->.ReadLines будет взрываться на огромных файлах? Это просто перезапуск функции сверху во второй раз?

примечание: Я немного не уверен в некоторых терминах генераторы и сопрограммы, так что я пытался избежать маркировки.

6 ответов


ты очень близко. Ан IEnumerable - объект, способный создать итератор (IEnumerator). Ан IEnumerator ведет себя точно как вы описали.

так IEnumerable генерирует генераторы.

если вы не выходите из своего пути, чтобы создать какое-то состояние, разделяемое между сгенерированными итераторами,IEnumerator объекты не будут влиять друг на друга, будь то отдельные вызовы блока итератора или другого IEnumerator генерируется же IEnumerable.


просмотрев каждую часть кода, я считаю, что это связано с IEnumerable. Если мы посмотрим на MSDN, IEnumerable не является перечислителем сам по себе, но он создает перечислитель каждый раз, когда вызывается GetEnumerator (). Если мы посмотрим на GetEnumerator, мы видим, что foreach (и я представляю строку.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, как ожидалось.


когда компилятор видит ключевое слово 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 должен.

если ваш основной метод строка:

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

и вы шаг через него с отладчиком, вы увидите, что отладчик прыгает взад и вперед между главным и Г методы. Обычно невозможно войти и выйти из такого метода, но поскольку на самом деле это класс, это работает. (За строкой.Join просто перечисляет все это, ваш пример не покажет этого.)

теперь, каждый раз, когда вы звоните

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

вызывается другой цикл foreach, поэтому создается другой перечислитель. Это возможно, потому что внутренний класс также реализует IEnumerable (они просто подумали обо всем, когда реализовали ключевое слово yield), поэтому происходит много магии компилятора. Одна строка с доходностью превращается в целый класс.


компилятор создает объект, который реализует IEnumerable вашего y-метода.

этот объект в основном является государственной машиной, которая отслеживает текущее состояние объекта при перемещении перечислителя вперед. Посмотрите на IL метода MoveNext перечислителя, созданного IEnumerable, возвращенного из вашего y-метода:

        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

когда перечислитель-объект находится в его intial состоянии (он только что был new'eded вверх GetEnumerator-call), метод создает внутренний список, содержащий все данные значения. Последующие вызовы MoveNext работают во внутреннем списке, пока он не будет исчерпан. Это означает, что каждый раз, когда кто-то начинает итерацию над возвращаемым IEnumerable создается новый перечислитель, и вы начинаете все сначала.

то же самое происходит с файлом.ReadLines. Каждый раз, когда вы начинаете итерацию, создается новый дескриптор файла, возвращающий одну строку из базового потока для каждого вызова MoveNext / Current


я не знаю о Python, но в C#yield ключевое слово по существу является автоматически реализованным объектом итератора, используя код, "окружающий" операторы yield в качестве логики итератора.

компилятор выдает объекты, реализующие IEnumerable<T> и IEnumerator<T> интерфейсы.

IEnumerable говорит, что объект может быть определен и обеспечивает GetEnumerator() метод. Любой код, который потребляет . IEnumerator является реализацией шаблона итератора в C# / CLR, и это этот объект итератора (не IEnumerable one) содержит состояние перечисления, т. е. объект, реализующий IEnumerator интерфейс-это конечностная машина (FSM, конечный автомат). The yield return и yield break ключевые слова представляют собой передачу состояния в этом FSM.

Итак, то, что происходит в вашем примере кода это-несколько вызовов к вам y () метод возвращает новые экземпляры IEnumerator содержащий вашу логику, и каждый из этих экземпляров имеет свое собственное состояние, так что перечисление над ними не зависит друг от друга.

надеюсь, я написал это так, что это имеет смысл и проясняет проблему для вас.


причина, по которой код ведет себя по-разному в каждом случае, заключается в том, что в python вы используете тот же IEnumerator экземпляр дважды, но во второй раз он уже был перечислен (он не может повторить его, поэтому он этого не делает). Однако, в C#, каждый вызов GetEnumerator() возвращает новая IEnumerator, который будет повторяться через коллекцию с самого начала. Каждый экземпляр перечислителя не влияет на другие перечислители. Перечислители неявно блокируют коллекцию, таким образом, два перечислителя могут выполнять цикл по всей коллекции. Однако в вашем примере python используется только один перечислитель, поэтому без сброса он может выполнять только итерацию

на доходность оператор-это утилита для возвращения IEnumerable или IEnumerator легче экземпляров. Он реализует интерфейс, добавляя элемент в возвращаемый итератор при каждом вызове yield return. С каждым вызовом Y(), создается новый перечисляемый, но каждый перечисляемый может иметь более один перечислитель. каждый вызов String.Join звонки GetEnumerator внутренне, что создает новый перечислитель для каждого вызова. Поэтому с каждым призывом к String.Join, вы петли через всю коллекцию от начала до конца.