В Rust отсутствие значения и ошибка обычно не прячутся в null или исключение. Они выражаются через типы: Option<T> для значения, которое может отсутствовать, и Result<T, E> для операции, которая может завершиться ошибкой
В этом уроке мы напишем маленький пример: найдем элемент в списке через Option, разберем ввод числа через Result, обработаем оба результата через match и поймем, почему unwrap не должен быть первой привычкой
- Что получится в конце
- Option: значение есть или его нет
- match: разобрать все варианты
- Result: успех или ошибка
- unwrap и expect: когда можно, а когда не стоит
- if let: когда нужен один интересный вариант
- Мини-практика: безопасный бонус
- map и unwrap_or для коротких случаев
- Result в main
- Как проектировать функцию с Option или Result
- Разбор ошибки без паники
- Частые ошибки и порядок проверки
- Что может быть еще интересно по этой теме
- Что почитать дальше по Rust
Что получится в конце
Создайте проект:
cargo new rust_result_option
cd rust_result_option
Код src/main.rs:
fn find_score(name: &str) -> Option<i32> {
if name == "Dinar" {
Some(95)
} else {
None
}
}
fn parse_points(raw: &str) -> Result<i32, std::num::ParseIntError> {
raw.parse::<i32>()
}
fn main() {
match find_score("Dinar") {
Some(score) => println!("Score: {score}"),
None => println!("Score not found"),
}
match parse_points("42") {
Ok(points) => println!("Points: {points}"),
Err(error) => println!("Parse error: {error}"),
}
}
Запуск:
cargo run
Ожидаемый вывод:
Score: 95
Points: 42
Option: значение есть или его нет
Option<T> означает: внутри либо Some(value), либо None. Например, Option<i32> может хранить Some(95) или None
Функция:
fn find_score(name: &str) -> Option<i32>
честно говорит сигнатурой: оценка может быть найдена, а может отсутствовать. Вызывающий код обязан это обработать
В языках с null ошибка часто появляется позже: кто-то ожидал значение, получил null и упал в другом месте. Rust заставляет решить вопрос рядом с местом, где значение используется
match: разобрать все варианты
match проверяет значение по вариантам:
match find_score("Alex") {
Some(score) => println!("Score: {score}"),
None => println!("Score not found"),
}
Важная особенность: Rust хочет, чтобы match был исчерпывающим. Если забыть None, компилятор напомнит, что вы не обработали все варианты
Это особенно полезно на enum-типах. Когда программа растет, новые состояния не теряются молча. Компилятор помогает найти места, где нужно дописать обработку
Result: успех или ошибка
Result<T, E> означает: либо Ok(value), либо Err(error). В примере parse::<i32>() пытается разобрать строку как число
match "42".parse::<i32>() {
Ok(value) => println!("{value}"),
Err(error) => println!("{error}"),
}
Если строка "42", получим Ok(42). Если строка "abc", получим ошибку разбора
Главное отличие от исключений: ошибка видна в типе. Функция не делает вид, что всегда возвращает число. Она возвращает результат операции, и вызывающий код принимает решение
unwrap и expect: когда можно, а когда не стоит
unwrap достает значение из Some или Ok, но падает, если там None или Err:
let points = "42".parse::<i32>().unwrap();
Для одноразового эксперимента это допустимо. Для учебного материала, который должен учить надежному коду, лучше сначала показать match
expect похож на unwrap, но позволяет написать сообщение:
let points = "42".parse::<i32>().expect("number expected");
Это полезнее при отладке, но все равно аварийный путь. Если ошибка ожидаема, обрабатывайте ее через match, if let, ? или возвращайте Result из функции
if let: когда нужен один интересный вариант
Иногда полный match выглядит слишком длинно:
if let Some(score) = find_score("Dinar") {
println!("Score: {score}");
}
if let удобен, когда вас интересует только один вариант, а остальные можно спокойно пропустить
Но если для None нужно показать сообщение или выполнить отдельную ветку, полный match читается честнее:
match find_score("Alex") {
Some(score) => println!("Score: {score}"),
None => println!("No score for this user"),
}
Мини-практика: безопасный бонус
Сделаем функцию, которая принимает строку с баллами и добавляет бонус:
fn add_bonus(raw: &str) -> Result<i32, std::num::ParseIntError> {
let points = raw.parse::<i32>()?;
Ok(points + 10)
}
fn main() {
match add_bonus("30") {
Ok(total) => println!("Total: {total}"),
Err(error) => println!("Invalid points: {error}"),
}
}
Оператор ? возвращает ошибку наружу, если parse дал Err. Если все хорошо, он достает значение из Ok
Это не магия и не скрытое исключение. Функция add_bonus сама возвращает Result, поэтому может передать ошибку вызывающему коду
map и unwrap_or для коротких случаев
Полный match хорош для обучения, но Rust дает и более компактные методы. Например, если нужно преобразовать значение внутри Option, используйте map:
let score = Some(95);
let label = score.map(|value| format!("Score: {value}"));
println!("{label:?}");
Если значения нет, можно дать запасной вариант:
let score: Option<i32> = None;
let value = score.unwrap_or(0);
println!("{value}");
unwrap_or не падает, потому что вы явно указываете значение по умолчанию. Это безопаснее, чем unwrap, если дефолт действительно имеет смысл в задаче
Но не спешите заменять каждый match цепочкой методов. В учебном коде match часто лучше показывает смысл. Методы вроде map, and_then, unwrap_or приходят позже, когда базовая модель уже понятна
Result в main
Rust позволяет сделать main, который возвращает Result. Это удобно, когда внутри много операций, которые могут ошибиться:
fn main() -> Result<(), std::num::ParseIntError> {
let points = "42".parse::<i32>()?;
println!("Points: {points}");
Ok(())
}
Здесь ? может вернуть ошибку прямо из main. Для маленьких учебных программ это хороший мост между простым match и более реальной обработкой ошибок
Но если вы пишете статью для новичка, сначала покажите явный match. Тогда человек понимает, что ? не исчезает ошибку, а передает ее вызывающему уровню
Как проектировать функцию с Option или Result
Задайте себе вопрос: отсутствие значения — это нормальный исход или ошибка? Если пользователь может не иметь оценки, это Option. Если строку не удалось разобрать как число, это Result
Пример с Option:
fn first_item(items: &[String]) -> Option<&String> {
items.first()
}
Пустой список — нормальная ситуация, не авария
Пример с Result:
fn read_points(raw: &str) -> Result<i32, std::num::ParseIntError> {
raw.parse::<i32>()
}
Ошибка разбора несет причину, поэтому лучше вернуть Result
Эта граница делает код понятнее. Option отвечает на вопрос «есть или нет», Result отвечает на вопрос «получилось или почему не получилось»
Разбор ошибки без паники
Попробуйте заменить "42" на "forty two". Программа с match не упадет, а выведет сообщение об ошибке. Это и есть нормальный путь для пользовательского ввода: не доверять строке заранее
В CLI, API и чтении файлов ошибка ввода является обычной частью программы. Поэтому Rust подталкивает к тому, чтобы обработка была видна в типах и в коде рядом с точкой риска
Частые ошибки и порядок проверки
Слишком рано используете unwrap Если ошибка ожидаема, не падайте через unwrap. Разберите Result через match или верните ошибку наружу
Забыли обработать None Option существует именно потому, что значения может не быть. Не обходите эту мысль, обработайте оба варианта
Не понимаете тип ошибки Посмотрите сигнатуру функции или сообщение компилятора. parse::<i32>() возвращает ошибку типа ParseIntError
Путаете Option и Result Option — значение есть или нет. Result — операция успешна или завершилась ошибкой
Что может быть еще интересно по этой теме
Почему Rust не использует null? Потому что отсутствие значения должно быть видно в типе. Это заставляет обработать ситуацию до запуска, а не ловить падение позже
Когда ? лучше match? Когда функция сама возвращает Result и вы не хотите обрабатывать ошибку на месте. ? делает код короче, но не отменяет тип ошибки
Можно ли использовать unwrap в тестах? В тестах и коротких проверках иногда можно. В пользовательском коде лучше писать понятную обработку или expect с объяснением
Что дальше после Result? Коллекции. Vec, String и HashMap постоянно возвращают Option и работают с ownership, поэтому темы хорошо сцепляются
Что почитать дальше по Rust
- Ownership и borrowing в Rust простыми словами
- Vec, String и HashMap в Rust
- Struct, enum и impl в Rust
- CLI на Rust: мини-проект с аргументами командной строки



