сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Трейты и дженерики

Структуры и enum в Rust

Структуры и enum

Моделирование данных - то, за что 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, остальные разбираются в тренажёре.

  1. #rs_structs_enums1 / 5
    Что делает #[derive(Debug, Clone, PartialEq)] над структурой?
    A)Помечает структуру как наследника трёх базовых классов
    B)Регистрирует структуру в таблице типов для рефлексии
    C)Включает проверки во время выполнения при печати и сравнении
    D)Генерирует реализации этих трейтов на этапе компиляции
    показать ответ и разбор
    +D)Генерирует реализации этих трейтов на этапе компиляции

    // разбор: derive — процедурный макрос: он раскрывает структуру в готовый impl-блок ещё до компиляции тела. Требование одно — все поля тоже должны реализовывать эти трейты, иначе сгенерированный код не соберётся. Рефлексии и наследования в Rust нет, а Debug нужен для {:?} и полезен в тестах и логах.

  2. #rs_structs_enums2 / 5
    Сколько места займёт 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 таскает лишние байты.

  3. #rs_structs_enums3 / 5
    Зачем библиотеке помечать публичный enum как #[non_exhaustive]?
    A)Чтобы добавление варианта не ломало код пользователей по семверу
    B)Чтобы запретить создание значений этого enum вне крейта
    C)Чтобы значения enum можно было сериализовать в JSON без потерь
    D)Чтобы компилятор не проверял исчерпывающий разбор внутри крейта
    показать ответ и разбор
    +A)Чтобы добавление варианта не ломало код пользователей по семверу

    // разбор: Без атрибута любой match у пользователя перечисляет все варианты — и новый вариант в минорной версии ломает сборку. non_exhaustive обязывает внешний код держать ветку _, поэтому расширение перечисления перестаёт быть ломающим изменением. Внутри своего крейта проверка остаётся полной.

  4. #rs_structs_enums4 / 5
    Когда состояния лучше моделировать отдельными типами, а не одним enum со статусом?
    A)Когда состояния меняются часто: смена типа дешевле смены поля
    B)Когда у состояний разный набор операций и это проверяет компилятор
    C)Когда состояния хранятся в базе и нужна сериализация
    D)Когда состояний больше трёх: match становится нечитаемым
    показать ответ и разбор
    +B)Когда у состояний разный набор операций и это проверяет компилятор

    // разбор: Приём называют typestate: Connection<Open> и Connection<Closed> — разные типы, и метод send() существует только у первого. Ошибка «отправил в закрытое соединение» становится ошибкой компиляции, а не веткой с паникой. Плата — сложность сигнатур, поэтому так делают на критичных переходах, а не везде подряд.

  5. #rs_structs_enums5 / 5
    Когда берут кортежную структуру struct Meters(f64), а когда обычную с именованными полями?
    A)Кортежную — когда структура публичная, обычную — когда она приватная для крейта
    B)Кортежную — для чисел, обычную — для строк и коллекций внутри
    C)Кортежную — когда поле одно и его роль ясна из имени типа, обычную — когда полей несколько и им нужны имена
    D)Кортежную — когда нужен Copy, обычную — когда тип владеет данными в куче
    показать ответ и разбор
    +C)Кортежную — когда поле одно и его роль ясна из имени типа, обычную — когда полей несколько и им нужны имена

    // разбор: Кортежная форма — это обёртка: одно поле, смысл несёт имя типа (Meters, UserId, Wrapper). Как только полей становится два-три и по позиции их уже не различить, берут именованные — иначе на месте вызова получается загадка вроде P(1.0, 2.0, 3.0). Раскладка у обеих форм одинаковая, выбор чисто про читаемость.

дальше

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

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