Область видимости в Rust
Область видимости (scope) — это участок кода, в котором переменная действительна. Идея простая, но когда она становится понятной, многие другие правила Rust (особенно владение) сразу встают на свои места.
Область видимости задают фигурные скобки
Заголовок раздела «Область видимости задают фигурные скобки»Переменная существует с момента объявления и до закрывающей } того блока, в котором она объявлена:
fn main() { let x = 5; // здесь x появляется в области видимости
{ let y = 10; // здесь появляется y println!("x = {x}, y = {y}"); // обе действительны } // здесь y выходит из области видимости
println!("x = {x}"); // println!("{y}"); // ❌ ошибка: y уже вне области видимости}Вывод:
x = 5, y = 10x = 5y существует только внутри внутреннего блока { }. Как только блок заканчивается, y исчезает, и попытка использовать её дальше — это ошибка компиляции, а не неожиданность во время работы программы.
Видимость работает только в одну сторону: внутренний блок видит переменные окружающих его блоков (поэтому внутренний блок может вывести x), а внешний блок не может заглянуть во внутренний.
Тело функции — тоже область видимости
Заголовок раздела «Тело функции — тоже область видимости»Параметры и все переменные, созданные внутри функции, существуют только пока эта функция выполняется:
fn calculate() -> i32 { let result = 5 * 2; // result существует только внутри calculate result}
fn main() { let value = calculate(); println!("{value}"); // println!("{result}"); // ❌ ошибка: здесь result не существует}Вывод:
10В обратную сторону это тоже верно: функция не видит локальные переменные main. Если ей нужно значение, передай его как параметр — так, как ты видел в уроке Функции.
Блок — тоже выражение
Заголовок раздела «Блок — тоже выражение»Блок { } не только ограничивает, где живут переменные. Он ещё и может давать значение — последнее выражение внутри него, записанное без точки с запятой. Это то же правило, по которому функция возвращает свою последнюю строку.
Это удобно, когда для вычисления нужно несколько вспомогательных переменных, но не хочется, чтобы они болтались в коде дальше:
fn main() { let price = 40;
let total = { let tax = price / 10; // tax живёт только внутри этого блока let shipping = 5; price + tax + shipping // без точки с запятой: это значение блока };
println!("итого = {total}"); // println!("{tax}"); // ❌ ошибка: tax вне области видимости}Вывод:
итого = 49Две детали, за которыми стоит следить:
- В последней строке
price + tax + shippingнет точки с запятой. Если её поставить, блок вернёт()(ничего) вместо числа, иprintln!не сможет это вывести. - Вся конструкция — это инструкция
let, поэтому после закрывающей скобки стоит};.
Когда блок заканчивается, tax и shipping исчезают. Остаётся только результат — total. Если держать вспомогательные переменные в таком маленьком блоке, сразу видно, что они нужны только для одного вычисления.
Почему область видимости важна для владения
Заголовок раздела «Почему область видимости важна для владения»Это напрямую связано с владением, с которым ты познакомишься чуть позже: значение автоматически освобождается в тот момент, когда заканчивается область видимости его владельца.
fn main() { { let text = String::from("привет"); println!("{text}"); } // здесь заканчивается область видимости text — Rust сам её удаляет}Вывод:
приветВ этом и весь механизм, благодаря которому Rust не нужен сборщик мусора: ещё на этапе компиляции он точно знает, где заканчивается каждая область видимости.
Затенение и область видимости
Заголовок раздела «Затенение и область видимости»Помнишь затенение из урока Переменные? Каждый let с тем же именем внутри области видимости создаёт отдельную переменную. Он не стирает внешнюю, а просто подменяет её до конца этой области:
fn main() { let x = 5; { let x = x * 2; // новая x, только внутри этого блока println!("внутренняя x = {x}"); // 10 } println!("внешняя x = {x}"); // 5 — не изменилась}Вывод:
внутренняя x = 10внешняя x = 5Практика
Заголовок раздела «Практика»1. Исправь ошибку области видимости
Заголовок раздела «1. Исправь ошибку области видимости»Эта программа не компилируется, потому что area используется после того, как её блок закончился:
fn main() { let width = 3; { let height = 4; let area = width * height; } println!("площадь = {area}");}Измени её так, чтобы она выводила площадь = 12, а height по-прежнему жила только внутри блока. Подсказка: пусть блок даёт площадь, а результат сохрани в переменную, объявленную снаружи.
Показать решение
fn main() { let width = 3; let area = { let height = 4; width * height }; println!("площадь = {area}");}Вывод:
площадь = 12Теперь area живёт в области видимости main, и println! её видит. height по-прежнему нужна только внутри блока и удаляется, когда блок заканчивается.
2. Уровни вложенности
Заголовок раздела «2. Уровни вложенности»Создай переменную level, равную 1. Открой блок, который затеняет её значением 2, а внутри него — ещё один блок, который затеняет её значением 3. Выведи level в каждом блоке и ещё раз сразу после закрытия каждого блока. Подсказка: числа должны идти 1, 2, 3, а потом обратно 2 и 1.
Показать решение
fn main() { let level = 1; println!("уровень {level}"); { let level = 2; println!("уровень {level}"); { let level = 3; println!("уровень {level}"); } println!("снова уровень {level}"); } println!("снова уровень {level}");}Вывод:
уровень 1уровень 2уровень 3снова уровень 2снова уровень 1Каждая внутренняя level прячет внешнюю только до конца своего блока. Внешние переменные никто не менял, поэтому они никуда не делись и ждут своей очереди.
- Область видимости — это участок кода (обозначенный
{ }), где переменная действительна. - Переменная перестаёт существовать в тот момент, когда достигнута закрывающая
}её области. - Тело функции — тоже область видимости: локальные переменные исчезают, когда функция возвращает результат.
- Блок — это выражение: его последняя строка (без точки с запятой) — это его значение, а вспомогательные переменные исчезают, когда он заканчивается.
- Именно область видимости говорит Rust, когда удалить значение, поэтому сборщик мусора не нужен.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Почему, судя по этой странице, Rust не нужен сборщик мусора?
2. Что произойдёт, если затенить переменную через let x = x * 2; во внутреннем блоке?
3. Чему равно total после let total = { let a = 2; a * 10 };?
Результат: 0 / 3
Что почитать
Заголовок раздела «Что почитать»- Область видимости переменной — книга «Язык программирования Rust»: как работает область видимости — как первый шаг к владению (перевод на русский).
- Область видимости и затенение — Rust на примерах: короткий запускаемый пример с блоками и затенением (перевод на русский).
- RAII — Rust на примерах: что происходит со значениями, когда заканчивается их область видимости, — когда будешь готов к следующему шагу (перевод на русский).
- Block expressions — The Rust Reference (на английском): точные правила для блоков и значений, которые они дают.