Типы, 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, остальные разбираются в тренажёре.
- Что произойдёт при переполнении i32 в debug-сборке и в release-сборке?A)И там, и там неопределённое поведение, как в CB)И там, и там паника: Rust не допускает переполненияC)Debug — паника, release — арифметика по модулю (wrapping)D)Debug — предупреждение в stderr, release — насыщение до i32::MAX
показать ответ и разбор
+C)Debug — паника, release — арифметика по модулю (wrapping)// разбор: Переполнение целых в Rust определено и не является UB. По умолчанию debug собирается с проверками (overflow-checks), и при переполнении поток паникует; в release проверки выключены и результат оборачивается по модулю. Обе стратегии предсказуемы, а если поведение важно — берут явные wrapping_*, checked_*, saturating_*.
- Что вернёт 300i32 as u8 и почему такой каст компилируется без ошибок?A)255 — as насыщает значение до границы целевого типаB)44 — as отбрасывает старшие битыC)Ошибку компиляции: значение не влезает в u8D)Панику в рантайме: as проверяет диапазон и падает
показать ответ и разбор
+B)44 — as отбрасывает старшие биты// разбор: as — приведение без проверок: у целых оно просто берёт младшие биты, поэтому 300 (0b1_0010_1100) превращается в 44. Компилятор не мешает, потому что программист попросил каст явно. Когда усечение недопустимо, берут TryFrom/try_into и обрабатывают Err, а для float→int as с 1.45 насыщает.
- Чем const отличается от static в Rust?A)const считается в рантайме, static — на этапе компиляцииB)Разницы нет, static — устаревший синоним constC)const виден только в модуле, static — всюду по крейтуD)const подставляется значением, static — ячейка с адресом
показать ответ и разбор
+D)const подставляется значением, static — ячейка с адресом// разбор: const — это значение, которое подставляется в каждую точку использования: своего адреса у него нет, дублирование нормально. static — единственный объект с фиксированным адресом и лайфтаймом 'static, поэтому на него можно взять ссылку. Мутабельный static требует unsafe: к нему нет синхронизации, гонка была бы UB.
- Что вернёт эта функция?
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, а нашёл ().
- Почему Option<Box<u8>> занимает те же 8 байт, что и голый Box, а Option<i32> — 8 байт против 4 у i32?A)Box — жирный указатель на 16 байт, Option влезает в негоB)У Box есть невозможное значение (null) — тег прячется в него, у i32 нетC)Option<i32> хранит тег в отдельном байте, а выравнивание раздувает до 8D)Компилятор упаковывает Option<Box<T>> в машинное слово через сжатие указателя
показать ответ и разбор
+B)У Box есть невозможное значение (null) — тег прячется в него, у i32 нет// разбор: Это niche-оптимизация: если у типа есть заведомо невалидное битовое состояние (у Box и &T это ноль), компилятор кодирует None этим значением, и Option<Box<u8>> занимает те же 8 байт. У i32 валидны все 2^32 значения, свободной ниши нет — приходится добавлять дискриминант, а выравнивание по 4 округляет размер до 8.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.