Из Option в Result в Rust: туториал без макросов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему вам надоело писать макросы для Option в Result?

Вы пишете на Rust, и вдруг понимаете: у вас есть Option<T>, а нужно Result<T, E>. Казалось бы, мелочь. Но если вы новичок, то первая мысль: «Наверное, нужен макрос!» Или вы уже опытный разработчик, но каждый раз пишете одно и то же: match option { Some(v) => Ok(v), None => Err("error") }. Согласитесь, это утомляет.

Вопрос на Stack Overflow «Is there a way to simplify converting an Option into a Result without a macro?» собрал тысячи просмотров. Это не случайно: проблема знакома каждому, кто работает с Rust. Но хорошая новость в том, что решение есть, и оно удивительно простое. В этом туториале мы разберём все способы, от базовых до элегантных, без единого макроса.

Мы не просто дадим ответ — мы покажем, как думать как Rust-разработчик. Поехали!

Что такое Option и Result в Rust?

Прежде чем конвертировать, давайте убедимся, что мы на одной волне.

  • Option<T> — это тип, который может быть либо Some(T) (значение есть), либо None (значения нет). Используется, когда отсутствие значения — это нормальная ситуация (например, поиск элемента в коллекции).
  • Result<T, E> — это тип, который может быть либо Ok(T) (всё хорошо), либо Err(E) (произошла ошибка). Используется для операций, которые могут завершиться неудачей (например, чтение файла).

Проблема: иногда у вас есть Option, но по логике программы отсутствие значения — это ошибка. Вот тут и нужна конвертация.

Базовый способ: match (но вы это уже знаете)

Самый очевидный и часто используемый новичками способ — это match. Да, он работает, но он многословен:

fn get_user_name(id: u32) -> Option<String> {
    // ... какая-то логика
    if id == 1 { Some("Alice".to_string()) } else { None }
}

fn main() {
    let user_id = 2;
    let name = match get_user_name(user_id) {
        Some(name) => Ok(name),
        None => Err("User not found"),
    };
    println!("{:?}", name); // Err("User not found")
}

Этот код корректен, но если таких конвертаций в проекте десятки, вы устанете писать match. К тому же легко ошибиться и забыть обработать None. Нужно что-то более лаконичное.

Метод ok_or: ваш новый лучший друг

Стандартная библиотека Rust предоставляет метод ok_or для Option. Он делает ровно то, что нам нужно: преобразует Some(v) в Ok(v), а None — в Err(error), где error — это значение, которое вы передаёте.

fn main() {
    let user_id = 2;
    let name = get_user_name(user_id).ok_or("User not found");
    println!("{:?}", name); // Err("User not found")
}

Всего одна строка! Но есть нюанс: ok_or принимает значение по владению (owned value). Если вы передаёте строковый литерал, как в примере, это работает, потому что &str реализует Into<String>. Но если вам нужно передать ошибку, которая уже где-то хранится, это может привести к ненужному копированию.

Для таких случаев есть ok_or_else — он принимает замыкание, которое вызывается только если Option равен None. Это экономит ресурсы:

fn main() {
    let user_id = 2;
    let error_msg = format!("User with id {} not found", user_id);
    let name = get_user_name(user_id).ok_or_else(|| error_msg.clone());
    // или ещё лучше:
    let name = get_user_name(user_id).ok_or_else(|| format!("User {} not found", user_id));
    println!("{:?}", name);
}

Совет: используйте ok_or, если сообщение об ошибке — это константа. Используйте ok_or_else, если сообщение нужно вычислить динамически.

Что делать, если Option пустой, а ошибка сложная?

Иногда вам нужно не просто строку вернуть, а структурированную ошибку. Например, вы используете библиотеку anyhow или thiserror. Метод ok_or всё равно подходит:

use anyhow::{anyhow, Result};

fn find_user(id: u32) -> Option<String> {
    // ...
    None
}

fn main() -> Result<()> {
    let name = find_user(42).ok_or_else(|| anyhow!("User not found"))?;
    println!("{}", name);
    Ok(())
}

Или с собственной ошибкой:

#[derive(Debug)]
enum MyError {
    NotFound,
    PermissionDenied,
}

fn main() {
    let value: Option<i32> = None;
    let result: Result<i32, MyError> = value.ok_or(MyError::NotFound);
    println!("{:?}", result); // Err(NotFound)
}

Обратите внимание: ok_or автоматически выводит тип ошибки из контекста. Вам не нужно указывать его явно, если компилятор может это сделать.

А если у нас Result, а нужно Option?

Для симметрии — обратная задача тоже встречается. Если у вас есть Result, а нужно Option, используйте метод ok:

fn read_file(path: &str) -> Result<String, std::io::Error> {
    // ...
    Ok("content".to_string())
}

fn main() {
    let content: Option<String> = read_file("test.txt").ok();
    // Если Ok — будет Some, если Err — None
}

Или err — если вам нужно получить ошибку как Option:

let error: Option<std::io::Error> = read_file("missing.txt").err();

Эти методы часто используются в цепочках итераторов или при работе с ?>.

Продвинутый приём: комбинируем с map и and_then

Иногда вам нужно не просто конвертировать, но и трансформировать значение. Например, у вас есть Option<String>, а нужно Result<usize, String> (длина строки или ошибка). Метод map_or_else может помочь, но есть более идиоматичный способ:

fn get_length(input: Option<String>) -> Result<usize, String> {
    input
        .ok_or_else(|| "No input".to_string())
        .map(|s| s.len())
}

fn main() {
    let result = get_length(Some("hello".to_string()));
    println!("{:?}", result); // Ok(5)

    let result = get_length(None);
    println!("{:?}", result); // Err("No                

Блоги

Рекомендаций по книгам