сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Основы Rust

Типы, let и mut в Rust

Переменные, типы и арифметика

С этого начинают почти любой Rust-собес: чем mut отличается от повторного let, что будет при переполнении, почему as молча портит число. Проверяют не зубрёжку синтаксиса, а понимание, что в языке нет неявных сюрпризов - всё, что может пойти не так, названо явно.

Стержень: значение по умолчанию неизменяемо, преобразования типов явные, а поведение при переполнении определено и различается между debug и release.

// Формулировки: «чем shadowing отличается от mut?», «что вернёт 300i32 as u8?», «чем const отличается от static?»

mut и shadowing - разные вещи

let x = 5 создаёт неизменяемую привязку. Хочешь менять значение в той же ячейке - пиши let mut x. Но есть второй путь: объявить let x ещё раз. Это shadowing, и он заводит новую переменную с тем же именем - старая продолжает жить до конца области видимости, просто до неё больше не дотянуться по имени.

Практическая разница одна и важная: при shadowing можно сменить тип, при mut - нет. Отсюда идиома разбора входных данных: строку читаем, потом перекрываем её же именем уже как число.

// Побочный эффект: если случайно объявить let дважды вместо присваивания, компилятор не ругнётся - переменная просто «перекроется». Это одна из немногих ловушек, которые язык не ловит.

let s = "42";
let s: i32 = s.parse().unwrap();
let mut n = 1;
n += 1; // тип менять нельзя
shadowing
повторный let с тем же именем: новая переменная, можно сменить тип
привязка
связь имени со значением; по умолчанию неизменяемая

Переполнение: паника или обёртка

В C переполнение знакового числа - неопределённое поведение, и это источник дыр. В Rust оно определено, но ведёт себя по-разному: в debug включены проверки, и поток паникует; в release проверки выключены, и результат оборачивается по модулю - 255u8 + 1 даёт 0.

Когда важен конкретный исход, его пишут явно: checked_add вернёт Option (None при переполнении), saturating_add упрётся в границу типа, wrapping_add честно обернётся. Такой код одинаково работает в обеих сборках и читается без догадок.

// Отсюда же ответ на вопрос про as: 300i32 as u8 == 44, потому что as просто берёт младшие биты. Если потеря данных недопустима - TryFrom и разбор Err.

let x: u8 = 255;
x.checked_add(1);    // None
x.saturating_add(1); // 255
x.wrapping_add(1);   // 0
overflow-checks
проверки переполнения: включены в debug, выключены в release
TryFrom
преобразование с проверкой; возвращает Result вместо усечения

const, static и блоки-выражения

const это значение, которое подставляется в каждое место использования: собственного адреса у него нет, дублирование нормально. static - одна ячейка с фиксированным адресом и лайфтаймом 'static, на неё можно взять ссылку. Изменяемый static требует unsafe, потому что синхронизации у него нет.

Ещё одна базовая деталь: блок в Rust - выражение. Значение последней строки без точки с запятой становится значением блока и функции. Поставил точку с запятой - получил (), и компилятор пожалуется уже там, где результат попробуют использовать.

const MAX: u32 = 100;
static NAME: &str = "сеньорчик";
let y = { let z = 2; z * 10 }; // 20
const
константа, подставляемая значением в места использования
static
единственная ячейка с адресом и лайфтаймом 'static

Как отвечать: «Что будет при переполнении i32 и как это контролировать?»

Переполнение в Rust определено, это не UB как в C. В debug-сборке включены overflow-checks, и поток паникует. В release проверки выключены, и результат оборачивается по модулю. Если поведение важно для логики, я не полагаюсь на профиль сборки, а пишу явно: checked_add, когда переполнение - ошибка и её надо обработать, saturating_add, когда нужно упереться в границу, wrapping_add, когда обёртка это и есть намерение - например, в хешах или счётчиках.

Ответ показывает, что ты знаешь и разницу профилей, и то, что на неё не стоит закладываться. Явные методы - признак человека, который писал прод, а не только учебные примеры.

