Перейти к содержимому

Область видимости в 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 = 10
x = 5
▶ Попробуй сам

y существует только внутри внутреннего блока { }. Как только блок заканчивается, 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
▶ Попробуй сам

Эта программа не компилируется, потому что 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 по-прежнему нужна только внутри блока и удаляется, когда блок заканчивается.

Создай переменную 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