Семантика ленивого вычисления
Разберитесь в отложенном выполнении итераторов.
«Семантика ленивого вычисления» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Методы-итераторы с yield return
- yield break и досрочное завершение
- Семантика ленивого вычисления
- Пользовательские перечисляемые типы