Замыкания и захваченные переменные
Узнайте, как локальные функции захватывают состояние.
«Замыкания и захваченные переменные» — бесплатный урок C# Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Замыкания над локальным состоянием
Локальная функция (или лямбда-выражение) может читать и изменять переменные из содержащего её метода. Это называется захватом, а функция вместе с захваченными переменными образует замыкание.
Захват переменной
Локальная функция напрямую использует factor из содержащего её метода. Она не получает его как параметр, а захватывает из окружающей области видимости.
using System;
public class Program
{
public static void Main()
{
int factor = 3;
int Scale(int n) => n * factor; // captures factor
Console.WriteLine(Scale(5));
Console.WriteLine(Scale(10));
}
}Захваченные переменные остаются актуальными ссылками
Замыкание захватывает переменную, а не снимок её значения. Если переменная изменится после определения функции, функция увидит новое значение.
using System;
public class Program
{
public static void Main()
{
int offset = 10;
int Add(int n) => n + offset;
Console.WriteLine(Add(1)); // 11
offset = 100;
Console.WriteLine(Add(1)); // 101 - sees updated offset
}
}Изменение захваченных переменных
Замыкание может записывать значение в захваченную переменную, и это изменение также видно в содержащем методе. Благодаря этому замыкания удобно использовать для накопления состояния.
using System;
public class Program
{
public static void Main()
{
int count = 0;
void Tick() => count++;
Tick(); Tick(); Tick();
Console.WriteLine(count); // 3
}
}Возврат замыкания
Когда вы возвращаете лямбда-выражение, захватившее локальные переменные, эти переменные переживают завершение метода. Захваченное состояние живёт столько же, сколько и делегат.
using System;
public class Program
{
static Func<int> MakeCounter()
{
int count = 0;
return () => ++count; // captures count
}
public static void Main()
{
var next = MakeCounter();
Console.WriteLine(next());
Console.WriteLine(next());
Console.WriteLine(next());
}
}Независимые замыкания
Каждый вызов фабрики создаёт новый набор захваченных переменных. Два созданных таким образом счётчика не влияют друг на друга.
using System;
public class Program
{
static Func<int> MakeCounter()
{
int count = 0;
return () => ++count;
}
public static void Main()
{
var a = MakeCounter();
var b = MakeCounter();
Console.WriteLine(a()); // 1
Console.WriteLine(a()); // 2
Console.WriteLine(b()); // 1 - separate state
}
}Исправление захвата в цикле foreach
В современном C# переменная цикла foreach создаётся заново на каждой итерации, поэтому замыкания захватывают ожидаемое значение. Каждое приведённое ниже лямбда-выражение запоминает собственный элемент.
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var actions = new List<Action>();
foreach (var i in new[] { 1, 2, 3 })
actions.Add(() => Console.WriteLine(i));
foreach (var act in actions) act(); // prints 1, 2, 3
}
}Захват в цикле for
Классическая ошибка: одна переменная цикла for используется во всех итерациях, поэтому все замыкания видят её последнее значение. Скопируйте её в локальную переменную, создаваемую для каждой итерации, чтобы захватить разные значения.
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i; // fresh local each iteration
actions.Add(() => Console.WriteLine(copy));
}
foreach (var act in actions) act(); // prints 0, 1, 2
}
}Замыкания захватывают по ссылке, а не по значению
Поскольку захват выполняется по ссылке, изменение исходной переменной после захвата будет замечено замыканием. Помните об этом, когда важен момент чтения и записи.
using System;
public class Program
{
public static void Main()
{
string message = "first";
Action print = () => Console.WriteLine(message);
print(); // first
message = "second";
print(); // second
}
}Практическое применение: настраиваемое поведение
Замыкания позволяют создавать небольшие функции, заранее снабжённые контекстом. Здесь функция скидки захватывает ставку и создаёт повторно используемый настроенный вычислитель.
using System;
public class Program
{
static Func<double, double> MakeDiscount(double rate)
=> price => price * (1 - rate);
public static void Main()
{
var tenOff = MakeDiscount(0.10);
var halfOff = MakeDiscount(0.50);
Console.WriteLine(tenOff(100));
Console.WriteLine(halfOff(100));
}
}Попробуйте сами
Создайте фабрику счётчиков, возвращаемое замыкание которой сохраняет доступное ему частное состояние, а затем создайте с её помощью два независимых счётчика.
using System;
public class Program
{
static Func<int> MakeStepper(int step)
{
int current = 0;
return () => { current += step; return current; }; // captures current and step
}
public static void Main()
{
var byTwos = MakeStepper(2);
var byTens = MakeStepper(10);
Console.WriteLine(byTwos()); // 2
Console.WriteLine(byTwos()); // 4
Console.WriteLine(byTens()); // 10 - separate state
}
}Быстрая проверка
Разберитесь, как работает захват.
Повторение
Замыкания захватывают переменные из содержащего контекста по ссылке.
- Функция видит текущее значение переменной, включая последующие изменения.
- Она может изменять захваченные переменные, совместно используя состояние с методом.
- Возвращённые замыкания сохраняют захваченные переменные.
- В цикле
forиспользуйте локальную переменную для каждой итерации, чтобы захватывать разные значения.
Часто задаваемые вопросы
Урок «Замыкания и захваченные переменные» бесплатный?
Да — полный текст урока «Замыкания и захваченные переменные» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Замыкания и захваченные переменные»?
Узнайте, как локальные функции захватывают состояние. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Замыкания и захваченные переменные»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Объявление локальных функций
- Замыкания и захваченные переменные
- Статические локальные функции
- Локальные функции и лямбда-выражения