Типы, match, Option и Result в Rust

В Rust отсутствие значения и ошибка обычно не прячутся в null или исключение. Они выражаются через типы: Option<T> для значения, которое может отсутствовать, и Result<T, E> для операции, которая может завершиться ошибкой

В этом уроке мы напишем маленький пример: найдем элемент в списке через Option, разберем ввод числа через Result, обработаем оба результата через match и поймем, почему unwrap не должен быть первой привычкой

Что получится в конце

Создайте проект:

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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x