На чём валятся

  • Говорят, что переполнение в release это UB (undefined behavior). Нет: поведение определено, результат оборачивается по модулю.
  • Путают shadowing с mut. Второй let создаёт новую переменную и разрешает сменить тип, mut только меняет значение.
  • Считают, что as безопасно приводит числа. Он молча отбрасывает старшие биты; проверку даёт TryFrom.
  • Забывают, что лишняя точка с запятой в конце функции превращает возврат в ().
  • Не могут объяснить разницу const и static, а вопрос про static mut и unsafe идёт следом почти всегда.

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

Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.

  1. #rs_types_vars1 / 5
    Что произойдёт при переполнении i32 в debug-сборке и в release-сборке?
    A)И там, и там неопределённое поведение, как в C
    B)И там, и там паника: Rust не допускает переполнения
    C)Debug — паника, release — арифметика по модулю (wrapping)
    D)Debug — предупреждение в stderr, release — насыщение до i32::MAX
    показать ответ и разбор
    +C)Debug — паника, release — арифметика по модулю (wrapping)

    // разбор: Переполнение целых в Rust определено и не является UB. По умолчанию debug собирается с проверками (overflow-checks), и при переполнении поток паникует; в release проверки выключены и результат оборачивается по модулю. Обе стратегии предсказуемы, а если поведение важно — берут явные wrapping_*, checked_*, saturating_*.

  2. #rs_types_vars2 / 5
    Что вернёт 300i32 as u8 и почему такой каст компилируется без ошибок?
    A)255 — as насыщает значение до границы целевого типа
    B)44 — as отбрасывает старшие биты
    C)Ошибку компиляции: значение не влезает в u8
    D)Панику в рантайме: as проверяет диапазон и падает
    показать ответ и разбор
    +B)44 — as отбрасывает старшие биты

    // разбор: as — приведение без проверок: у целых оно просто берёт младшие биты, поэтому 300 (0b1_0010_1100) превращается в 44. Компилятор не мешает, потому что программист попросил каст явно. Когда усечение недопустимо, берут TryFrom/try_into и обрабатывают Err, а для float→int as с 1.45 насыщает.

  3. #rs_types_vars3 / 5
    Чем const отличается от static в Rust?
    A)const считается в рантайме, static — на этапе компиляции
    B)Разницы нет, static — устаревший синоним const
    C)const виден только в модуле, static — всюду по крейту
    D)const подставляется значением, static — ячейка с адресом
    показать ответ и разбор
    +D)const подставляется значением, static — ячейка с адресом

    // разбор: const — это значение, которое подставляется в каждую точку использования: своего адреса у него нет, дублирование нормально. static — единственный объект с фиксированным адресом и лайфтаймом 'static, поэтому на него можно взять ссылку. Мутабельный static требует unsafe: к нему нет синхронизации, гонка была бы UB.

  4. #rs_types_vars4 / 5
    Что вернёт эта функция?
    fn f(n: i32) -> i32 {
        let x = {
            let y = n * 2;
            y + 1;
        };
        n
    }
    A)Ничего: код не компилируется — блок с ; даёт (), а x объявлен без типа
    B)n * 2 + 1 — точка с запятой в конце блока роли не играет
    C)n — блок вернул (), x получил единичный тип, это валидно
    D)Ничего: не компилируется, потому что y не используется дальше
    показать ответ и разбор
    +C)n — блок вернул (), x получил единичный тип, это валидно

    // разбор: Точка с запятой отбрасывает значение выражения, поэтому блок возвращает () и x: () — это законно, тип выводится. Функция же возвращает n. Грабля в том, что ошибка вылезет не здесь, а там, где x попробуют использовать как число: компилятор скажет, что ожидал i32, а нашёл ().

  5. #rs_types_vars5 / 5
    Почему Option<Box<u8>> занимает те же 8 байт, что и голый Box, а Option<i32> — 8 байт против 4 у i32?
    A)Box — жирный указатель на 16 байт, Option влезает в него
    B)У Box есть невозможное значение (null) — тег прячется в него, у i32 нет
    C)Option<i32> хранит тег в отдельном байте, а выравнивание раздувает до 8
    D)Компилятор упаковывает Option<Box<T>> в машинное слово через сжатие указателя
    показать ответ и разбор
    +B)У Box есть невозможное значение (null) — тег прячется в него, у i32 нет

    // разбор: Это niche-оптимизация: если у типа есть заведомо невалидное битовое состояние (у Box и &T это ноль), компилятор кодирует None этим значением, и Option<Box<u8>> занимает те же 8 байт. У i32 валидны все 2^32 значения, свободной ниши нет — приходится добавлять дискриминант, а выравнивание по 4 округляет размер до 8.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.