Исправимые ошибки и Result
Большинство ошибок не настолько серьёзны, чтобы останавливать программу целиком. Часто функция не срабатывает по причине, которую легко понять и на которую можно отреагировать. Например, если вы пытаетесь открыть файл, а его нет, возможно, разумнее создать файл, а не завершать процесс.
Вспомните раздел «Обрабатываем возможный сбой с помощью
Result» главы 2: перечисление Result
определено с двумя вариантами, Ok и Err:
#![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 с дескриптором файла. Если нет — экземпляр 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:?}"),
};
}
Result выражением matchЗаметьте, что перечисление Result и его варианты, как и Option, уже
подключены прелюдией, поэтому в ветках match не нужно писать Result::
перед Ok и Err.
Если результат — Ok, этот код достаёт из варианта Ok внутреннее значение
file, и мы присваиваем этот дескриптор переменной 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' (6018048) 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 не сработала по любой другой причине — например, не хватает
прав на открытие файла, — код по-прежнему должен вызывать 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:?}");
}
},
};
}
Внутри варианта Err, который возвращает File::open, лежит значение типа
io::Error — это структура из стандартной библиотеки. У неё есть метод
kind, который возвращает значение io::ErrorKind. Перечисление
io::ErrorKind тоже из стандартной библиотеки, его варианты описывают разные
виды ошибок, которые могут возникнуть при операциях ввода-вывода. Нам нужен
вариант ErrorKind::NotFound: он означает, что файла, который мы пытаемся
открыть, ещё нет. Так что мы сопоставляем greeting_file_result, а внутри —
ещё и 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!("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. Если 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!. Только сообщением при вызове panic! будет не
стандартный текст, который использует unwrap, а параметр, переданный в
expect. Выглядит это так:
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" }
В коде продакшен-качества большинство растацеанов выбирают expect, а не
unwrap, и поясняют в сообщении, почему операция должна всегда проходить
успешно. Тогда, если ваши предположения однажды окажутся неверными, при
отладке у вас будет больше информации.
Проброс ошибок
Когда функция вызывает что-то, что может не сработать, ошибку не обязательно обрабатывать внутри самой функции: её можно вернуть вызывающему коду, и пусть он решает, что делать. Это называется пробросом ошибки (propagating). Так у вызывающего кода больше контроля: у него может быть больше информации или логики, определяющей, как обработать ошибку, чем есть в контексте вашего кода.
Например, в листинге 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 внутри — именем пользователя (username), которое функция прочитала
из файла. Если возникнут проблемы, вызывающий код получит значение 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 и вызывает у дескриптора username_file метод
read_to_string, чтобы прочитать содержимое файла в username. Метод
read_to_string тоже возвращает Result: он может не сработать, даже если
File::open прошла успешно. Поэтому нужен ещё один match. Если
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 вместо открытия и последующего чтения файлаПрочитать файл в строку — довольно частая операция, поэтому в стандартной
библиотеке есть удобная функция fs::read_to_string: она открывает файл,
создаёт новую String, читает в неё содержимое файла и возвращает её.
Конечно, с fs::read_to_string не получилось бы объяснить всю обработку
ошибок, поэтому сначала мы пошли длинным путём.
Где можно использовать оператор ?
Оператор ? можно использовать только в функциях, возвращаемый тип которых
совместим со значением, к которому применяется ?. Ведь оператор ? досрочно
возвращает значение из функции — так же, как выражение match из листинга
9-6. Там match работал со значением Result, а ветка досрочного выхода
возвращала Err(e). Чтобы такой return был допустим, функция должна
возвращать Result.
В листинге 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.
Исправить это можно двумя способами. Первый — поменять возвращаемый тип
функции на совместимый со значением, к которому применяется ?, если ничто
этому не мешает. Второй — обработать Result<T, E> выражением match или
одним из методов 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, а завершившиеся с ошибкой — какое-то
другое число. Rust тоже возвращает из исполняемых файлов целые числа, чтобы
соответствовать этому соглашению.
Функция main может возвращать любые типы, реализующие трейт
std::process::Termination. В нём есть функция
report, которая возвращает ExitCode. О том, как реализовать трейт
Termination для своих типов, читайте в документации стандартной библиотеки.
Теперь, когда мы разобрали подробности вызова panic! и возврата Result,
вернёмся к вопросу о том, как решить, что уместнее в том или ином случае.