Как yield реализует шаблон ленивой загрузки? - PullRequest
9 голосов
/ 03 мая 2010

Как yield реализует шаблон lazy loading?

Ответы [ 3 ]

13 голосов
/ 03 мая 2010

реализация yield не достигает кода, пока не понадобится.

Например, этот код:

public IEnumerable<int> GetInts()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

Фактически скомпилируется во вложенный класс, который реализует IEnumerable<int>, а тело GetInts() вернет экземпляр этого класса.

Используя отражатель, вы можете увидеть:

public IEnumerable<int> GetInts()
{
    <GetInts>d__6d d__d = new <GetInts>d__6d(-2);
    d__d.<>4__this = this;
    return d__d;
}

Редактировать - добавить дополнительную информацию о реализации GetInts:
Способ, которым эта реализация делает его ленивым, основан на методе Enumerator MoveNext(). Когда генерируется перечислимый вложенный класс (<GetInts>d__6d в примере), он имеет состояние и к каждому состоянию подключается значение (это простой случай, в более сложных случаях значение будет оцениваться, когда код достигнет состояния ). Если мы посмотрим на MoveNext() код <GetInts>d__6d, то увидим состояние:

private bool MoveNext()
{
    switch (this.<>1__state)
    {
        case 0:
            this.<>1__state = -1;
            this.<>2__current = 1;
            this.<>1__state = 1;
            return true;

        case 1:
            this.<>1__state = -1;
            this.<>2__current = 2;
            this.<>1__state = 2;
            return true;

        case 2:
            this.<>1__state = -1;
            this.<>2__current = 3;
            this.<>1__state = 3;
            return true;

        case 3:
            this.<>1__state = -1;
            break;
    }
    return false;
}

Когда у счетчика запрашивается текущий объект, он возвращает объект, который связан с текущим состоянием.

Чтобы показать, что код оценивается только тогда, когда он требуется, вы можете взглянуть на этот пример:

[TestFixture]
public class YieldExample
{
    private int flag = 0;
    public IEnumerable<int> GetInts()
    {
        yield return 1;
        flag = 1;
        yield return 2;
        flag = 2;
        yield return 3;
        flag = 3;
    }

    [Test]
    public void Test()
    {
        int expectedFlag = 0;
        foreach (var i in GetInts())
        {
            Assert.That(flag, Is.EqualTo(expectedFlag));
            expectedFlag++;
        }

        Assert.That(flag, Is.EqualTo(expectedFlag));
    }
}

Надеюсь, это немного яснее. Я рекомендую взглянуть на код с помощью Reflector и наблюдать за скомпилированным кодом при изменении кода «yield».

7 голосов
/ 03 мая 2010

Если вы хотите узнать больше о том, что делает компилятор при использовании yield return, прочитайте эту статью от Jon Skeet: Подробности реализации блока итератора

4 голосов
/ 03 мая 2010

В основном итераторы, реализованные с помощью операторов yield, скомпилированы в класс, реализующий конечный автомат .

Если вы никогда не foreach (= итерируете и фактически используете) возвращенное IEnumerable<T>, код фактически никогда не выполняется. И если вы это сделаете, будет выполнен только минимальный код, необходимый для определения следующего возвращаемого значения, только для возобновления выполнения при запросе следующего значения.

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

...