Вызывается ли утилизация до дна через возврат доходности? - PullRequest
0 голосов
/ 04 января 2019

Я даю глупые примеры для простоты.

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    foreach(var x in source) yield return x;
}

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

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    using(var sillier = source.GetEnumerator())
    {
        while(sillier.MoveNext()) yield return sillier.Current;
    }
}

Теперь рассмотрим это использование

list.Silly().Take(2).ToArray();

Здесь вы можете видеть, что перечислимое число Silly может быть использовано не полностью, но само значение Take(2) будет полностью израсходовано.

Вопрос: при вызове dispose для Take перечислитель также вызовет dispose для перечислителя Silly и, в частности, sillier перечислитель?

Полагаю, компилятор может справиться с этим простым вариантом использования из-за foreach, а как быть с не такими простыми вариантами использования?

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    using(var sillier = source.GetEnumerator())
    {
        // move next can be called on different stages.
    }
}

Будет ли это когда-нибудь проблемой? поскольку большинство перечислителей не используют неуправляемые ресурсы, но если это так, это может привести к утечкам памяти.


Если распоряжение не называется, Как сделать одноразовым перечислимым?


Идея: может быть if(disposed) yield break; после каждого yield return. Теперь метод dispose для глупого перечислителя должен будет просто установить disposed = true и один раз переместить перечислитель, чтобы избавиться от всех необходимых вещей.

1 Ответ

0 голосов
/ 04 января 2019

Компилятор C # позаботится о lot для вас, когда он превратит ваш итератор в реальный код. Например, вот MoveNext, который содержит реализацию вашего второго примера 1 :

private bool MoveNext()
{
    try
    {
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                this.<sillier>5__1 = this.source.GetEnumerator();
                this.<>1__state = -3;
                while (this.<sillier>5__1.MoveNext())
                {
                    this.<>2__current = this.<sillier>5__1.Current;
                    this.<>1__state = 1;
                    return true;
                Label_005A:
                    this.<>1__state = -3;
                }
                this.<>m__Finally1();
                this.<sillier>5__1 = null;
                return false;

            case 1:
                goto Label_005A;
        }
        return false;
    }
    fault
    {
        this.System.IDisposable.Dispose();
    }
}

Итак, вы заметите, что предложение finally из вашего using вообще отсутствует, и это конечный автомат 2 , который полагается на то, что он находится в определенном состоянии (> = 0 ) государства для дальнейшего продвижения вперед. (Это также незаконно C #, но эй хо).

Теперь давайте посмотрим на его Dispose:

[DebuggerHidden]
void IDisposable.Dispose()
{
    switch (this.<>1__state)
    {
        case -3:
        case 1:
            try
            {
            }
            finally
            {
                this.<>m__Finally1();
            }
            break;
    }
}

Таким образом, мы видим, что здесь вызывается <>m__Finally1 (а также из-за выхода из цикла while в MoveNext.

А <>m__Finally1:

private void <>m__Finally1()
{
    this.<>1__state = -1;
    if (this.<sillier>5__1 != null)
    {
        this.<sillier>5__1.Dispose();
    }
}

Итак, мы можем видеть, что sillier был утилизирован и мы перешли в отрицательное состояние, что означает, что MoveNext не нужно выполнять никакой специальной работы для обработки «мы уже были уничтожены».

Итак,

Идея: возможен разрыв доходности if (disposed); после каждого возврата доходности. Теперь метод dispose для глупого перечислителя должен будет просто установить disposed = true и переместить перечислитель один раз, чтобы избавиться от всех необходимых вещей.

Совершенно не нужно. Доверяйте компилятору преобразовывать код так, чтобы он выполнял все логические вещи, которые он должен - он просто запускает свой оператор finally один раз, когда он либо исчерпал логику итератора, либо когда он явно расположен.


1 Все примеры кода, созданные .NET Reflector. Но сейчас слишком хорошо декомпилировать эти конструкции, так что если вы посмотрите на сам метод Silly:

[IteratorStateMachine(typeof(<Silly>d__1)), Extension]
private static IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    IEnumerator<T> <sillier>5__1;
    using (<sillier>5__1 = source.GetEnumerator())
    {
        while (<sillier>5__1.MoveNext())
        {
            yield return <sillier>5__1.Current;
        }
    }
    <sillier>5__1 = null;
}

Ему удалось скрыть большинство деталей об этом автомате. Вам нужно преследовать тип, на который ссылается атрибут IteratorStateMachine, чтобы увидеть все биты, показанные выше.


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

...