$ sudo teach IT
Модуль 1 · Первые шаги · Урок 1.3

Переменные и изменяемость

Вы научитесь хранить значения под именами, поймёте, почему Rust не даёт их менять без разрешения, и заведёте в «Копилке» лимит месяца и счётчик трат.

Теория + практика~20 минутНовичок

Наша «Копилка» пока умеет только печатать две строки. Чтобы считать траты, ей нужно запоминать числа: лимит месяца, сколько потрачено на данный момент. Для этого в любом языке есть переменные, а в Rust у них есть особенность, которая сначала удивляет, а потом начинает нравиться: по умолчанию их нельзя менять.

let: коробка с наклейкой

Представьте коробку, на которую приклеена бумажка с названием, например «лимит». Внутрь положили число. Когда вам нужно число, вы не вспоминаете его, а говорите: «возьми то, что в коробке „лимит“». Переменная работает так же: это имя, под которым в памяти лежит значение.

Создаётся переменная ключевым словом let (по-английски «пусть»): «пусть limit будет равно 30000». Само действие называется объявлением переменной, а знак = кладёт значение в коробку. Чтобы напечатать содержимое, в строке пишут фигурные скобки: либо пустые {} и значение после запятой, либо имя прямо внутри, как {limit}.

Rust · main.rs
fn main() {
    let limit = 30000;
    println!("Лимит: {}", limit);
    println!("Лимит: {limit}");
}

Разбор по строкам

  • let limit = 30000; — объявляем переменную limit и кладём в неё число 30000. Точка с запятой в конце, как всегда.
  • println!("Лимит: {}", limit); — пустые скобки {} — место для значения; сам limit стоит после запятой.
  • println!("Лимит: {limit}"); — то же самое короче: имя переменной прямо внутри скобок. В курсе мы будем писать именно так.
Вывод
Лимит: 30000
Лимит: 30000

Почему значение нельзя поменять

Попробуем то, что в других языках делают не задумываясь: положим в «потрачено» ноль, а потом прибавим первую трату.

Rust · main.rs
fn main() {
    let spent = 0;
    spent = spent + 350;
    println!("{spent}");
}

Компилятор остановится и скажет:

Ошибка компилятора
error[E0384]: cannot assign twice to immutable variable `spent`
 --> src/main.rs:3:5
  |
2 |     let spent = 0;
  |         ----- first assignment to `spent`
3 |     spent = spent + 350;
  |     ^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut spent = 0;
  |         +++

Читаем по-человечески: «нельзя присвоить дважды неизменяемой переменной spent». Первое значение она получила в строке 2, а в строке 3 мы попытались положить второе. Внизу компилятор сам подсказывает выход: добавить mut.

Такое свойство называется неизменяемостью по умолчанию (immutability): переменная, объявленная через let, после первого значения только читается. Это не придирка. Когда вы видите в большой программе let limit = 30000;, вы твёрдо знаете, что дальше limit везде равен 30000, и не нужно проверять, не изменил ли его кто-то по дороге. Меняться разрешено только тому, что вы сами пометили.

mut: разрешаем менять

Пометка называется mut (от mutable, «изменяемый»). Пишется она сразу после let. Теперь коробку можно открывать и класть в неё новое значение.

Rust · main.rs
fn main() {
    let mut spent = 0;
    println!("Потрачено: {spent}");
    spent = spent + 350;
    println!("Потрачено: {spent}");
    spent += 1200;
    println!("Потрачено: {spent}");
}

Разбор по строкам

  • let mut spent = 0; — spent объявлена изменяемой, начальное значение 0.
  • spent = spent + 350; — берём старое значение, прибавляем 350 и кладём результат обратно. Справа от = всегда считается первым.
  • spent += 1200; — сокращённая запись той же идеи: «прибавить к spent 1200». Есть и -=, *=, но нам пока хватит +=.
Вывод
Потрачено: 0
Потрачено: 350
Потрачено: 1550

Менять можно значение, но не вид данных

Изменяемая переменная остаётся числовой коробкой. Положить в spent текст не получится: компилятор сообщит error[E0308]: mismatched types. Если нужна коробка другого типа, пригодится затенение, о нём чуть ниже.

const: константа на всю программу

Есть значения, которые не меняются никогда: лимит месяца, число дней в неделе, ставка налога. Для них в Rust есть константы. Это не просто неизменяемая переменная, а значение, известное ещё до запуска программы и прописанное в ней навсегда.

Rust · main.rs
const MONTH_LIMIT: u32 = 30_000;

fn main() {
    println!("Лимит месяца: {MONTH_LIMIT} руб.");
}

Разбор по строкам

  • const MONTH_LIMIT: u32 = 30_000; — ключевое слово const, имя, двоеточие, тип и значение. u32 — целое число без минуса; типы мы разберём в следующем уроке.
  • MONTH_LIMIT — имя константы принято писать ЗАГЛАВНЫМИ буквами через подчёркивание, чтобы отличать от обычных переменных.
  • 30_000 — подчёркивания внутри числа ничего не значат и нужны для читаемости, как пробел в «30 000».
  • const — ставится над main, снаружи: тогда константу видно во всей программе.
