Подтвердить что ты не робот

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

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

В 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:

До недавнего времени я думал, что то же самое верно для С#, но попытка его в 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 взорвется на огромные файлы? Это просто перезапуск функции сверху сверху?

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

Просматривая каждую часть кода, я считаю, что это связано с 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);
    }
}

(dotnetfiddle)

Когда MoveNext достигает конца, он имеет поведение python, как ожидалось.

Ответ 3

Когда компилятор видит ключевое слово yield, он реализует конечный автомат в вложенном частном классе внутри класса Program. Этот вложенный класс будет реализовывать IEnumerator. (До того, как у С# было ключевое слово 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). Таким образом, происходит много магии компилятора. Одна строка с возвратом доходности превращается в целый класс.

Ответ 4

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

Этот объект в основном является автоматом состояния, который отслеживает текущее состояние объекта, в то время как перечислитель перемещается вперед. Посмотрите на IL из MoveNext-метода Enumerator, созданного 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

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

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

Ответ 5

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

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

IEnumerable говорит, что объект можно перечислить и предоставляет метод GetEnumerator(). Любой код, который потребляет объект IEnumerable, вызывает метод GetEnumerator() в какой-то момент.

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

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

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

Ответ 6

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

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