Структуры и enum в Rust
Моделирование данных - то, за что Rust любят даже те, кто не пишет системный код. Спрашивают, чем enum отличается от перечисления в C, сколько он занимает, зачем non_exhaustive. Проверяют, умеешь ли ты выражать состояния типом, а не полем-статусом с проверками в рантайме.
Стержень: enum - размеченное объединение, где вариант несёт свои данные, а компилятор требует разобрать все случаи.
// Формулировки: «чем enum отличается от enum в Java?», «сколько занимает enum с u64 внутри?», «зачем non_exhaustive?»
Вариант с данными
В C перечисление - просто набор именованных чисел. В Rust каждый вариант может нести свою полезную нагрузку: Quit без данных, Move { x, y } со структурой, Write(String) со строкой. Всё это один тип, и разбирается он через match.
Именно поэтому Option и Result не встроены в язык - они объявлены обычным enum в стандартной библиотеке. Никакой магии компилятора: тот же механизм доступен и вашему коду.
// Отсюда правило моделирования: если у сущности несколько взаимоисключающих форм, это enum, а не структура с кучей Option-полей и молчаливым уговором, какие из них заполнены.
enum Event {
Click { x: i32, y: i32 },
Key(char),
Close,
}- размеченное объединение
- тип, где вариант несёт данные и помечен тегом
- дискриминант
- тег, указывающий активный вариант
Сколько это занимает
Варианты делят одну область памяти, поэтому размер определяет самый крупный плюс дискриминант и выравнивание. У enum с u64 внутри получится 16 байт: восемь под данные, восемь под тег с учётом выравнивания.
Практический вывод: если один вариант заметно толще остальных, весь enum таскает лишние байты на каждом значении - в векторе это заметно. Толстый вариант прячут в Box, и тогда в самом enum остаётся указатель.
// Обратная сторона той же механики - niche-оптимизация: если у типа есть невозможное значение, тег прячется в него. Поэтому Option<Box<T>> занимает те же восемь байт, что и голый Box.
- выравнивание
- требование к адресу, кратному размеру типа
- niche
- невозможное значение типа, которым кодируется вариант
Расширяемость и состояния типами
Публичный enum библиотеки это обещание: любой match у пользователя перечисляет все варианты, и добавление нового ломает сборку. Атрибут #[non_exhaustive] обязывает внешний код держать ветку _, и тогда расширение перестаёт быть мажорным изменением.
Более продвинутый приём - typestate: состояния становятся разными типами. Connection<Open> и Connection<Closed> это два разных типа, и метод send() существует только у первого. Ошибка «отправил в закрытое соединение» превращается из ветки с паникой в ошибку компиляции.
// Typestate стоит сложности сигнатур, поэтому его применяют точечно - на переходах, где ошибка дорого обходится.
- non_exhaustive
- атрибут, разрешающий добавлять варианты без мажорной версии
- typestate
- состояния как отдельные типы вместо поля-статуса
Как отвечать: «Чем enum в Rust отличается от перечислений в других языках?»
Это полноценное размеченное объединение: каждый вариант может нести собственные данные - структуру, строку, вообще ничего. Разбирают его через match, и компилятор требует покрыть все случаи, поэтому забытый вариант становится ошибкой сборки, а не багом в проде. На этом же механизме построены Option и Result: они объявлены обычным enum, без поддержки компилятора. По памяти enum занимает размер самого крупного варианта плюс тег с выравниванием, и если один вариант сильно толще остальных, его обычно прячут в Box.
Ты показываешь и семантику, и цену по памяти, и связь с Option/Result. Три уровня в одном ответе - редко кто даёт больше одного.
На чём валятся
- −Думают, что размер enum - сумма вариантов. Они перекрываются, берётся максимум плюс тег.
- −Моделируют взаимоисключающие состояния структурой с полями Option вместо enum.
- −Забывают non_exhaustive в публичном enum и делают ломающий релиз при добавлении варианта.
- −Ставят ветку _ в match по своему enum и теряют проверку компилятора при расширении.
- −Не знают, что Option и Result - обычные enum, и приписывают им поддержку компилятора.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Что делает #[derive(Debug, Clone, PartialEq)] над структурой?A)Помечает структуру как наследника трёх базовых классовB)Регистрирует структуру в таблице типов для рефлексииC)Включает проверки во время выполнения при печати и сравненииD)Генерирует реализации этих трейтов на этапе компиляции
показать ответ и разбор
+D)Генерирует реализации этих трейтов на этапе компиляции// разбор: derive — процедурный макрос: он раскрывает структуру в готовый impl-блок ещё до компиляции тела. Требование одно — все поля тоже должны реализовывать эти трейты, иначе сгенерированный код не соберётся. Рефлексии и наследования в Rust нет, а Debug нужен для {:?} и полезен в тестах и логах.
- Сколько места займёт enum E { A(u64), B(u8), C }?A)Сумму всех вариантов: 8 + 1 + 0 плюс тегB)8 байт: варианты лежат в одной ячейке, тег хранится отдельноC)Как самый крупный вариант плюс тег и выравнивание — 16 байтD)24 байта: каждый вариант получает своё поле в раскладке
показать ответ и разбор
+C)Как самый крупный вариант плюс тег и выравнивание — 16 байт// разбор: Варианты делят одну область памяти, поэтому размер определяет самый крупный, плюс дискриминант и выравнивание: u64 требует границы в 8, тег занимает следующие 8 — итого 16. Отсюда практика: если один вариант заметно толще остальных, его прячут в Box, иначе весь enum таскает лишние байты.
- Зачем библиотеке помечать публичный enum как #[non_exhaustive]?A)Чтобы добавление варианта не ломало код пользователей по семверуB)Чтобы запретить создание значений этого enum вне крейтаC)Чтобы значения enum можно было сериализовать в JSON без потерьD)Чтобы компилятор не проверял исчерпывающий разбор внутри крейта
показать ответ и разбор
+A)Чтобы добавление варианта не ломало код пользователей по семверу// разбор: Без атрибута любой match у пользователя перечисляет все варианты — и новый вариант в минорной версии ломает сборку. non_exhaustive обязывает внешний код держать ветку _, поэтому расширение перечисления перестаёт быть ломающим изменением. Внутри своего крейта проверка остаётся полной.
- Когда состояния лучше моделировать отдельными типами, а не одним enum со статусом?A)Когда состояния меняются часто: смена типа дешевле смены поляB)Когда у состояний разный набор операций и это проверяет компиляторC)Когда состояния хранятся в базе и нужна сериализацияD)Когда состояний больше трёх: match становится нечитаемым
показать ответ и разбор
+B)Когда у состояний разный набор операций и это проверяет компилятор// разбор: Приём называют typestate: Connection<Open> и Connection<Closed> — разные типы, и метод send() существует только у первого. Ошибка «отправил в закрытое соединение» становится ошибкой компиляции, а не веткой с паникой. Плата — сложность сигнатур, поэтому так делают на критичных переходах, а не везде подряд.
- Когда берут кортежную структуру struct Meters(f64), а когда обычную с именованными полями?A)Кортежную — когда структура публичная, обычную — когда она приватная для крейтаB)Кортежную — для чисел, обычную — для строк и коллекций внутриC)Кортежную — когда поле одно и его роль ясна из имени типа, обычную — когда полей несколько и им нужны именаD)Кортежную — когда нужен Copy, обычную — когда тип владеет данными в куче
показать ответ и разбор
+C)Кортежную — когда поле одно и его роль ясна из имени типа, обычную — когда полей несколько и им нужны имена// разбор: Кортежная форма — это обёртка: одно поле, смысл несёт имя типа (Meters, UserId, Wrapper). Как только полей становится два-три и по позиции их уже не различить, берут именованные — иначе на месте вызова получается загадка вроде P(1.0, 2.0, 3.0). Раскладка у обеих форм одинаковая, выбор чисто про читаемость.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.