1. Введение
Вспоминаем область видимости
Давайте представим, что переменные — это сотрудники большого офиса, а методы, циклы и блоки кода — это комнаты и кабинеты. Некоторых сотрудников пускают только в свою комнату, а других — вообще во всё здание. Вот где кто может находиться — это и есть их область видимости (scope).
Область видимости определяет, где объявленная переменная “видна” в программе и где её можно использовать.
Основные виды областей видимости
В C# можно выделить такие основные области видимости:
| Область | Пример | Где "видна" переменная |
|---|---|---|
| Локальная | Внутри метода или блока | Только внутри этого блока |
| Параметр метода | В сигнатуре метода | Только внутри метода |
| Переменная класса (поле) | В теле класса вне методов | Во всех методах этого класса |
| Переменная внутри цикла/условия | Внутри цикла/if |
Только внутри этих |
Пример с пояснениями
public class Office
{
int buildingNumber = 50; // Поле класса: видно во всех методах public void PrintInfo() {
int roomNumber = 101; // Локальная переменная: видна только внутри PrintInfo if (roomNumber > 100) {
int deskNumber = 5; // Видна только внутри этого блока if Console.WriteLine(deskNumber);
} Console.WriteLine(deskNumber); //
Ошибка! deskNumber тут уже не видна
}
}
2. Локальные функции и область видимости
Кто кого "видит"?
Когда вы объявляете локальную функцию внутри метода (или даже внутри цикла или условия), она оказывается в той же области видимости, что и переменные, объявленные выше. Локальная функция — как бы “часть этого же кабинета”.
Пример
Локальная функция видит переменные из окружающей области
void PrintWithPrefix(string message)
{
string
prefix = "[ЛОГ]: "; void Print() {
Console.WriteLine(
prefix +
message); // видит обе переменные!
} Print();
}
Здесь переменные prefix и message видимы внутри локальной функции Print, потому что они объявлены в той же или в более широкой области.
Сколько тут областей?
В примере выше:
- есть область метода PrintWithPrefix
- внутри неё — область функции Print
3. Захват переменных (Capture)
Захват переменных — это ситуация, когда локальная функция использует переменные, которые были объявлены вне этой функции, но в той же области видимости.
Локальные функции и анонимные методы (лямбда-выражения, к ним придём чуть позднее) запоминают (или “захватывают”) все переменные, которые им были доступны при объявлении.
Можно сказать, что функции как будто делают снимок (capture) окружающего мира — и могут использовать эти переменные даже тогда, когда вызываются сильно позднее.
Схематично
Метод Main
└─ переменная x
└─ локальная функция F() ← "захватывает" x
Пример — простейший захват
void CounterExample()
{
int counter = 0;
void Increase()
{
counter++; // Эта функция захватывает переменную counter
}
Increase();
Increase();
Console.WriteLine(counter); // Выведет 2
}
Здесь после двух вызовов локальной функции Increase значение counter увеличивается до 2.
4. Применение захвата переменных
Захват переменных позволяет удобно “передавать” данные между областью метода и локальными функциями, не мучаясь с лишними параметрами.
Если бы захвата не было, вам приходилось бы передавать все переменные как параметры:
void CounterExampleWithoutCapture()
{
int counter = 0;
void Increase(ref int c)
{
c++;
}
Increase(ref counter);
Increase(ref counter);
Console.WriteLine(counter);
}
Это неудобно — зачем постоянно писать ref и портить сигнатуру функции, если она легко может “видеть” переменные снаружи?
5. Локальные функции и жизнь переменных после выхода из метода
Живут ли переменные дольше метода?
Если вы передаёте локальные функции (или делегаты с лямбдами) куда-то вне текущего метода, захваченные переменные автоматически перестают “умирать” при выходе из метода. CLR (виртуальная машина .NET) позаботится — всё нужное будет “держаться за уши” в памяти.
Пример: функции живут вне метода
Func<int> GetCounter()
{
int count = 0;
int Increment()
{
count++;
return count;
}
return Increment; // Возвращаем функцию наружу!
}
var counter = GetCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Здесь, даже после завершения метода GetCounter, переменная count продолжает жить, потому что возвращённая функция её захватила. Это называется замыкание (closure) — мы к этому ещё придём в отдельной лекции, но на уровне локальных функций механизм такой же.
6. Типичные ошибки и интересные сценарии
Перезапись переменной до вызова локальной функции
Иногда можно столкнуться с тем, что переменная, которую захватывает локальная функция, изменяется до её вызова — и тогда результат может отличаться от ожидаемого.
Пример:
void Example()
{
int x = 42;
void PrintX() { Console.WriteLine(x); }
x = 100; // Переменную изменили!
PrintX(); // Выведет 100, а не 42!
}
Трюк: Локальная функция всегда видит самое последнее значение переменной на момент вызова.
Захват переменных в цикле for/foreach (ещё раз)
Классическая боль: если вы пишете логику на собеседовании или в крупном проекте, всегда проверяйте: а не захватываю ли я "живую" переменную из цикла, и как она себя поведёт.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