Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Исправимые ошибки с Result

Most errors aren’t serious enough to require the program to stop entirely. Sometimes when a function fails, it’s for a reason that you can easily interpret and respond to. For example, if you try to open a file and that operation fails because the file doesn’t exist, you might want to create the file instead of terminating the process.

Вспомните про перечисление Result и его варианты Ok и Err, которыми мы пользовались в разделе “Обработка возможных ошибок с помощью Result Главы 2. Его определение выглядит так:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

The T and E are generic type parameters: We’ll discuss generics in more detail in Chapter 10. What you need to know right now is that T represents the type of the value that will be returned in a success case within the Ok variant, and E represents the type of the error that will be returned in a failure case within the Err variant. Because Result has these generic type parameters, we can use the Result type and the functions defined on it in many different situations where the success value and error value we want to return may differ.

Let’s call a function that returns a Result value because the function could fail. In Listing 9-3, we try to open a file.

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}
Listing 9-3: Opening a file

The return type of File::open is a Result<T, E>. The generic parameter T has been filled in by the implementation of File::open with the type of the success value, std::fs::File, which is a file handle. The type of E used in the error value is std::io::Error. This return type means the call to File::open might succeed and return a file handle that we can read from or write to. The function call also might fail: For example, the file might not exist, or we might not have permission to access the file. The File::open function needs to have a way to tell us whether it succeeded or failed and at the same time give us either the file handle or error information. This information is exactly what the Result enum conveys.

В случае успеха выполнения File::open, значением переменной greeting_file_result будет экземпляр Ok, содержащий дескриптор файла. В случае неудачи, значение в переменной greeting_file_result будет экземпляром Err, содержащим дополнительную информацию о том, какая именно ошибка произошла.

Необходимо дописать в код Листинга 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов File::open. Листинг 9-4 показывает один из способов обработки Result — использование выражения match (которое было рассмотрено в Главе 6).

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("При открытии файла произошла ошибка: {error:?}"),
    };
}
Listing 9-4: Using a match expression to handle the Result variants that might be returned

Обратите внимание, что как перечисление Option, так и перечисление Result и его варианты подключаются в область видимости по умолчанию, поэтому не нужно указывать Result:: перед использованием вариантов Ok и Err в ветках выражения match.

Если результатом будет Ok, этот код вернёт значение file из варианта Ok, а мы затем присвоим переменной greeting_file полученный дескриптор файла. После match мы сможем его для чтения или записи.