letlet mutconst
Можно менятьнетданет, никогда
Тип указыватьне обязательноне обязательнообязательно
Где объявлятьвнутри функциивнутри функциигде угодно, обычно сверху

Затенение: то же имя, новая коробка

Есть ещё один способ «поменять» значение, и он вовсе не про mut. Если написать let с уже занятым именем, Rust заводит новую переменную, а старая становится недоступной: новая её «затеняет». По-английски — shadowing.

Rust · main.rs
fn main() {
    let spent = 350;
    let spent = spent + 1200;
    println!("{spent}");

    let spent = "много";
    println!("{spent}");
}

Разбор по строкам

  • let spent = 350; — первая коробка с числом.
  • let spent = spent + 1200; — справа ещё видна старая spent, из неё берём 350 и прибавляем; слева создаётся новая, которая теперь и называется spent.
  • let spent = "много"; — новая коробка с текстом. Для затенения это допустимо, потому что это новая переменная, а не изменение старой.
Вывод
1550
много

Затенение и mut — не одно и то же

С mut коробка одна, и тип в ней неизменен. При затенении каждый раз новая коробка, и тип может быть любым. Зато слово let перед каждым шагом обязательно: без него получится обычное присваивание и ошибка E0384.

Практика: лимит и траты в «Копилке»

Продолжаем проект kopilka, который создали в прошлом уроке. Откройте папку проекта в редакторе и замените содержимое src/main.rs целиком:

Rust · main.rs
const MONTH_LIMIT: u32 = 30_000;

fn main() {
    println!("Копилка 0.1");
    println!("Учёт расходов за месяц");

    let mut spent = 0;
    spent += 350;
    spent += 1200;

    println!("Потрачено: {spent} руб. из {MONTH_LIMIT} руб.");
}

Что изменилось: над main появилась константа с лимитом, а внутри — изменяемая переменная spent, в которую мы записали две пробные траты. Запустите программу из папки kopilka:

Терминал
cargo run

Должно получиться:

Вывод
Копилка 0.1
Учёт расходов за месяц
Потрачено: 1550 руб. из 30000 руб.

Теперь поэкспериментируйте. Это быстрее любого объяснения.

  • Удалите слово mut и запустите cargo run: вы должны увидеть уже знакомую error[E0384] со стрелкой на строку spent += 350;. Верните mut.
  • Уберите : u32 из константы: компилятор напишет missing type for `const` item. Верните тип.
  • Добавьте строку let bonus = 5; и не используйте её: появится предупреждение unused variable, но программа запустится. Удалите строку.

Проверьте себя

Вы справились, если можете ответить: 1) почему без mut строка spent += 350; не собирается; 2) чем const отличается от let; 3) что изменится в выводе, если поменять 30_000 на 25_000; 4) что делает вторая строка let spent = spent + 1;: меняет коробку или заводит новую.

Частые ошибки

1. Забыли mut. Видно: error[E0384]: cannot assign twice to immutable variable. Причина: переменная объявлена через let без mut, а вы её меняете. Исправление: let mut spent = 0;, как подсказывает строка help: в самой ошибке.

2. const без типа. Видно: error: missing type for `const` item и подсказка добавить тип. Причина: у констант тип обязателен, Rust его не выводит. Исправление: const MONTH_LIMIT: u32 = 30_000;.

3. Предупреждение unused variable. Видно: warning: unused variable: `bonus`. Это не ошибка, программа всё равно соберётся: просто вы объявили переменную и ни разу ею не воспользовались. Исправление: удалить её или, если она нужна «на потом», начать имя с подчёркивания — _bonus, и предупреждение пропадёт.

4. Присваивание вместо затенения. Хотели положить в переменную текст, а написали spent = "много"; без let. Видно: error[E0308]: mismatched types. Исправление: либо let spent = "много"; (новая коробка), либо отдельное имя.

Что важно запомнить

  • let имя = значение; создаёт переменную; по умолчанию она неизменяема.
  • Попытка изменить такую переменную даёт error[E0384]; лечится словом mut: let mut spent = 0;.
  • += прибавляет к изменяемой переменной: spent += 350;.
  • const ИМЯ: тип = значение; — константа: тип обязателен, имя ЗАГЛАВНЫМИ, менять нельзя никогда, ставится над main.
  • Затенение (let x = x + 1;) создаёт новую переменную с тем же именем и позволяет сменить тип.
  • {имя} внутри строки println! подставляет значение переменной.

Подробнее — в главе Variables and Mutability официальной книги.

Дальше: в spent лежит число, но какие вообще бывают числа, текст и логические значения, и как положить в одну переменную сразу несколько трат? Об этом — в уроке про типы данных.

На следующий урок