0Pricing
C# Academy · Урок

Семантика ленивого вычисления

Разберитесь в отложенном выполнении итераторов.

«Семантика ленивого вычисления» — бесплатный урок C# Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.

Отложенное выполнение

Итераторы и запросы LINQ используют отложенное выполнение: определение query почти ничего не делает. Работа начинается только при фактическом перечислении результата, по одному элементу за раз.

Ничего не выполняется до перечисления

Вызов метода-итератора не запускает его тело. Тело начинает выполняться только тогда, когда foreach (или другой потребитель) запрашивает первое значение.

using System;
using System.Collections.Generic;

public class Program
{
    static IEnumerable<int> Numbers()
    {
        Console.WriteLine("-- iterator body started --");
        yield return 1;
        yield return 2;
    }

    public static void Main()
    {
        var seq = Numbers();
        Console.WriteLine("query created, body not run yet");
        foreach (var n in seq) Console.WriteLine(n);
    }
}

Значения выдаются по одному

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

using System;
using System.Collections.Generic;

public class Program
{
    static IEnumerable<int> Trace()
    {
        for (int i = 1; i <= 3; i++)
        {
            Console.WriteLine("producing " + i);
            yield return i;
        }
    }

    public static void Main()
    {
        foreach (var n in Trace())
            Console.WriteLine("consumed " + n);
    }
}

Повторное перечисление запускает выполнение заново

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

using System;
using System.Collections.Generic;

public class Program
{
    static int _calls = 0;
    static IEnumerable<int> Counter()
    {
        _calls++;
        yield return _calls;
    }

    public static void Main()
    {
        var seq = Counter();
        foreach (var n in seq) Console.WriteLine(n);
        foreach (var n in seq) Console.WriteLine(n);
        Console.WriteLine("total runs: " + _calls);
    }
}

Работа с текущим состоянием

Поскольку выполнение отложено, итератор читает source во время перечисления, а не во время определения. Изменения, внесённые после определения query, будут видны.

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        var list = new List<int> { 1, 2 };
        IEnumerable<int> doubled = Double(list);
        list.Add(3); // added before enumeration
        foreach (var n in doubled) Console.WriteLine(n);
    }

    static IEnumerable<int> Double(IEnumerable<int> nums)
    {
        foreach (var n in nums) yield return n * 2;
    }
}

Принудительное немедленное выполнение

Чтобы зафиксировать результаты, материализуйте последовательность с помощью ToList() или ToArray(). Это однократно запускает итератор и сохраняет значения, отделяя результат от последующих изменений source.

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var list = new List<int> { 1, 2 };
        List<int> snapshot = Double(list).ToList(); // executed now
        list.Add(99);
        Console.WriteLine(string.Join(",", snapshot));
    }

    static IEnumerable<int> Double(IEnumerable<int> nums)
    {
        foreach (var n in nums) yield return n * 2;
    }
}

Досрочное прекращение экономит ресурсы

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

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    static IEnumerable<int> Numbers()
    {
        for (int i = 1; ; i++)
        {
            Console.WriteLine("checking " + i);
            yield return i;
        }
    }

    public static void Main()
    {
        int firstBig = Numbers().First(n => n > 3);
        Console.WriteLine("found " + firstBig);
    }
}

Отложенные исключения

Исключение внутри итератора возникает не при вызове метода, а в тот момент, когда перечисление доходит до строки с ошибкой. Это может удивить Вас, если Вы ожидаете предварительной проверки.

using System;
using System.Collections.Generic;

public class Program
{
    static IEnumerable<int> Risky(int divisor)
    {
        yield return 10 / divisor; // throws only during enumeration
    }

    public static void Main()
    {
        var seq = Risky(0);
        Console.WriteLine("no error yet");
        try { foreach (var n in seq) Console.WriteLine(n); }
        catch (DivideByZeroException) { Console.WriteLine("caught during enumeration"); }
    }
}

Ленивые конвейеры легко комбинируются

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

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var result = Enumerable.Range(1, 1000000)
            .Where(n => n % 2 == 0)
            .Select(n => n * n)
            .Take(3);
        foreach (var n in result) Console.WriteLine(n);
    }
}

Когда следует материализовать последовательность

Материализуйте последовательность, если будете перечислять её несколько раз, если source может измениться или если необходимо сохранить результаты до освобождения ресурса. В остальных случаях используйте ленивое выполнение, чтобы экономить память и вычислительные ресурсы.

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var evens = Enumerable.Range(1, 10).Where(n => n % 2 == 0).ToList();
        // Safe to enumerate many times now
        Console.WriteLine("count: " + evens.Count);
        Console.WriteLine("sum: " + evens.Sum());
    }
}

Попробуйте сами

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

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var source = new List<int> { 1, 2, 3 };
        var query = source.Where(n => n > 1); // not executed yet
        source.Add(10);
        source.Remove(2);
        // Executes now, reflecting all changes
        Console.WriteLine(string.Join(", ", query));
    }
}

Быстрая проверка

Порассуждайте об отложенном выполнении.

Итоги

Ленивое (отложенное) вычисление запускает последовательность только при её перечислении.

  • Определение query не выполняет работу, а её использование — выполняет.
  • Повторное перечисление повторно запускает итератор.
  • Данные из source читаются во время перечисления, поэтому последующие изменения видны.
  • Исключения становятся видны во время перечисления.
  • При необходимости используйте ToList()/ToArray() для материализации.

Часто задаваемые вопросы

Урок «Семантика ленивого вычисления» бесплатный?

Да — полный текст урока «Семантика ленивого вычисления» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.

Чему я научусь в уроке «Семантика ленивого вычисления»?

Разберитесь в отложенном выполнении итераторов. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C# Academy?

Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Семантика ленивого вычисления»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C# Academy?

Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Методы-итераторы с yield return
  2. yield break и досрочное завершение
  3. Семантика ленивого вычисления
  4. Пользовательские перечисляемые типы
← Назад к C# Academy