Якщо нам уже доводилося користуватися іншою мовою програмування (C/C++, Python, Java, Ruby, Lisp тощо), то, найімовірніше, ми зустрічали null або nil. За допомогою null чи nil ці мови позначають, що конкретна змінна не має значення. Однак через це легко (і часто) припуститися помилки, випадково використавши змінну, що вказує на null. Як неважко уявити, спроба викликати функцію, якої немає, або отримати значення, якого не існує, може призвести до найрізноманітніших помилок і збоїв. Творець null навіть назвав його своєю «помилкою на мільярд доларів».
Option
Щоб уникнути цих проблем, Rust не використовує нульові посилання. Однак йому все ще потрібен безпечний спосіб позначити, що конкретна змінна не має значення. Саме для цього існує Option. Замість змінної, якій бракує значення, змінні Rust можуть використовувати перелік Option. Цей перелік має два варіанти: None, аналог null у Rust, і Some(T), де T - значення будь-якого типу.
Він має такий вигляд:
enum Option<T> {
None,
Some(T),
}
Option можна уявити як шар безпеки між нами і проблемами, які здатні спричинити нульові посилання, і водночас він зберігає їхню концептуальну корисність.
Option
Присвоїти змінній None досить просто:
let nothing: Option<u32> = None; // Variable nothing is set to None
Однак, якщо ми хочемо, щоб тип Option ніс у собі значення, напряму присвоїти це значення не вдасться. Змінна типу Option і, скажімо, змінна типу i32 не рівнозначні. Доведеться скористатися Some:
let wrong_way: Option<i32> = -4; // This will not work
let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>
Із цієї ж причини не спрацює й таке:
let number = 47;
let option_number = Some(15);
let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types
Якщо ми хочемо дістати значення, яке містить Some, спершу потрібно перевірити, чи воно існує:
let mut some_words = Some("choose something to say"); // some_words set to something
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"
some_words = None; // some_words now set to None
// exactly the same match block as above
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"
Крім match, у Rust є й інші інструменти для перевірки та доступу до значень, що містяться в Option, але match уже має бути нам знайомий.
До того ж, нехай це буде демонстрацією того, чому Rust використовує Option, а не нульове посилання. Річ у тім, що ми мусимо перевірити, чи є змінна Option значенням Some (і тоді можна сміливо видобути й використати значення, яке вона містить), чи None. Будь-що інше, і програма не скомпілюється; компілятор оберігає нас від null.
Ми працюємо над рольовою грою. Персонаж гравця представлений так:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
Гравці в цій грі повинні досягти 10 рівня, перш ніж відкрити запас мани, щоб почати застосовувати закляття. До цього моменту мана гравця має значення None.
Ми працюємо над двома частинами функціональності цієї гри: механікою відродження та механікою застосування заклять.
Метод revive повинен перевірити, що гравець справді мертвий (його здоровʼя досягло 0), і якщо це так, метод повинен повернути новий екземпляр Player зі здоровʼям 100.
Якщо рівень гравця 10 або вищий, його також слід відродити зі 100 мани.
Якщо рівень гравця нижчий за 10, його мана має бути None. Метод revive повинен зберегти рівень гравця.
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
Якщо метод revive викликати для гравця, чиє здоровʼя 1 або більше, метод повинен повернути None.
let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None
Метод cast_spell приймає змінне посилання на Player, а також параметр mana_cost, який указує, скільки мани коштує закляття. Він повертає обсяг шкоди, якої завдає застосоване закляття. Якщо закляття успішно застосовано, ця шкода завжди буде вдвічі більшою за витрати мани на закляття.
Якщо гравець не має доступу до запасу мани, спроба застосувати закляття повинна зменшити здоровʼя гравця на вартість закляття в мані. Повернена шкода має бути 0.
let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
assert_eq!(not_a_wizard_yet.health, 74);
assert_eq!(not_a_wizard_yet.mana, None);
Якщо гравець має запас мани, але мани недостатньо, метод не повинен впливати на запас, а натомість повернути 0.
let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
assert_eq!(low_mana_wizard.cast_spell(10), 0);
assert_eq!(low_mana_wizard.health, 93);
assert_eq!(low_mana_wizard.mana, Some(3));
В іншому разі mana_cost слід відняти від запасу мани гравця, і повернути відповідну кількість шкоди.
let wizard = Player { health: 123, mana: Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Some(20));
Гарного кодування!