Курс ЛР6 · Місія 6
Академічна↔ Сюжет С.І.Д.СРС до ЛР⌨️ команди 3

Лабораторна робота № 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. Компілятор не дозволяє "просто прочитати" значення без обробки випадку відсутності.

Обладнання та програмне забезпечення

Порядок виконання

  1. Відкрити стартовий каркас, ознайомитися з описом предметної області в starter/README.md.
  2. Визначити enum NodeState з варіантами, що покривають усі реальні стани охоронного вузла (наприклад: неактивний, активний з рівнем тривоги, заблокований з кодом). Зібрати проєкт.
  3. Написати struct GuardNode, що містить ідентифікатор і поле типу NodeState.
  4. Реалізувати функцію виведення стану без null/Option для базових полів — переконатися, що cargo build проходить і cargo test для базових тестів проходить.
  5. (Рівень 2) У блоці impl GuardNode реалізувати методи безпечного переходу між станами (наприклад, fn escalate(&mut self)), що коректно обробляють усі варіанти через match.
  6. (Рівень 3) Додати вкладену структуру (наприклад, показники сенсора) типу Option<Sensor> усередині GuardNode і реалізувати функцію, яка безпечно дістає значення з кількох рівнів вкладеності Option, не використовуючи unwrap() без перевірки.

Завдання за рівнями

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Рівень 1 (оцінка 3)

Описати предметну область (охоронні вузли) через struct та enum з мінімум трьома варіантами стану, реалізувати fn describe(node: &GuardNode) -> String через вичерпний match, вивести стан без null/Option-заглушок для базових полів. Написати юніт-тест describe_reports_state, що перевіряє коректний опис для кожного варіанта стану.

РІВЕНЬ 2 · «4» · МАГІСТР

Рівень 2 (оцінка 4, захист)

Написати логіку взаємодії (impl GuardNode) для безпечного перемикання станів охоронної системи: мінімум два методи, що змінюють self.state, кожен обробляє всі варіанти вхідного стану через match, без unreachable!() як основної логіки. На захисті — усно обґрунтувати набір станів і кожен перехід.

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

Рівень 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
❓ КОНТРОЛЬНІ ПИТАННЯ

Контрольні питання

  1. Чим struct (product type) принципово відрізняється від enum (sum type)?
  2. Що означає вичерпність (exhaustiveness) конструкції match і як компілятор її перевіряє?
  3. Наведіть приклад, коли поле-прапорець типу bool разом із Option-полем дозволяє створити логічно некоректний стан — і як enum усуває цю можливість.
  4. Що виведе компілятор, якщо в match над enum з трьома варіантами обробити лише два й не додати _ => ...?
  5. Як ? працює з Option<T> усередині функції, що сама повертає Option?
  6. Чим if let Some(x) = opt { ... } відрізняється від повного match?
  7. Чому патерн "зробити некоректний стан непредставним" зменшує кількість потрібних тестів?
  8. Наведіть приклад enum-варіанта з даними (наприклад, Locked(String)) і поясніть, які дані він несе і чому саме ці.

Парні теми СРС

Критерії оцінювання та форма звіту

Студент здає лабораторну тегом submit/lab06. Автотести рівня 1 (autograder/) перевіряють: проєкт збирається, enum NodeState містить принаймні три різні варіанти (перевірка через компіляцію тестового коду, що зіставляє зі зразком усі три), базові юніт-тести на створення й виведення стану вузла проходять.

Для рівня 2 автотести перевіряють, що методи impl GuardNode коректно обробляють переходи між усіма варіантами NodeState (тестуються через послідовні виклики й перевірку кінцевого стану). Захист рівня 2 — усне пояснення, чому обраний саме такий набір станів і чому їх достатньо для опису предметної області.

Рівень 3 оцінюється вручну: перевіряється наявність вкладеного Option, відсутність панічних unwrap() без обробки в основній логіці (лише в тестах допустимо), і коректність обробки випадку "дані відсутні на будь-якому з рівнів вкладеності".

Форма звіту: короткий опис реалізації (3–5 речень) з обґрунтуванням вибору варіантів NodeState, скріншот терміналу із зеленим cargo test, для рівня 2 — опис кожного методу переходу, для рівня 3 — фрагмент коду з обробкою вкладеного Option і коментарем, який граничний випадок він покриває.

⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 3

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.