Исправимые ошибки с Result
Многие ошибки не являются настолько критичными, чтобы останавливать всю программу. Иногда, когда в функции происходит сбой, необходимо просто правильно его интерпретировать и обработать. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. В этом случае вы, возможно, захотите создать новый файл, а не останавливать всю программу.
Вспомните про перечисление Result
и его варианты Ok
и Err
, которыми мы пользовались в разделе "Обработка возможных ошибок с помощью Result
" Главы 2. Его определение выглядит так:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
и E
— это параметры обобщённого типа; мы обсудим обобщённые типы более подробно в Главе 10. Всё, что вам нужно знать прямо сейчас — это то, что T
представляет тип значения, которое будет возвращено внутри варианта Ok
в случае успеха, а E
— тип ошибки, которая будет возвращена внутри варианта Err
в случае сбоя. Благодаря параметрам обобщённого типа в определении Result
, мы можем использовать его в тех случаях, когда типы значения в случае успешного и неудачного выполнения различны.
Давайте вызовем функцию, которая возвращает значение Result
, потому что может потерпеть неудачу. В Листинге 9-3 мы пытаемся открыть файл.
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
Возвращаемый тип File::open
— Result<T, E>
. Параметр обобщённого типа T
заменяется реализацией File::open
конкретным типом std::fs::File
, представляющим дескриптор файла. Параметр E
, обозначающий значение ошибки, заменяется конкретным типом std::io::Error
. Итого, такой тип возвращаемого значения означает, что вызов функции File::open
может удаться или не удаться. В случае успеха, мы получим дескриптор файла, при помощи которого мы сможем писать в файл или читать из него. В случае неудачи (например, если файл не существует или у нас нет доступа к нему), функция File::open
сообщит нам, что пошло не так. Получаемые нами значения отлично подходят для их обёртки в варианты перечисления Result
.
В случае успеха выполнения File::open
, значением переменной greeting_file_result
будет экземпляр Ok
, содержащий дескриптор файла. В случае неудачи, значение в переменной greeting_file_result
будет экземпляром Err
, содержащим дополнительную информацию о том, какая именно ошибка произошла.
Необходимо дописать в код Листинга 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов File::open
. Листинг 9-4 показывает один из способов обработки Result
— использование выражения match
(которое было рассмотрено в Главе 6).
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:?}"), }; }
Обратите внимание, что как перечисление 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:
При открытии файла произошла ошибка: 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.
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:?}"),
},
other_error => {
panic!("При открытии файла произошла ошибка: {other_error:?}");
}
},
};
}
Типом значения ошибки, возвращаемого функцией File::open
в варианте Err
, является io::Error
— структура из стандартной библиотеки. Данная структура имеет метод kind
, который можно вызвать для получения значения io::ErrorKind
. Перечисление io::ErrorKind
имеет варианты, представляющие различные типы ошибок, которые могут появиться при выполнении операций ввода-вывода. Вариант, который мы хотим сейчас использовать — ErrorKind::NotFound
. Он сообщает о том, что файл, который мы пытаемся открыть, не существует. Во второй строчке нашей программы мы передаём greeting_file_result
в выражение match
; если произойдёт ошибка, мы вызовем на ней метод kind()
и передадим возвращаемое значение во вложенный match
.
Во вложенном 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:?}"); } }); }
Хотя данный код имеет такое же поведение, как и код Листинга 9-5, он не содержит ни одного выражения
match
и, плюсом, легче читается. Рекомендуем вам вернуться к этому примеру после того, как прочитаете Главу 13 и заглянете в документацию методаunwrap_or_else
. С этими методами вы сможете обрабатывать ошибки без необходимости писать многоуровневые выраженияmatch
.
Простая паника при ошибке: методы unwrap
и expect
Использование match
работает достаточно хорошо, но оно может быть довольно многословным и не всегда хорошо передавать смысл. Тип Result<T, E>
имеет множество вспомогательных методов для выполнения различных, более специфических задач. Метод unwrap
— это метод, реализованный так же, как и выражение match
, которое мы написали в Листинге 9-4. Если значение Result
является вариантом Ok
, unwrap
возвращает значение внутри Ok
. Если значение Result
— вариант Err
, то unwrap
вызовет макрос panic!
. Вот пример использования unwrap
:
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
:
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
, а также добавляется комментарий о том, почему предполагается, что ошибки не произойдёт. Даже если предположение окажется неверным, у вас будет больше информации для отладки.
Проброс ошибок
Если вы пишете функцию, реализация которой вызывает что-то, что может завершиться ошибкой, то вместо обработки ошибки в этой же функции вы можете вернуть в вызывающий код ошибку целиком, чтобы уже он мог решить, что с ней делать. Такой приём известен как проброс ошибки. Благодаря нему мы даём больше контроля вызывающему коду, которому может быть яснее, как следует обрабатывать ошибку.
Например, код Листинга 9-6 содержит функцию, читающую имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию.
#![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), } } }
Эта функция может быть написана гораздо более коротким способом, но мы начнём с того, что многое сделаем вручную, чтобы дать вам понимание обработки ошибок; в конце покажем более короткий способ. Давайте сначала рассмотрим тип возвращаемого значения: Result<String, io::Error>
. То есть, наша функция будет возвращать тип Result<T, E>
где параметр обобщённого типа T
был заменён конкретным типом String
, а E
— типом io::Error
.
Если эта функция выполнится без проблем, то код, вызывающий эту функцию, получит значение Ok
, содержащее String
— имя пользователя, прочитанное этой функцией из файла. Если функция столкнётся с какими-либо проблемами, вызывающий код получит значение Err
, содержащее экземпляр io::Error
, который содержит дополнительную информацию о том, какие проблемы возникли. Мы выбрали io::Error
в качестве возвращаемого типа этой функции, поскольку он — тип значения ошибки, возвращаемого из обеих операций, которые мы вызываем в теле этой функции и которые могут завершиться неудачей: функции File::open
и метода read_to_string
.
Тело функции начинается с вызова File::open
. Затем мы обрабатываем значение Result
с помощью match
, аналогично match
из Дистинга 9-4. Если File::open
завершается успешно, то дескриптор файла в file
(переменной шаблона) становится значением в изменяемой переменной username_file
, и функция продолжает свою работу. В случае неудачи, вместо вызова panic!
мы используем ключевое слово return
для досрочного возвращения значения ошибки из File::open
(которое было сохранено в e
— переменной шаблона) обратно в вызывающий код как значение ошибки этой функции.
Таким образом, если у нас есть файловый дескриптор в username_file
, функция создаёт новую String
в переменной username
и вызывает метод read_to_string
на файловом дескрипторе (который в username_file
), чтобы записать содержимое файла в username
. Метод read_to_string
также возвращает Result
, потому что он может потерпеть неудачу, даже если File::open
завершился успешно. Поэтому нам нужен ещё один match
для обработки этого Result
. Если read_to_string
завершится успешно, то наша собственная функция выполнила свою работу, а потому мы возвращаем из неё имя пользователя из файла (которое теперь находится в username
), обернув его в Ok
. Если же read_to_string
потерпит неудачу, мы возвращаем значение ошибки таким же образом, как мы возвращали значение ошибки в match
, который обрабатывал возвращаемое значение File::open
. Однако нам не нужно явно указывать return
, потому что всё это — последнее выражение в функции.
После исполнения функции код, вызывающий ей, будет обрабатывать полученное значение: либо Ok
, содержащее имя пользователя, либо различные Err
, содержащие ошибки типа io::Error
. Вызывающий код должен будет решить, что делать с этими значениями. Если вызывающий код получает значение Err
, он может вызвать panic!
и завершить работу программы; может использовать имя пользователя по умолчанию; может найти имя пользователя, например, не в файле. У нас нет информации о том, что на самом деле пытается сделать вызывающий код, поэтому мы пробрасываем всю информацию об успехе или ошибках "выше", чтобы она могла обрабатываться соответствующим образом.
Эта схема проброса ошибок столь распространена, что в Rust был добавлен оператор вопросительного знака ?
, упрощающий проделанную нами работу.
Простой проброс ошибки: оператор ?
В Листинге 9-7 показана реализация read_username_from_file
, которая имеет ту же функциональность, что и в Листинге 9-6, но в этой реализации используется оператор ?
.
#![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) } }
Оператор ?
, расположенный после Result
, работает почти так же, как и те выражения match
, которые мы использовали для обработки значений Result
в Листинге 9-6. Если в качестве значения Result
будет Ok
, то значение внутри Ok
будет возвращено из этого выражения, и программа продолжит работу. Если же значение представляет собой Err
, то Err
будет возвращено из всей функции, как если бы мы использовали ключевое слово return
, так что значение ошибки будет передано в вызывающий код.
Существует разница между тем, что делает выражение match
из Листинга 9-6 и тем, что делает оператор ?
: значения ошибок, для которых вызван оператор ?
, проходят через функцию from
, определённую в трейте From
стандартной библиотеки, которая используется для преобразования значений из одного типа в другой. Когда оператор ?
вызывает функцию from
, полученный тип ошибки преобразуется в тип ошибки, определённый в возвращаемом типе текущей функции. Это полезно, когда функция возвращает только один тип ошибки, для описания всех возможных вариантов сбоев, даже если её отдельные компоненты могут выходить из строя по разным причинам.
Например, мы могли бы изменить функцию 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.
#![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) } }
Мы перенесли создание новой 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
.
#![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") } }
Чтение файла в строку — довольно распространённая операция, так что стандартная библиотека предоставляет удобную функцию fs::read_to_string
, которая открывает файл, создаёт новую String
, читает содержимое файла, размещает его в String
и возвращает её. Конечно, использование функции fs::read_to_string
не даёт возможности объяснить обработку всех ошибок, поэтому мы сначала изучили длинный способ.
Где можно использовать оператор ?
Оператор ?
может использоваться только в функциях, тип возвращаемого значения которых совместим со значением, для которого используется ?
. Это потому что оператор ?
определён так, чтобы выполнять преждевременный возврат значения из функции таким же образом, как и выражение match
, определённое в Листинге 9-6. В Листинге 9-6 match
использовало значение Result
, а ветвь с преждевременным возвратом значения вернула значение Err(e)
. Тип возвращаемого значения функции должен быть Result
, чтобы он был совместим с этим return
.
Давайте посмотрим на ошибку, которую мы получим, если воспользуемся оператором ?
в функции main
с типом возвращаемого значения, несовместимым с типом значения, для которого мы используем ?
. Взгляните на Листинг 9-10:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
Этот код открывает файл, что может не удасться. Оператор ?
выводит для себя тип 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: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
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>
любым подходящим способом.
В сообщении об ошибке также упоминалось, что ?
можно использовать и на значениях Option<T>
. Как и при использовании ?
на Result
, вы можете использовать ?
на Option
только в функции, которая возвращает Option
. Поведение оператора ?
при вызове Option<T>
похоже на его поведение при вызове Result<T, E>
: если значение равно None
, то None
будет сразу же будет возвращено из функции. Если же значение — Some
, то значение внутри Some
будет результирующим значением выражения, и функция продолжает исполняться. В Листинге 9-11 приведён пример функции, которая находит последний символ первой строки заданного текста.
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); }
Эта функция возвращает 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(())
в конец. Теперь этот код компилируется.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Тип Box<dyn Error>
— это трейт-объект, о которых мы поговорим в разделе "Использование трейт-объектов, позволяющих использовать значения разных типов" Главы 18. Пока что вы можете считать, что Box<dyn Error>
означает "любой вид ошибки". Использование ?
для значения Result
в функции main
с типом ошибки Box<dyn Error>
допустимо, так как позволяет вернуть любое значение Err
раньше времени. Даже если тело этой функции main
будет возвращать только ошибки типа std::io::Error
, указав Box<dyn Error>
, эта сигнатура останется корректной, даже если в тело main
будет добавлен код, возвращающий другие ошибки.
Если функция main
возвращает Result<(), E>
, то программа завершится со значением 0
, если main
вернёт Ok(())
, но завершится с ненулевым значением, если main
вернёт значение Err
. Программы, написанные на C, при выходе возвращают целые числа: успешно завершённые программы возвращают целое число 0
, а программы с ошибкой возвращают целое число, отличное от 0
. Rust также возвращает целые числа из программ, чтобы быть совместимым с этим соглашением.
Функция main
может возвращать любые типы, реализующие трейт std::process::Termination
, который содержит функцию report
, возвращающую ExitCode
. Обратитесь к документации стандартной библиотеки за дополнительной информацией о реализации трейта Termination
для ваших собственных типов.
Теперь, когда мы обсудили детали вызова panic!
и возврата Result
, давайте вернёмся к тому, как решить, какой из случаев подходит для какой ситуации.