Відсутність і помилка як тип
VTFK · 2026
Фокус: компілятор не дає забути про порожній чи хибний випадок
Option<T> (Rust)Дістати значення безпечно: match або комбінатори (map, unwrap_or, and_then)
enum Option<T> {
Some(T),
None,
}
let some_number: Option<i32> = Some(3101); // 0xC1D
let absent_number: Option<i32> = None;Option<T> — частина стандартної бібліотеки, є завжди в prelude
fn describe(x: Option<i32>) -> String {
match x {
Some(n) if n < 0 => format!("від'ємне: {}", n),
Some(n) => format!("значення: {}", n),
None => String::from("значення відсутнє"),
}
}Прибрати гілку None — і код взагалі не скомпілюється: error[E0004] non-exhaustive
Очікувано: Без гілки None компіляція падає з точною вказівкою відсутнього варіанта; з гілкою — успішна збірка
Альтернатива — прапорці is_connected + is_connecting + error: дозволяє «з'єднано» і «помилка» водночас. Option — окремий випадок цього прийому (СРС 12)
enum Result<T, E> {
Ok(T),
Err(E),
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("ділення на нуль"))
} else {
Ok(a / b)
}
}Тип помилки E — це звичайний тип, часто власний enum або String
Result<T, E> або Option<T>Замінює ланцюжки match { Ok(v) => v, Err(e) => return Err(e) } на кожен виклик
Ok(v) — значення підставляється й виконання йде далі; Err(e) — From::from(e) конвертує тип помилки і функція негайно повертає Err. Тому потрібен impl From<E> for E2
use std::fs::File;
use std::io::{self, Read};
fn read_username() -> Result<String, io::Error> {
let mut f = File::open("user.txt")?; // якщо Err — функція одразу повертає його
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}Без ? довелося б писати два ручні match — по одному на кожен виклик
| Спосіб | Що робить | Коли доречно |
|---|---|---|
match | усі варіанти явно, вимога повноти | складна логіка, різні дії на варіант |
? | поширює Err/None нагору | функція сама повертає Result/Option |
.map(f) | перетворити всередині Some/Ok | не займаючись None/Err |
.unwrap_or(d) | значення або типове d | є розумне значення за замовчуванням |
.and_then(f) | ланцюжок, кожен крок дає Option/Result | послідовні операції, що можуть провалитись |
.unwrap() / .expect("…") | значення або panic! | тести, прототипи, провал неможливий за контрактом |
У коді, що обробляє зовнішні дані (файли, мережа, ввід), unwrap() майже завжди програє match або ?
panic = "abort" у Cargo.toml дає менший бінарник, але без коректного Drop
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput { field: String, reason: String },
Io(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for AppError {}enum описує рівно ті варіанти провалу, які реально можливі — жодного зайвого
From<E1> for E2Без impl From<E1> for E2 код із ? між різними типами помилок не скомпілюється
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::Io(e)
}
}
fn read_config() -> Result<String, AppError> {
let content = std::fs::read_to_string("config.toml")?; // io::Error → AppError
Ok(content)
}? конвертує помилку неявно в момент застосування, якщо From реалізовано
| Ознака | thiserror | anyhow |
|---|---|---|
| Де застосовують | бібліотеки — крейти, які імпортують інші | бінарники, main.rs, застосунки |
| Тип помилки | власний типізований enum | універсальний anyhow::Error |
| Споживач розрізняє варіант? | так, через match | зазвичай ні — повідомлення й контекст |
| Головна можливість | #[error("…")], #[from] | .context("…") — ланцюжок причин |
| Що генерує | Display і std::error::Error | обгортку для будь-якої Error |
Один проєкт часто поєднує обидва: thiserror у внутрішніх крейтах, anyhow у main.rs
// бібліотека: thiserror
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("не знайдено: {0}")]
NotFound(String),
#[error("помилка вводу-виводу")]
Io(#[from] std::io::Error),
}
// застосунок: anyhow
fn main() -> anyhow::Result<()> {
let text = std::fs::read_to_string("config.toml")
.context("не вдалося прочитати config.toml")?;
Ok(())
}Один і той самий проєкт часто поєднує обидва: thiserror у внутрішніх крейтах (crate), anyhow у main.rs
fn first_char_upper(s: &str) -> Option<char> {
let c = s.chars().next()?; // якщо None — функція одразу повертає None
c.to_uppercase().next() // Option<char>: перший символ верхнього регістру
}
assert_eq!(first_char_upper("привіт"), Some('П'));
assert_eq!(first_char_upper(""), None);Без ? довелося б писати match Some/None вручну на кожному кроці
Я називаю це своєю помилкою на мільярд доларів — винаходом null-посилання в 1965 році.
— Тоні Гоар, «Null References: The Billion Dollar Mistake» (QCon, 2009)
🥚 У прикладі Option значення дорівнює Some(3101) — 3101 це 0xC1D, константа, що супроводжує курс від першого модуля. None, як і належить, значення не має.