Как доход реализует образец ленивой загрузки?

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

Ответ 1

Результат реализации не доходит до кода, пока он не понадобится.

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

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".

Ответ 2

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

Ответ 3

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

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

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