Другая ветвь match обрабатывает случай, когда мы получаем значение Err после вызова File::open. В этом примере мы решили вызвать макрос panic!. Если в нашей текущей директории нет файла с именем hello.txt, но мы всё же выполним этот код, то мы увидим следующее сообщение от макроса panic!:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`

thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Как обычно, данное сообщение точно говорит, что пошло не так.

Обработка перечня ошибок

Код в Листинге 9-4 будет вызывать panic! независимо причины неудачи при вызове File::open. Однако мы хотим предпринимать различные действия для разных причин сбоя. Допустим, мы хотим, чтобы если открытие File::open не удалось из-за отсутствия файла, то файл создавался и возвращался его дескриптор. Если же вызов File::open не удался по любой другой причине (например, потому что у нас не было прав на открытие файла), то мы всё так же хотим вызывать panic! как у нас сделано в Листинге 9-4. Для реализации перечисленного мы добавим вложенное выражение match, как показано в Листинге 9-5.

Filename: src/main.rs
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("При создании файла произошла ошибка: {e:?}"),
            },
            _ => {
                panic!("При открытии файла произошла ошибка: {error:?}");
            }
        },
    };
}
Listing 9-5: Handling different kinds of errors in different ways

The type of the value that File::open returns inside the Err variant is io::Error, which is a struct provided by the standard library. This struct has a method, kind, that we can call to get an io::ErrorKind value. The enum io::ErrorKind is provided by the standard library and has variants representing the different kinds of errors that might result from an io operation. The variant we want to use is ErrorKind::NotFound, which indicates the file we’re trying to open doesn’t exist yet. So, we match on greeting_file_result, but we also have an inner match on error.kind().

Во вложенном match мы хотим проверить, не является ли значение, возвращаемое функцией error.kind(), вариантом NotFound перечисления ErrorKind. Если оно является, то мы пытаемся создать файл с помощью File::create. Однако, поскольку File::create тоже может завершиться с ошибкой, нам нужна вторая ветвь во вложенном match. Если файл не может быть создан, выводится иное сообщение об ошибке. Вторую ветвь внешнего match мы не меняем. Итого, программа паникует при любой ошибке, кроме ошибки отсутствия файла.

Альтернативы использованию match с Result<T, E>

Как много match! Выражение match является очень полезным, но в то же время довольно примитивным. В Главе 13 вы узнаете о замыканиях, которые используются со многими методами типа Result<T, E>. Эти методы куда лаконичнее, чем прямая обработка значений Result<T, E> через match.

Например, программу из Листинга 9-5 можно вот так переписать с использованием замыканий и метода unwrap_or_else:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("При создании файла произошла ошибка: {error:?}");
            })
        } else {
            panic!("При открытии файла произошла ошибка: {error:?}");
        }
    });
}

Although this code has the same behavior as Listing 9-5, it doesn’t contain any match expressions and is cleaner to read. Come back to this example after you’ve read Chapter 13 and look up the unwrap_or_else method in the standard library documentation. Many more of these methods can clean up huge, nested match expressions when you’re dealing with errors.

Shortcuts for Panic on Error

Использование match работает достаточно хорошо, но оно может быть довольно многословным и не всегда хорошо передавать смысл. Тип Result<T, E> имеет множество вспомогательных методов для выполнения различных, более специфических задач. Метод unwrap — это метод, реализованный так же, как и выражение match, которое мы написали в Листинге 9-4. Если значение Result является вариантом Ok, unwrap возвращает значение внутри Ok. Если значение Result — вариант Err, то unwrap вызовет макрос panic!. Вот пример использования unwrap:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

Если мы запустим этот код при отсутствии файла hello.txt, то увидим сообщение об ошибке из вызова panic! методом unwrap:

thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Другой метод, похожий на unwrap — это expect, позволяющий указать собственное сообщение об ошибке для макроса panic!. Использование expect вместо unwrap с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. Вот пример использования expect:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt должен быть доступен этому проекту");
}

Метод expect используется так же, как unwrap: либо возвращается дескриптор файла, либо вызывается макрос panic!. Наше сообщение об ошибке, переданное expect, будет передано в panic! и заменит стандартное используемое сообщение. Вот как это выглядит:

thread 'main' panicked at src/main.rs:5:10:
hello.txt должен быть доступен этому проекту: Os { code: 2, kind: NotFound, message: "No such file or directory" }

В реальных программах часто используется expect вместо unwrap, а также добавляется комментарий о том, почему предполагается, что ошибки не произойдёт. Даже если предположение окажется неверным, у вас будет больше информации для отладки.

Проброс ошибок

When a function’s implementation calls something that might fail, instead of handling the error within the function itself, you can return the error to the calling code so that it can decide what to do. This is known as propagating the error and gives more control to the calling code, where there might be more information or logic that dictates how the error should be handled than what you have available in the context of your code.

Например, код Листинга 9-6 содержит функцию, читающую имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}
Listing 9-6: A function that returns errors to the calling code using match

Эта функция может быть написана гораздо более коротким способом, но мы начнём с того, что многое сделаем вручную, чтобы дать вам понимание обработки ошибок; в конце покажем более короткий способ. Давайте сначала рассмотрим тип возвращаемого значения: Result<String, io::Error>. То есть, наша функция будет возвращать тип Result<T, E> где параметр обобщённого типа T был заменён конкретным типом String, а E — типом io::Error.

Если эта функция выполнится без проблем, то код, вызывающий эту функцию, получит значение Ok, содержащее String — имя пользователя, прочитанное этой функцией из файла. Если функция столкнётся с какими-либо проблемами, вызывающий код получит значение Err, содержащее экземпляр io::Error, который содержит дополнительную информацию о том, какие проблемы возникли. Мы выбрали io::Error в качестве возвращаемого типа этой функции, поскольку он — тип значения ошибки, возвращаемого из обеих операций, которые мы вызываем в теле этой функции и которые могут завершиться неудачей: функции File::open и метода read_to_string.

The body of the function starts by calling the File::open function. Then, we handle the Result value with a match similar to the match in Listing 9-4. If File::open succeeds, the file handle in the pattern variable file becomes the value in the mutable variable username_file and the function continues. In the Err case, instead of calling panic!, we use the return keyword to return early out of the function entirely and pass the error value from File::open, now in the pattern variable e, back to the calling code as this function’s error value.

So, if we have a file handle in username_file, the function then creates a new String in variable username and calls the read_to_string method on the file handle in username_file to read the contents of the file into username. The read_to_string method also returns a Result because it might fail, even though File::open succeeded. So, we need another match to handle that Result: If read_to_string succeeds, then our function has succeeded, and we return the username from the file that’s now in username wrapped in an Ok. If read_to_string fails, we return the error value in the same way that we returned the error value in the match that handled the return value of File::open. However, we don’t need to explicitly say return, because this is the last expression in the function.

После исполнения функции код, вызывающий ей, будет обрабатывать полученное значение: либо Ok, содержащее имя пользователя, либо различные Err, содержащие ошибки типа io::Error. Вызывающий код должен будет решить, что делать с этими значениями. Если вызывающий код получает значение Err, он может вызвать panic! и завершить работу программы; может использовать имя пользователя по умолчанию; может найти имя пользователя, например, не в файле. У нас нет информации о том, что на самом деле пытается сделать вызывающий код, поэтому мы пробрасываем всю информацию об успехе или ошибках “выше”, чтобы она могла обрабатываться соответствующим образом.

Эта схема проброса ошибок столь распространена, что в Rust был добавлен оператор вопросительного знака ?, упрощающий проделанную нами работу.

The ? Operator Shortcut

В Листинге 9-7 показана реализация read_username_from_file, которая имеет ту же функциональность, что и в Листинге 9-6, но в этой реализации используется оператор ?.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}
Listing 9-7: A function that returns errors to the calling code using the ? operator

The ? placed after a Result value is defined to work in almost the same way as the match expressions that we defined to handle the Result values in Listing 9-6. If the value of the Result is an Ok, the value inside the Ok will get returned from this expression, and the program will continue. If the value is an Err, the Err will be returned from the whole function as if we had used the return keyword so that the error value gets propagated to the calling code.

There is a difference between what the match expression from Listing 9-6 does and what the ? operator does: Error values that have the ? operator called on them go through the from function, defined in the From trait in the standard library, which is used to convert values from one type into another. When the ? operator calls the from function, the error type received is converted into the error type defined in the return type of the current function. This is useful when a function returns one error type to represent all the ways a function might fail, even if parts might fail for many different reasons.

Например, мы могли бы изменить функцию read_username_from_file в Листинге 9-7 так, чтобы возвращать пользовательский тип ошибки под именем OurError. Если мы определим impl From<io::Error> for OurError (чтобы получить возможность создавать экземпляры OurError из io::Error), то оператор ?, вызываемый в теле read_username_from_file, вызовет from и преобразует типы ошибок без необходимости добавления дополнительного кода в функцию.

Обращаясь к Листингу 9-7 как к примеру, оператор ? в конце вызова File::open вернёт значение внутри Ok в переменную username_file. Если произойдёт ошибка, оператор ? выполнит преждевременный возврат значения Err вызывающему коду. То же самое относится к оператору ? в конце вызова read_to_string.

Оператор ? позволил избавиться от большого количества шаблонного кода и упростить реализацию этой функции. Мы могли бы даже ещё больше сократить этот код, если бы использовали цепочку вызовов методов сразу после ?, как показано в Листинге 9-8.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}
Listing 9-8: Chaining method calls after the ? operator

Мы перенесли создание новой String в username в начало функции; эта часть не изменилась. Вместо создания переменной username_file, мы вызвали read_to_string непосредственно на результате File::open("hello.txt")?. У нас по-прежнему есть ? в конце вызова read_to_string, и мы по-прежнему возвращаем значение Ok, содержащее username, когда и File::open и read_to_string завершаются успешно, а не возвращают ошибки. Функциональность снова такая же, как в Листинге 9-6 и Листинге 9-7; это просто другой, более эргономичный способ её написания.

Листинг 9-9 демонстрирует ещё более короткое определение нашей функции с помощью fs::read_to_string.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}
Listing 9-9: Using fs::read_to_string instead of opening and then reading the file

Чтение файла в строку — довольно распространённая операция, так что стандартная библиотека предоставляет удобную функцию fs::read_to_string, которая открывает файл, создаёт новую String, читает содержимое файла, размещает его в String и возвращает её. Конечно, использование функции fs::read_to_string не даёт возможности объяснить обработку всех ошибок, поэтому мы сначала изучили длинный способ.

Where to Use the ? Operator

Оператор ? может использоваться только в функциях, тип возвращаемого значения которых совместим со значением, для которого используется ?. Это потому что оператор ? определён так, чтобы выполнять преждевременный возврат значения из функции таким же образом, как и выражение match, определённое в Листинге 9-6. В Листинге 9-6 match использовало значение Result, а ветвь с преждевременным возвратом значения вернула значение Err(e). Тип возвращаемого значения функции должен быть Result, чтобы он был совместим с этим return.

Давайте посмотрим на ошибку, которую мы получим, если воспользуемся оператором ? в функции main с типом возвращаемого значения, несовместимым с типом значения, для которого мы используем ?. Взгляните на Листинг 9-10:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}
Listing 9-10: Attempting to use the ? in the main function that returns () won’t compile.

Этот код открывает файл, что может не удасться. Оператор ? выводит для себя тип Result, возвращаемый File::open, но функция main возвращает тип (), а не Result. Если мы скомпилируем этот код, мы получим следующее сообщение об ошибке:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 |     let greeting_file = File::open("hello.txt")?;
5 +     Ok(())
  |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

Эта ошибка указывает на то, что оператор ? разрешено использовать только в функции, которая возвращает Result, Option или другой тип, реализующий FromResidual.

Для исправления ошибки есть два варианта. Один — изменить возвращаемый тип вашей функции так, чтобы он был совместим со значением, для которого вы используете оператор ?, если ничто этому не препятствует. Другой — использовать match или один из методов Result<T, E> для обработки Result<T, E> любым подходящим способом.

The error message also mentioned that ? can be used with Option<T> values as well. As with using ? on Result, you can only use ? on Option in a function that returns an Option. The behavior of the ? operator when called on an Option<T> is similar to its behavior when called on a Result<T, E>: If the value is None, the None will be returned early from the function at that point. If the value is Some, the value inside the Some is the resultant value of the expression, and the function continues. Listing 9-11 has an example of a function that finds the last character of the first line in the given text.

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Привет!\nКак ты?"),
        Some('!')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nсойдёт"), None);
}
Listing 9-11: Using the ? operator on an Option<T> value

Эта функция возвращает Option<char>, потому что может быть, что строка будет не пустой и символ будет найден, а может быть, что его и не будет. Этот код принимает строковый срез text и вызывает на нём метод lines, который возвращает итератор всех строчек в строке. Поскольку эта функция хочет проверить первую строку, она вызывает next у итератора, чтобы получить первое значение от итератора. Если text является пустой строкой, этот вызов next вернёт None, и в этом случае мы используем ? чтобы остановить и вернуть None из last_char_of_first_line. Если text не является пустой строкой, next вернёт значение Some, содержащее строковый срез первой строки в text.

Символ ? извлекает строковый срез, и мы можем вызвать на нём метод chars, чтобы получить итератор символов. Нас интересует последний символ в первой строке, поэтому мы вызываем last, чтобы вернуть последний элемент в итераторе. Вернётся Option, потому что возможно, что первая строка пуста: например, если text начинается с пустой строки, но имеет символы в других строках, как в "\nсойдёт". Однако, если в первой строке есть последний символ, он будет возвращён в варианте Some. Оператор ? в середине даёт нам лаконичный способ выразить эту логику, позволяя реализовать функцию в одной строке. Если бы мы не могли использовать оператор ? в Option, нам пришлось бы реализовать эту логику, используя больше вызовов методов или выражение match.

Обратите внимание: вы можете использовать оператор ? на Result в функции, которая возвращает Result; вы можете использовать оператор ? на Option в функции, которая возвращает Option; но вы не можете пытаться использовать один вместо другого. Оператор ? не будет автоматически преобразовывать Result в Option или наоборот; в этих случаях вы можете использовать такие методы, как метод ok для Result или метод ok_or для Option, чтобы выполнять преобразование явно.

До сих пор все функции main, которые мы использовали, возвращали (). Функция main — особенная, потому что это точка входа и выхода исполняемых программ, и существуют ограничения на тип возвращаемого значения, необходимые для того, что программы вели себя так, как ожидается.

К счастью, main ещё может возвращать Result<(), E>. В Листинге 9-12 используется код из Листинга 9-10, но мы изменили возвращаемый тип main на Result<(), Box<dyn Error>> и добавили возвращаемое значение Ok(()) в конец. Теперь этот код компилируется.

Filename: src/main.rs
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}
Listing 9-12: Changing main to return Result<(), E> allows the use of the ? operator on Result values.

The Box<dyn Error> type is a trait object, which we’ll talk about in “Using Trait Objects to Abstract over Shared Behavior” in Chapter 18. For now, you can read Box<dyn Error> to mean “any kind of error.” Using ? on a Result value in a main function with the error type Box<dyn Error> is allowed because it allows any Err value to be returned early. Even though the body of this main function will only ever return errors of type std::io::Error, by specifying Box<dyn Error>, this signature will continue to be correct even if more code that returns other errors is added to the body of main.

When a main function returns a Result<(), E>, the executable will exit with a value of 0 if main returns Ok(()) and will exit with a nonzero value if main returns an Err value. Executables written in C return integers when they exit: Programs that exit successfully return the integer 0, and programs that error return some integer other than 0. Rust also returns integers from executables to be compatible with this convention.

Функция main может возвращать любые типы, реализующие трейт std::process::Termination, который содержит функцию report, возвращающую ExitCode. Обратитесь к документации стандартной библиотеки за дополнительной информацией о реализации трейта Termination для ваших собственных типов.

Теперь, когда мы обсудили детали вызова panic! и возврата Result, давайте вернёмся к тому, как решить, какой из случаев подходит для какой ситуации.