Восстанавливаемые ошибки с Result
Большинство ошибок недостаточно серьезны, чтобы требовать полной остановки программы. Иногда функция завершается неудачно по причине, которую можно легко понять и на которую можно отреагировать. Например, если вы пытаетесь открыть файл и эта операция завершается неудачно, потому что файла не существует, вы можете захотеть создать файл, а не завершать процесс.
Вспомните из раздела «Обработка потенциальной ошибки с
Result» главы 2, что enum Result определен
с двумя вариантами, Ok и Err, следующим образом:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T и E – это обобщенные параметры типов: подробнее об обобщениях мы
поговорим в главе 10. Сейчас вам нужно знать, что T представляет тип
значения, которое будет возвращено в случае успеха внутри варианта Ok, а E
представляет тип ошибки, которая будет возвращена в случае неудачи внутри
варианта Err. Поскольку у Result есть эти обобщенные параметры типов, мы
можем использовать тип 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 нужен способ сообщить нам, завершилась ли она
успешно или неудачно, и одновременно дать нам либо дескриптор файла, либо
информацию об ошибке. Именно эту информацию и передает enum 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!("Problem opening the file: {error:?}"),
};
}
match для обработки вариантов Result, которые могут быть возвращеныОбратите внимание: как и enum Option, enum Result и его варианты уже
введены в область видимости прелюдией, поэтому нам не нужно указывать
Result:: перед вариантами Ok и Err в ветвях match.
Когда результат равен Ok, этот код вернет внутреннее значение file из
варианта Ok, а затем мы присвоим этот дескриптор файла переменной
greeting_file. После match мы сможем использовать дескриптор файла для
чтения или записи.
Другая ветвь match обрабатывает случай, когда от File::open мы получаем
значение Err. В этом примере мы решили вызвать макрос 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.
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!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
Тип значения, которое File::open возвращает внутри варианта Err, –
io::Error, структура из стандартной библиотеки. У этой структуры есть метод
kind, который можно вызвать, чтобы получить значение io::ErrorKind. Enum
io::ErrorKind предоставляется стандартной библиотекой и содержит варианты,
представляющие разные виды ошибок, которые могут возникнуть при операции
ввода-вывода. Нужный нам вариант – ErrorKind::NotFound, который указывает,
что файла, который мы пытаемся открыть, еще не существует. Поэтому мы
сопоставляем greeting_file_result, но также используем внутренний match для
error.kind().
Условие, которое мы хотим проверить во внутреннем match, состоит в том,
является ли значение, возвращенное error.kind(), вариантом NotFound enum
ErrorKind. Если это так, мы пытаемся создать файл с помощью File::create.
Однако, поскольку File::create тоже может завершиться неудачно, нам нужна
вторая ветвь во внутреннем выражении match. Когда файл нельзя создать,
печатается другое сообщение об ошибке. Вторая ветвь внешнего match остается
прежней, поэтому программа паникует при любой ошибке, кроме ошибки отсутствия
файла.
Альтернативы использованию match с Result<T, E>
Здесь много match! Выражение match очень полезно, но при этом довольно
низкоуровневое. В главе 13 вы узнаете о замыканиях, которые используются со
многими методами, определенными для Result<T, E>. Эти методы могут быть
более лаконичными, чем использование match, когда вы обрабатываете значения
Result<T, E> в своем коде.
Например, вот еще один способ записать ту же логику, что и в листинге 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!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
Хотя этот код ведет себя так же, как код из листинга 9-5, в нем нет ни одного
выражения match, и его проще читать. Вернитесь к этому примеру после
прочтения главы 13 и найдите метод unwrap_or_else в документации
стандартной библиотеки. Многие подобные методы могут привести в порядок
огромные вложенные выражения match, когда вы работаете с ошибками.
Краткие способы вызвать панику при ошибке
Использование 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" }
Аналогично, метод expect позволяет нам также выбрать сообщение об ошибке для
panic!. Использование expect вместо unwrap и хорошие сообщения об ошибках
могут передать ваше намерение и упростить поиск источника паники. Синтаксис
expect выглядит так:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Мы используем expect так же, как unwrap: чтобы вернуть дескриптор файла или
вызвать макрос panic!. Сообщение об ошибке, которое expect использует при
вызове panic!, будет параметром, который мы передаем в expect, а не
стандартным сообщением panic!, используемым unwrap. Вот как это выглядит:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
В коде промышленного качества большинство Rust-разработчиков выбирают
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),
}
}
}
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.
Тело функции начинается с вызова функции File::open. Затем мы обрабатываем
значение Result с помощью match, похожего на match из листинга 9-4. Если
File::open завершается успешно, дескриптор файла в переменной шаблона file
становится значением изменяемой переменной username_file, и функция
продолжает выполнение. В случае Err вместо вызова 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, что 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 вместо открытия и последующего чтения файлаЧтение файла в строку – довольно распространенная операция, поэтому
стандартная библиотека предоставляет удобную функцию fs::read_to_string,
которая открывает файл, создает новую String, читает содержимое файла,
помещает содержимое в эту String и возвращает ее. Конечно, использование
fs::read_to_string не дало бы нам возможности объяснить всю обработку
ошибок, поэтому сначала мы сделали это более длинным способом.
Где можно использовать оператор ?
Оператор ? можно использовать только в функциях, возвращаемый тип которых
совместим со значением, к которому применяется ?. Причина в том, что
оператор ? определен так, чтобы выполнять досрочный возврат значения из
функции, так же как выражение match, которое мы определили в листинге 9-6. В
листинге 9-6 match использовал значение Result, а ветвь досрочного
возврата возвращала значение Err(e). Возвращаемый тип функции должен быть
Result, чтобы быть совместимым с этим return.
В листинге 9-10 посмотрим, какую ошибку мы получим, если используем оператор
? в функции main с возвращаемым типом, несовместимым с типом значения, к
которому мы применяем ?.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
? в функции main, которая возвращает (), не скомпилируется.Этот код открывает файл, что может завершиться неудачно. Оператор ? следует
за значением 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> подходящим образом.
Сообщение об ошибке также упоминало, что ? можно использовать и со значениями
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("Hello, world\nHow are you today?"),
Some('d')
);
assert_eq!(last_char_of_first_line(""), None);
assert_eq!(last_char_of_first_line("\nhi"), None);
}
? для значения Option<T>Эта функция возвращает Option<char>, потому что символ может существовать, а
может и отсутствовать. Этот код принимает строковый срез text в качестве
аргумента и вызывает для него метод lines, который возвращает итератор по
строкам в строке. Поскольку эта функция хочет проверить первую строку, она
вызывает next у итератора, чтобы получить первое значение из итератора. Если
text – пустая строка, этот вызов next вернет None; в таком случае мы
используем ?, чтобы остановиться и вернуть None из
last_char_of_first_line. Если text не является пустой строкой, next
вернет значение Some, содержащее строковый срез первой строки в text.
Оператор ? извлекает строковый срез, и мы можем вызвать chars для этого
строкового среза, чтобы получить итератор по его символам. Нас интересует
последний символ в этой первой строке, поэтому мы вызываем last, чтобы
вернуть последний элемент итератора. Это Option, потому что первая строка
может быть пустой: например, если text начинается с пустой строки, но
содержит символы в других строках, как в "\nhi". Однако если в первой строке
есть последний символ, он будет возвращен в варианте 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(())
}
main так, чтобы она возвращала Result<(), E>, позволяет использовать оператор ? для значений Result.Тип 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,
вернемся к вопросу о том, как решать, что из этого уместно использовать в
каких случаях.