Лабораторна робота № 6. Структури і перелічення без null
Мета
Навчитися моделювати предметну область через struct та enum, писати методи (impl),
використовувати вичерпне зіставлення зі зразком (match) і застосовувати Option у вкладених
структурах так, щоб некоректний або відсутній стан був неможливий на рівні типів.
Теоретичні відомості
Struct описує іменований набір полів (product type): усі поля присутні одночасно, кожен екземпляр — це кортеж усіх полів разом. Enum описує один із заздалегідь відомого набору варіантів (sum type), причому кожен варіант може нести власні дані:
enum GuardState { Idle, Alert { level: u8 }, Locked(String) }
На відміну від enum у C, варіанти Rust-enum можуть мати різну "форму" даних, а компілятор
гарантує, що доступ до цих даних можливий лише через явне зіставлення зі зразком — неможливо
випадково прочитати поле level, коли фактичний варіант — Locked.
match — конструкція зіставлення зі зразком, яка зобов'язана покрити всі можливі варіанти
enum (вичерпність, exhaustiveness); якщо додати новий варіант в enum і забути обробити його в
одному з match, компілятор видасть помилку компіляції в кожному такому місці. Це практичне
застосування алгебраїчних типів даних: неможливо "забути" гілку, як це легко зробити з if/else
чи switch без default в інших мовах — помилка виявляється на етапі збірки, а не в проді.
Патерн «зробити некоректний стан непредставним» (make invalid states unrepresentable)
означає: замість структури з набором незалежних необов'язкових полів і коментарем "ці поля
мають сенс лише коли статус X" — використовувати enum, варіанти якого несуть рівно ті дані, які
дійсно існують у цьому стані. Наприклад, замість struct Node { locked: bool, key: Option<String> } (де теоретично можливий безглуздий стан locked: false, key: Some(...)) — enum NodeState { Unlocked, Locked(String) }, де такий стан фізично неможливо створити. Вигода не лише
стилістична: кількість комбінацій полів у першому варіанті росте як добуток можливих значень
кожного поля, тоді як у другому — рівно стільки, скільки варіантів enum, і жодної зайвої.
Option<T> у вкладених структурах (наприклад, struct Sensor { reading: Option<f64> }
усередині struct Node { sensor: Option<Sensor> }) моделює багаторівневу відсутність даних без
null: щоб дістатися значення, потрібно послідовно обробити кожен рівень Option — оператором
? усередині функції, що сама повертає Option, комбінатором and_then/map, або вкладеним
match. Компілятор не дозволяє "просто прочитати" значення без обробки випадку відсутності.
Обладнання та програмне забезпечення
- Codespaces / devcontainer курсу з Rust toolchain
- Наданий стартовий каркас
starter/з описом предметної області "охоронні вузли ТехНови"
Порядок виконання
- Відкрити стартовий каркас, ознайомитися з описом предметної області в
starter/README.md. - Визначити
enum NodeStateз варіантами, що покривають усі реальні стани охоронного вузла (наприклад: неактивний, активний з рівнем тривоги, заблокований з кодом). Зібрати проєкт. - Написати
struct GuardNode, що містить ідентифікатор і поле типуNodeState. - Реалізувати функцію виведення стану без
null/Optionдля базових полів — переконатися, щоcargo buildпроходить іcargo testдля базових тестів проходить. - (Рівень 2) У блоці
impl GuardNodeреалізувати методи безпечного переходу між станами (наприклад,fn escalate(&mut self)), що коректно обробляють усі варіанти черезmatch. - (Рівень 3) Додати вкладену структуру (наприклад, показники сенсора) типу
Option<Sensor>усерединіGuardNodeі реалізувати функцію, яка безпечно дістає значення з кількох рівнів вкладеностіOption, не використовуючиunwrap()без перевірки.
Завдання за рівнями
Рівень 1 (оцінка 3)
Описати предметну область (охоронні вузли) через struct та enum з мінімум трьома варіантами
стану, реалізувати fn describe(node: &GuardNode) -> String через вичерпний match, вивести
стан без null/Option-заглушок для базових полів. Написати юніт-тест
describe_reports_state, що перевіряє коректний опис для кожного варіанта стану.
Рівень 2 (оцінка 4, захист)
Написати логіку взаємодії (impl GuardNode) для безпечного перемикання станів охоронної
системи: мінімум два методи, що змінюють self.state, кожен обробляє всі варіанти вхідного
стану через match, без unreachable!() як основної логіки. На захисті — усно обґрунтувати
набір станів і кожен перехід.
Рівень 3 (оцінка 5, розширення)
Реалізувати глибоку обробку відсутніх даних за допомогою патерну Option у вкладених
структурах: додати поле типу Option<SensorReading> (або еквівалент) усередині GuardNode і
функцію, що безпечно дістає й обробляє показник на кількох рівнях вкладеності Option, без
unwrap() у виробничій логіці.
Розібраний приклад
Порівняння "поганого" і "хорошого" дизайну одного домену:
// Погано: незалежні поля дозволяють логічно безглузді комбінації
struct NodeBad {
active: bool,
alert_level: Option<u8>,
locked: bool,
lock_code: Option<String>,
}
// Добре: enum виключає безглузді комбінації на рівні типів
enum NodeState { Idle, Alert(u8), Locked(String) }
struct GuardNode { id: String, state: NodeState }
fn describe(node: &GuardNode) -> String {
match &node.state {
NodeState::Idle => format!("Вузол {} неактивний.", node.id),
NodeState::Alert(level) => format!("Вузол {} у тривозі, рівень {level}.", node.id),
NodeState::Locked(code) => format!("Вузол {} заблокований, код {code}.", node.id),
}
}
У поганому варіанті формально можливий стан active: true, locked: true, alert_level: None, lock_code: None — логічно безглуздий, але компілятор його пропускає. У хорошому варіанті
GuardNode фізично не може одночасно перебувати в двох станах: state — рівно один із трьох
варіантів. Кількість необхідних тестів падає з 2⁴ = 16 потенційних комбінацій полів до рівно
3 варіантів match.
Метод переходу, що обробляє всі варіанти:
impl GuardNode {
fn escalate(&mut self) {
self.state = match &self.state {
NodeState::Idle => NodeState::Alert(10),
NodeState::Alert(level) => NodeState::Alert((*level + 20).min(100)),
NodeState::Locked(code) => NodeState::Locked(code.clone()),
};
}
}
Перевірка:
cargo build --manifest-path enemy_architecture/Cargo.toml
cargo test --manifest-path enemy_architecture/Cargo.toml
Контрольні питання
- Чим
struct(product type) принципово відрізняється відenum(sum type)? - Що означає вичерпність (exhaustiveness) конструкції
matchі як компілятор її перевіряє? - Наведіть приклад, коли поле-прапорець типу
boolразом ізOption-полем дозволяє створити логічно некоректний стан — і як enum усуває цю можливість. - Що виведе компілятор, якщо в
matchнад enum з трьома варіантами обробити лише два й не додати_ => ...? - Як
?працює зOption<T>усередині функції, що сама повертаєOption? - Чим
if let Some(x) = opt { ... }відрізняється від повногоmatch? - Чому патерн "зробити некоректний стан непредставним" зменшує кількість потрібних тестів?
- Наведіть приклад enum-варіанта з даними (наприклад,
Locked(String)) і поясніть, які дані він несе і чому саме ці.
Парні теми СРС
- Рівень 2: srs11 — Алгебраїчні типи даних і вичерпне зіставлення зі зразком (match)
- Рівень 3: srs12 — Патерн «зробити некоректний стан непредставним»
Критерії оцінювання та форма звіту
Студент здає лабораторну тегом submit/lab06. Автотести рівня 1 (autograder/) перевіряють:
проєкт збирається, enum NodeState містить принаймні три різні варіанти (перевірка через
компіляцію тестового коду, що зіставляє зі зразком усі три), базові юніт-тести на створення й
виведення стану вузла проходять.
Для рівня 2 автотести перевіряють, що методи impl GuardNode коректно обробляють переходи між
усіма варіантами NodeState (тестуються через послідовні виклики й перевірку кінцевого стану).
Захист рівня 2 — усне пояснення, чому обраний саме такий набір станів і чому їх достатньо для
опису предметної області.
Рівень 3 оцінюється вручну: перевіряється наявність вкладеного Option, відсутність панічних
unwrap() без обробки в основній логіці (лише в тестах допустимо), і коректність обробки випадку
"дані відсутні на будь-якому з рівнів вкладеності".
Форма звіту: короткий опис реалізації (3–5 речень) з обґрунтуванням вибору варіантів NodeState,
скріншот терміналу із зеленим cargo test, для рівня 2 — опис кожного методу переходу, для
рівня 3 — фрагмент коду з обробкою вкладеного Option і коментарем, який граничний випадок він
покриває.