Могу ли я использовать «использование» в методе yield-return? - PullRequest
2 голосов
/ 18 октября 2019

Я только что видел видео на YouTube, где преподаватель использовал метод yield return, чтобы открыть файл и прочитать из него строки, которые возвращаются yield для вызывающей стороны (фактический код находился в блоке using вокруг FileStream).

Тогда мне стало интересно, можно ли использовать «using» или «try-finally» в методе доходности-возврата. Потому что я понимаю, что метод работает только до тех пор, пока от него получают значения. Например, с помощью метода «Any ()» метод выполняется после первого возврата доходности (или разрыва доходности, конечно).

Итак, если функция никогда не заканчивается до конца, когда выполняется блок finally? Безопасно ли использовать такую ​​конструкцию?

Ответы [ 2 ]

3 голосов
/ 18 октября 2019

IEnumerator<T> реализует IDisposable, и циклы foreach будут располагать то, что они перечисляют, когда они закончат (это включает методы linq, которые используют цикл foreach, такой как .ToArray()).

Оказывается, что конечный автомат, сгенерированный компилятором для методов генератора, реализует умный способ Dispose: если конечный автомат находится в состоянии, которое находится «внутри» блока using, тогда вызывается Dispose() на конечном автомате будет располагать объект, защищенный оператором using.


Давайте рассмотрим пример:

public IEnumerable<string> M() {
    yield return "1";
    using (var ms = new MemoryStream())
    {
        yield return "2";  
        yield return "3";
    }
    yield return "4";
}

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

Ядром конечного автомата является следующий оператор switch, который отслеживает наш прогресс после каждого из операторов yield return:

switch (<>1__state)
{
    default:
        return false;
    case 0:
        <>1__state = -1;
        <>2__current = "1";
        <>1__state = 1;
        return true;
    case 1:
        <>1__state = -1;
        <ms>5__1 = new MemoryStream();
        <>1__state = -3;
        <>2__current = "2";
        <>1__state = 2;
        return true;
    case 2:
        <>1__state = -3;
        <>2__current = "3";
        <>1__state = 3;
        return true;
    case 3:
        <>1__state = -3;
        <>m__Finally1();
        <ms>5__1 = null;
        <>2__current = "4";
        <>1__state = 4;
        return true;
    case 4:
        <>1__state = -1;
        return false;
}

Вы можете видеть, что мы создаем MemoryStream при входе в состояние 2 и удаляем его (вызывая <>m__Finally1()) при выходе из состояния 3.

Вот метод Dispose:

void IDisposable.Dispose()
{
    int num = <>1__state;
    if (num == -3 || (uint)(num - 2) <= 1u)
    {
        try
        {
        }
        finally
        {
            <>m__Finally1();
        }
    }
}

Если мы находимся в состояниях -3, 2 или 3, тогда мы назовем <>m__Finally1();. Состояния 2 и 3 - это те, которые находятся внутри блока using.

(Состояние -3 кажется защитником в случае, если мы написали yield return Foo() и Foo() сгенерировали исключение: в этом случае мы бы остались всостояние -3 и мы не сможем продолжить итерацию далее. Однако нам все еще разрешено использовать MemoryStream в этом случае).

Просто для полноты <>m__Finally1 определяется как:

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

Спецификацию для этого можно найти в C # Language Specification , раздел 10.14.4.3:

  • Если состояние объекта перечислителяприостанавливается, вызывая Dispose:
    • Изменяет состояние на рабочее.
    • Выполняет любые блоки finally, как если бы последний выполненный оператор yield return был оператором yield break. Если это вызывает исключение, которое выдается и распространяется из тела итератора, состояние объекта-перечислителя устанавливается равным after, а исключение распространяется на вызывающую функцию метода Dispose.
    • Изменяет состояние на after.
2 голосов
/ 18 октября 2019

Я только что написал некоторый тестовый код, и кажется, что деструктор вызывается каждый раз в соответствующую точку.

struct Test : IDisposable
{
    public void Dispose() => Console.WriteLine("Destructor called");
}

static IEnumerable<int> InfiniteInts()
{
    Console.WriteLine("Constructor Called");
    using(var test = new Test()) {
        int i = 0;
        while(true)
            yield return ++i;
    }
}

static void Main(string[] args)
{
    var seq = InfiniteInts();
    Console.WriteLine("Call Any()");
    bool b = seq.Any();
    Console.WriteLine("Call Take().ToArray()");
    int[] someInts = seq.Take(20).ToArray();

    Console.WriteLine("foreach loop");
    foreach(int i in seq)
    {
        if(i > 20) break;
    }

    Console.WriteLine("do it manually: while loop");
    var enumerator = seq.GetEnumerator();
    while(enumerator.MoveNext())
    {
         int i = enumerator.Current;
         if(i > 20) break;
    }
    Console.WriteLine("No destructor call has happened!");

    enumerator.Dispose();
    Console.WriteLine("Now destructor has beend called");

    Console.WriteLine("End of Block");
}

После того, как я позвонил seq.Any(), я уже получил сообщение "Destuctor called" до того, как"Call Take().ToArray()" message.

То же самое было для оператора seq.Take(20).ToArray(). Деструктора назвали.

Я вырыл немного глубже. Кажется, что созданный IEnumerator<int> сам является IDisposable. Все методы Linq, вероятно, вызывают этот метод Dispose, когда они завершены.

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

...