ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1156
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Исправимые ошибки с Result
Многие ошибки являются не настолько критичными, чтобы останавливать выполнение программы. Иногда, когда в функции происходит сбой, необходима просто правильная интерпретация и обработка ошибки. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. Вы, возможно, захотите исправить ситуацию и создать новый файл вместо остановки программы.
Вспомните раздел ["Обработка потенциального сбоя с помощью типа
Result
"] главы 2:
мы использовали там перечисление
Result
, имеющее два варианта,
Ok и
Err для обработки сбоев. Само перечисление определено следующим образом:
Типы
T
и
E
являются параметрами обобщённого типа: мы обсудим обобщённые типы более подробно в Главе 10. Все что вам нужно знать прямо сейчас - это то, что
T
представляет тип значения, которое будет возвращено в случае успеха внутри варианта
Ok
, а
E
представляет тип ошибки, которая будет возвращена при сбое внутри варианта
Err
. Так как тип
Result имеет эти обобщённые параметры (generic type parameters), мы можем использовать тип
Result и функции, которые определены для него, в разных ситуациях, когда тип успешного значение и значения ошибки, которые мы хотим вернуть, отличаются.
Давайте вызовем функцию, которая возвращает значение
Result
, потому что может потерпеть неудачу. В листинге 9-3 мы пытаемся открыть файл.
Файл: src/main.rs
Листинг 9-3: Открытие файла
File::open возвращает значения типа
Result
. Универсальный тип
T
в реализации
File::open соответствует типу успешно полученного значения, std::fs::File
, а именно дескриптору файла. Тип
E
, используемый для значения в случае возникновения ошибки, - std::io::Error
. Такой возвращаемый тип означает, что вызов
File::open может быть успешным и вернуть дескриптор файла, из которого мы можем читать или в который можем писать. Также вызов функции может завершиться неудачей: например, файл может не существовать, или у нас может не быть разрешения enum
Result
{
Ok
(T),
Err
(E),
} use std::fs::File; fn main
() { let greeting_file_result = File::open(
"hello.txt"
);
}
Как обычно, данное сообщение точно говорит, что пошло не так.
Обработка различных ошибок с помощью match
Код в листинге 9-4 будет вызывать panic!
независимо от того, почему вызов
File::open не удался. Однако мы хотим предпринять различные действия для разных причин сбоя.
Если открытие
File::open не удалось из-за отсутствия файла, мы хотим создать файл и вернуть его дескриптор. Если вызов
File::open не удался по любой другой причине - например, потому что у нас не было прав на открытие файла, то все равно мы хотим вызвать panic!
как у нас сделано в листинге 9-4. Для этого мы добавляем выражение внутреннего match
, показанное в листинге 9-5.
Файл: src/main.rs
Листинг 9-5: Обработка различных ошибок разными способами
Типом значения возвращаемого функцией
File::open внутри
Err варианта является io::Error
, структура из стандартной библиотеки. Данная структура имеет метод kind
,
который можно вызвать для получения значения io::ErrorKind
. Перечисление io::ErrorKind из стандартной библиотеки имеет варианты, представляющие различные типы ошибок, которые могут появиться при выполнении операций в io
. Вариант,
$
cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling` thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 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),
}, other_error => { panic!
(
"Problem opening the file: {:?}"
, other_error);
}
},
};
}
Эта функция может быть написана гораздо более коротким способом, но мы начнём с того, что многое сделаем вручную, чтобы изучить обработку ошибок; а в конце покажем более короткий способ. Давайте сначала рассмотрим тип возвращаемого значения:
Result
. Здесь есть возвращаемое значение функции типа
ResultE>
где шаблонный параметр
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, но в этой реализации используется оператор
?
Файл: src/main.rs
Листинг 9-7: Функция, возвращающая ошибки в вызывающий код с помощью оператора
?
Выражение
?
, расположенное после
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<:error> for OurError для создания экземпляра
OurError из io::Error
, то оператор
?
, вызываемый в теле read_username_from_file
, вызовет from и преобразует типы ошибок без необходимости добавления дополнительного кода в функцию.
use std::fs::File; use std::io; use std::io::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)
}
В случае листинга 9-7 оператор
?
в конце вызова
File::open вернёт значение внутри
Ok в переменную username_file
. Если произойдёт ошибка, оператор
?
выполнит ранний возврат значения
Err вызывающему коду. То же самое относится к оператору
?
в конце вызова read_to_string
Оператор
?
позволяет избавиться от большого количества шаблонного кода и упростить реализацию этой функции. Мы могли бы даже ещё больше сократить этот код,
если бы использовали цепочку вызовов методов сразу после
?
, как показано в листинге
9-8.
Файл: src/main.rs
Листинг 9-8: Цепочка вызовов методов после оператора
?
Мы перенесли создание новой
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
демонстрирует способ сделать её ещё короче.
Файл: src/main.rs
Листинг 9-9: Использование
fs::read_to_string
вместо открытия и последующего чтения файла
use std::fs::File; use std::io; use std::io::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)
} 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
В листинге 9-10 давайте посмотрим на ошибку, которую мы получим, если воспользуемся
?
оператор в main функции с типом возвращаемого значения, несовместимым с типом используемого нами значения
?
на:
Файл : src/main.rs
Листинг 9-10: Попытка использовать
?
в
main
функции, которая возвращает
()
, не будет
компилироваться
Этот код открывает файл, что может привести к сбою.
?
оператор следует за значением
Result
, возвращаемым
File::open
, но эта main функция имеет возвращаемый тип
()
, а не
Result
. Когда мы компилируем этот код, мы получаем следующее сообщение об ошибке:
use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
)?;
}
Эта ошибка указывает на то, что оператор
?
разрешено использовать только в функции,
которая возвращает
Result
,
Option или другой тип, реализующий
FromResidual
Для исправления ошибки есть два варианта. Первый - изменить возвращаемый тип вашей функции так, чтобы он был совместим со значением, для которого вы используете оператор
?
, если у вас нет ограничений, препятствующих этому. Другой способ - использовать match или один из методов
Result
для обработки
Result
любым подходящим способом.
Эта ошибка указывает на то, что оператор
?
разрешено использовать только в функции,
которая возвращает
Result
,
Option или другой тип, который реализует
FromResidual
Чтобы исправить ошибку, есть два варианта. Первый - изменить тип возвращаемый из функции, чтобы он был совместим со значением, для которого вы используете оператор
?
, если у вас нет ограничений, препятствующих этому. Второй заключается в использовании match или одного из методов
Result
для обработки
Result
любым подходящим способом.
Listing 9-11: Using the
?
operator on an
Option
value
Эта функция возвращает
Option
, потому что возможно, что там есть символ, но также возможно, что его нет. Этот код принимает аргумент среза text строки и вызывает для него метод lines
, который возвращает итератор для строк в строке.
Поскольку эта функция хочет проверить первую строку, она вызывает next у итератора,
чтобы получить первое значение от итератора. Если text является пустой строкой, этот вызов next вернёт
None
, и в этом случае мы используем
?
чтобы остановить и вернуть
$
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() {
4 | | let greeting_file = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`. error: could not compile `error-handling` due to previous error fn last_char_of_first_line
(text: &
str
) ->
Option
<
char
> { text.lines().next()?.chars().last()
}
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
, чтобы выполнить преобразование явно.
Обратите внимание, что вы можете использовать оператор
?
на
Result в функции,
которая возвращает
Result
, и вы можете использовать оператор
?
на
Option в
функции, которая возвращает
Option
, но вы не можете смешивать и сочетать их.
Оператор
?
не будет автоматически преобразовывать
Result в
Option или наоборот; в этих случаях, вы можете использовать такие методы, как ok из
Result или ok_or из
Option для явного преобразования.
К счастью, main также может возвращать
Result<(), E>
. В листинге 9-12 используется код из листинга 9-10, но мы изменили возвращаемый тип main на
Result<(), BoxError>>
и добавили возвращаемое значение
Ok(())
в конец. Теперь этот код будет скомпилирован:
Листинг 9-12: Замена
main
на return
Result<(), E>
позволяет использовать оператор
?
оператор над
значениями
Result
Тип
Box
является трейт-объектом, о котором мы поговорим в разделе "Использование трейт-объектов, допускающих значения разных типов"
в главе 17. Пока use std::error::Error; use std::fs::File; fn main
() ->
Result
<(),
Box
<
dyn
Error>> { let greeting_file = File::open(
"hello.txt"
)?;
Ok
(())
}
Многие ошибки являются не настолько критичными, чтобы останавливать выполнение программы. Иногда, когда в функции происходит сбой, необходима просто правильная интерпретация и обработка ошибки. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. Вы, возможно, захотите исправить ситуацию и создать новый файл вместо остановки программы.
Вспомните раздел ["Обработка потенциального сбоя с помощью типа
Result
"] главы 2:
мы использовали там перечисление
Result
, имеющее два варианта,
Ok и
Err для обработки сбоев. Само перечисление определено следующим образом:
Типы
T
и
E
являются параметрами обобщённого типа: мы обсудим обобщённые типы более подробно в Главе 10. Все что вам нужно знать прямо сейчас - это то, что
T
представляет тип значения, которое будет возвращено в случае успеха внутри варианта
Ok
, а
E
представляет тип ошибки, которая будет возвращена при сбое внутри варианта
Err
. Так как тип
Result имеет эти обобщённые параметры (generic type parameters), мы можем использовать тип
Result и функции, которые определены для него, в разных ситуациях, когда тип успешного значение и значения ошибки, которые мы хотим вернуть, отличаются.
Давайте вызовем функцию, которая возвращает значение
Result
, потому что может потерпеть неудачу. В листинге 9-3 мы пытаемся открыть файл.
Файл: src/main.rs
Листинг 9-3: Открытие файла
File::open возвращает значения типа
Result
. Универсальный тип
T
в реализации
File::open соответствует типу успешно полученного значения, std::fs::File
, а именно дескриптору файла. Тип
E
, используемый для значения в случае возникновения ошибки, - std::io::Error
. Такой возвращаемый тип означает, что вызов
File::open может быть успешным и вернуть дескриптор файла, из которого мы можем читать или в который можем писать. Также вызов функции может завершиться неудачей: например, файл может не существовать, или у нас может не быть разрешения enum
Result
Ok
(T),
Err
(E),
} use std::fs::File; fn main
() { let greeting_file_result = File::open(
"hello.txt"
);
}
на доступ к файлу. Функция
File::open должна иметь способ сообщить нам об успехе или неудаче и в то же время дать нам либо дескриптор файла, либо информацию об ошибке. Эту возможность как раз и предоставляет перечисление
Result
В случае успеха
File::open значением переменной greeting_file_result будет экземпляр
Ok
, содержащий дескриптор файла. В случае неудачи значение в переменной greeting_file_result будет экземпляром
Err
, содержащим дополнительную информацию о том, какая именно ошибка произошла.
Необходимо дописать в код листинга 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов
File::open
. Листинг 9-4 показывает один из способов обработки
Result
- пользуясь базовым инструментом языка, таким как выражение match
, рассмотренным в Главе 6.
Файл: src/main.rs
Листинг 9-4: Использование выражения
match
для обработки возвращаемых вариантов типа
Result
Обратите внимание, что также как перечисление
Option
, перечисление
Result и его варианты, входят в область видимости благодаря авто-импорту (prelude), поэтому не нужно указывать
Result::
перед использованием вариантов
Ok и
Err в ветках выражения match
Если результатом будет
Ok
, этот код вернёт значение file из варианта
Ok
, а мы затем присвоим это значение файлового дескриптора переменной greeting_file
. После match мы можем использовать дескриптор файла для чтения или записи.
Другая ветвь match обрабатывает случай, где мы получаем значение
Err после вызова
File::open
. В этом примере мы решили вызвать макрос panic!
. Если в нашей текущей директории нет файла с именем hello.txt и мы выполним этот код, то мы увидим следующее сообщение от макроса panic!
:
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),
};
}
File::open должна иметь способ сообщить нам об успехе или неудаче и в то же время дать нам либо дескриптор файла, либо информацию об ошибке. Эту возможность как раз и предоставляет перечисление
Result
В случае успеха
File::open значением переменной greeting_file_result будет экземпляр
Ok
, содержащий дескриптор файла. В случае неудачи значение в переменной greeting_file_result будет экземпляром
Err
, содержащим дополнительную информацию о том, какая именно ошибка произошла.
Необходимо дописать в код листинга 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов
File::open
. Листинг 9-4 показывает один из способов обработки
Result
- пользуясь базовым инструментом языка, таким как выражение match
, рассмотренным в Главе 6.
Файл: src/main.rs
Листинг 9-4: Использование выражения
match
для обработки возвращаемых вариантов типа
Result
Обратите внимание, что также как перечисление
Option
, перечисление
Result и его варианты, входят в область видимости благодаря авто-импорту (prelude), поэтому не нужно указывать
Result::
перед использованием вариантов
Ok и
Err в ветках выражения match
Если результатом будет
Ok
, этот код вернёт значение file из варианта
Ok
, а мы затем присвоим это значение файлового дескриптора переменной greeting_file
. После match мы можем использовать дескриптор файла для чтения или записи.
Другая ветвь match обрабатывает случай, где мы получаем значение
Err после вызова
File::open
. В этом примере мы решили вызвать макрос panic!
. Если в нашей текущей директории нет файла с именем hello.txt и мы выполним этот код, то мы увидим следующее сообщение от макроса panic!
:
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
Код в листинге 9-4 будет вызывать panic!
независимо от того, почему вызов
File::open не удался. Однако мы хотим предпринять различные действия для разных причин сбоя.
Если открытие
File::open не удалось из-за отсутствия файла, мы хотим создать файл и вернуть его дескриптор. Если вызов
File::open не удался по любой другой причине - например, потому что у нас не было прав на открытие файла, то все равно мы хотим вызвать panic!
как у нас сделано в листинге 9-4. Для этого мы добавляем выражение внутреннего match
, показанное в листинге 9-5.
Файл: src/main.rs
Листинг 9-5: Обработка различных ошибок разными способами
Типом значения возвращаемого функцией
File::open внутри
Err варианта является io::Error
, структура из стандартной библиотеки. Данная структура имеет метод kind
,
который можно вызвать для получения значения io::ErrorKind
. Перечисление io::ErrorKind из стандартной библиотеки имеет варианты, представляющие различные типы ошибок, которые могут появиться при выполнении операций в io
. Вариант,
$
cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling` thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 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),
}, other_error => { panic!
(
"Problem opening the file: {:?}"
, other_error);
}
},
};
}
который мы хотим использовать, это
ErrorKind::NotFound
, который даёт информацию, о том, что файл который мы пытаемся открыть ещё не существует. Итак, во второй строке мы вызываем сопоставление шаблона с переменной greeting_file_result и попадаем в ветку с обработкой ошибки, но также у нас есть внутренняя проверка для сопоставления error.kind()
ошибки.
Условие, которое мы хотим проверить во внутреннем match
, заключается в том,
является ли значение, возвращаемое error.kind()
, вариантом
NotFound перечисления
ErrorKind
. Если это так, мы пытаемся создать файл с помощью функции
File::create
Однако, поскольку вызов
File::create тоже может завершиться ошибкой, нам нужна обработка ещё одной ошибки, теперь уже во внутреннем выражении match
. Заметьте:
если файл не может быть создан, выводится другое, специализированное сообщение об ошибке. Вторая же ветка внешнего match
(который обрабатывает вызов error.kind()
),
остаётся той же самой - в итоге программа паникует при любой ошибке, кроме ошибки отсутствия файла.
Альтернативы использованию match с Result
Как много match
! Выражение match является очень полезным, но в то же время довольно примитивным. В главе 13 вы узнаете о замыканиях (closures), которые используются во многих методах типа
Result
. Эти методы помогают быть более лаконичным, чем использование match при работе со значениями
ResultE>
в вашем коде.
Например, вот другой способ написать ту же логику, что показана в Листинге 9-5, но с использованием замыканий и метода unwrap_or_else
:
Хотя этот код ведёт себя так же, как и код из листинга 9-5, он не содержит никаких выражений match и его легче читать. После прочтения главы 13 и поищите метод 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);
}
});
}
ErrorKind::NotFound
, который даёт информацию, о том, что файл который мы пытаемся открыть ещё не существует. Итак, во второй строке мы вызываем сопоставление шаблона с переменной greeting_file_result и попадаем в ветку с обработкой ошибки, но также у нас есть внутренняя проверка для сопоставления error.kind()
ошибки.
Условие, которое мы хотим проверить во внутреннем match
, заключается в том,
является ли значение, возвращаемое error.kind()
, вариантом
NotFound перечисления
ErrorKind
. Если это так, мы пытаемся создать файл с помощью функции
File::create
Однако, поскольку вызов
File::create тоже может завершиться ошибкой, нам нужна обработка ещё одной ошибки, теперь уже во внутреннем выражении match
. Заметьте:
если файл не может быть создан, выводится другое, специализированное сообщение об ошибке. Вторая же ветка внешнего match
(который обрабатывает вызов error.kind()
),
остаётся той же самой - в итоге программа паникует при любой ошибке, кроме ошибки отсутствия файла.
Альтернативы использованию match с Result
Как много match
! Выражение match является очень полезным, но в то же время довольно примитивным. В главе 13 вы узнаете о замыканиях (closures), которые используются во многих методах типа
Result
. Эти методы помогают быть более лаконичным, чем использование match при работе со значениями
Result
в вашем коде.
Например, вот другой способ написать ту же логику, что показана в Листинге 9-5, но с использованием замыканий и метода unwrap_or_else
:
Хотя этот код ведёт себя так же, как и код из листинга 9-5, он не содержит никаких выражений match и его легче читать. После прочтения главы 13 и поищите метод 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);
}
});
}
подобных методов могут очистить огромные вложенные выражения match, когда вы имеете дело с ошибками.
Лаконичные способы обработки ошибок - unwrap и expect
Использование match работает достаточно хорошо, но может быть довольно многословным и не всегда хорошо передаёт смысл. Тип
Result
имеет множество вспомогательных методов для выполнения различных, более специфических задач.
Метод unwrap
- это метод быстрого доступа к значениям, реализованный так же, как и выражение match
, которое мы написали в Листинге 9-4. Если значение
Result является вариантом
Ok
, unwrap возвращает значение внутри
Ok
. Если
Result
- вариант
Err
, то unwrap вызовет для нас макрос panic!
. Вот пример unwrap в действии:
Файл: src/main.rs
Если мы запустим этот код при отсутствии файла hello.txt, то увидим сообщение об ошибке из вызова panic!
метода unwrap
:
Другой метод, похожий на unwrap
, это expect
, позволяющий указать сообщение об ошибке для макроса panic!
. Использование expect вместо unwrap с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. Синтаксис метода expect выглядит так:
Файл: src/main.rs expect используется так же как и unwrap
: либо возвращается дескриптор файла либо вызывается макрос panic!
Наше сообщение об ошибке в expect будет передано в panic!
и заменит стандартное use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
).unwrap();
} thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49 use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
)
.expect(
"hello.txt should be included in this project"
);
}
Лаконичные способы обработки ошибок - unwrap и expect
Использование match работает достаточно хорошо, но может быть довольно многословным и не всегда хорошо передаёт смысл. Тип
Result
имеет множество вспомогательных методов для выполнения различных, более специфических задач.
Метод unwrap
- это метод быстрого доступа к значениям, реализованный так же, как и выражение match
, которое мы написали в Листинге 9-4. Если значение
Result является вариантом
Ok
, unwrap возвращает значение внутри
Ok
. Если
Result
- вариант
Err
, то unwrap вызовет для нас макрос panic!
. Вот пример unwrap в действии:
Файл: src/main.rs
Если мы запустим этот код при отсутствии файла hello.txt, то увидим сообщение об ошибке из вызова panic!
метода unwrap
:
Другой метод, похожий на unwrap
, это expect
, позволяющий указать сообщение об ошибке для макроса panic!
. Использование expect вместо unwrap с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. Синтаксис метода expect выглядит так:
Файл: src/main.rs expect используется так же как и unwrap
: либо возвращается дескриптор файла либо вызывается макрос panic!
Наше сообщение об ошибке в expect будет передано в panic!
и заменит стандартное use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
).unwrap();
} thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49 use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
)
.expect(
"hello.txt should be included in this project"
);
}
используемое сообщение.
Вот как это выглядит:
В рабочем коде, большинство выбирает expect в угоду unwrap и добавляет описание,
почему операция должна закончиться успешно. Но даже если предположение оказалось неверным, информации для отладки будет больше.
Проброс ошибок
Когда вы пишете функцию, реализация которой вызывает что-то, что может завершиться ошибкой, вместо обработки ошибки в этой функции, вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что с ней делать. Такой приём известен как
распространение ошибки (propagating the error). Благодаря нему мы даём больше контроля вызывающему коду, где может быть больше информации или логики, которая диктует, как ошибка должна обрабатываться, чем было бы в месте появления этой ошибки.
Например, код программы 9-6 читает имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию.
Файл: src/main.rs
Листинг 9-6: Функция, которая возвращает ошибки в вызывающий код, используя оператор
match thread 'main' panicked at 'hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:10 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),
}
}
Вот как это выглядит:
В рабочем коде, большинство выбирает expect в угоду unwrap и добавляет описание,
почему операция должна закончиться успешно. Но даже если предположение оказалось неверным, информации для отладки будет больше.
Проброс ошибок
Когда вы пишете функцию, реализация которой вызывает что-то, что может завершиться ошибкой, вместо обработки ошибки в этой функции, вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что с ней делать. Такой приём известен как
распространение ошибки (propagating the error). Благодаря нему мы даём больше контроля вызывающему коду, где может быть больше информации или логики, которая диктует, как ошибка должна обрабатываться, чем было бы в месте появления этой ошибки.
Например, код программы 9-6 читает имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию.
Файл: src/main.rs
Листинг 9-6: Функция, которая возвращает ошибки в вызывающий код, используя оператор
match thread 'main' panicked at 'hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:10 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
. Здесь есть возвращаемое значение функции типа
Result
где шаблонный параметр
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, но в этой реализации используется оператор
?
Файл: src/main.rs
Листинг 9-7: Функция, возвращающая ошибки в вызывающий код с помощью оператора
?
Выражение
?
, расположенное после
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<:error> for OurError для создания экземпляра
OurError из io::Error
, то оператор
?
, вызываемый в теле read_username_from_file
, вызовет from и преобразует типы ошибок без необходимости добавления дополнительного кода в функцию.
use std::fs::File; use std::io; use std::io::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)
}
В случае листинга 9-7 оператор
?
в конце вызова
File::open вернёт значение внутри
Ok в переменную username_file
. Если произойдёт ошибка, оператор
?
выполнит ранний возврат значения
Err вызывающему коду. То же самое относится к оператору
?
в конце вызова read_to_string
Оператор
?
позволяет избавиться от большого количества шаблонного кода и упростить реализацию этой функции. Мы могли бы даже ещё больше сократить этот код,
если бы использовали цепочку вызовов методов сразу после
?
, как показано в листинге
9-8.
Файл: src/main.rs
1 ... 16 17 18 19 20 21 22 23 ... 62
Листинг 9-8: Цепочка вызовов методов после оператора
?
Мы перенесли создание новой
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
демонстрирует способ сделать её ещё короче.
Файл: src/main.rs
Листинг 9-9: Использование
fs::read_to_string
вместо открытия и последующего чтения файла
use std::fs::File; use std::io; use std::io::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)
} 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
В листинге 9-10 давайте посмотрим на ошибку, которую мы получим, если воспользуемся
?
оператор в main функции с типом возвращаемого значения, несовместимым с типом используемого нами значения
?
на:
Файл : src/main.rs
Листинг 9-10: Попытка использовать
?
в
main
функции, которая возвращает
()
, не будет
компилироваться
Этот код открывает файл, что может привести к сбою.
?
оператор следует за значением
Result
, возвращаемым
File::open
, но эта main функция имеет возвращаемый тип
()
, а не
Result
. Когда мы компилируем этот код, мы получаем следующее сообщение об ошибке:
use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
)?;
}
Эта ошибка указывает на то, что оператор
?
разрешено использовать только в функции,
которая возвращает
Result
,
Option или другой тип, реализующий
FromResidual
Для исправления ошибки есть два варианта. Первый - изменить возвращаемый тип вашей функции так, чтобы он был совместим со значением, для которого вы используете оператор
?
, если у вас нет ограничений, препятствующих этому. Другой способ - использовать match или один из методов
Result
для обработки
Result
любым подходящим способом.
Эта ошибка указывает на то, что оператор
?
разрешено использовать только в функции,
которая возвращает
Result
,
Option или другой тип, который реализует
FromResidual
Чтобы исправить ошибку, есть два варианта. Первый - изменить тип возвращаемый из функции, чтобы он был совместим со значением, для которого вы используете оператор
?
, если у вас нет ограничений, препятствующих этому. Второй заключается в использовании match или одного из методов
Result
для обработки
Result
любым подходящим способом.
Listing 9-11: Using the
?
operator on an
Option
value
Эта функция возвращает
Option
, потому что возможно, что там есть символ, но также возможно, что его нет. Этот код принимает аргумент среза text строки и вызывает для него метод lines
, который возвращает итератор для строк в строке.
Поскольку эта функция хочет проверить первую строку, она вызывает next у итератора,
чтобы получить первое значение от итератора. Если text является пустой строкой, этот вызов next вернёт
None
, и в этом случае мы используем
?
чтобы остановить и вернуть
$
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() {
4 | | let greeting_file = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual
For more information about this error, try `rustc --explain E0277`. error: could not compile `error-handling` due to previous error fn last_char_of_first_line
(text: &
str
) ->
Option
<
char
> { text.lines().next()?.chars().last()
}
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
, чтобы выполнить преобразование явно.
Обратите внимание, что вы можете использовать оператор
?
на
Result в функции,
которая возвращает
Result
, и вы можете использовать оператор
?
на
Option в
функции, которая возвращает
Option
, но вы не можете смешивать и сочетать их.
Оператор
?
не будет автоматически преобразовывать
Result в
Option или наоборот; в этих случаях, вы можете использовать такие методы, как ok из
Result или ok_or из
Option для явного преобразования.
К счастью, main также может возвращать
Result<(), E>
. В листинге 9-12 используется код из листинга 9-10, но мы изменили возвращаемый тип main на
Result<(), Box
и добавили возвращаемое значение
Ok(())
в конец. Теперь этот код будет скомпилирован:
Листинг 9-12: Замена
main
на return
Result<(), E>
позволяет использовать оператор
?
оператор над
значениями
Result
Тип
Box
является трейт-объектом, о котором мы поговорим в разделе "Использование трейт-объектов, допускающих значения разных типов"
в главе 17. Пока use std::error::Error; use std::fs::File; fn main
() ->
Result
<(),
Box
<
dyn
Error>> { let greeting_file = File::open(
"hello.txt"
)?;
Ok
(())
}
что вы можете считать, что
Box
означает "любой вид ошибки".
Использование
?
для значения
Result в функции main с типом ошибки
Box
разрешено, так как позволяет вернуть любое значение
Err раньше времени. Даже если тело этой функции main будет возвращать только ошибки типа std::io::Error
, указав
Box
, эта сигнатура останется корректной, даже если в тело main будет добавлен код, возвращающий другие ошибки.
Когда main функция возвращает
Result<(), E>
, исполняемый файл завершится со значением
0
, если main вернёт
Ok(())
, и выйдет с ненулевым значением, если main вернёт значение
Err
. Исполняемые файлы, написанные на C, при выходе возвращают целые числа: успешно завершённые программы возвращают целое число
0
, а программы с ошибкой возвращают целое число, отличное от
0
. Rust также возвращает целые числа из исполняемых файлов, чтобы быть совместимым с этим соглашением.
Функция main может возвращать любые типы, реализующие трейт std::process::Termination
, в которых имеется функция report
, возвращающая
ExitCode
. Обратитесь к документации стандартной библиотеки за дополнительной информацией о порядке реализации трейта
Termination для ваших собственных типов.
Теперь, когда мы обсудили детали вызова panic!
или возврата
Result
, давайте вернёмся к тому, как решить, какой из случаев подходит для какой ситуации.
Box
означает "любой вид ошибки".
Использование
?
для значения
Result в функции main с типом ошибки
Box
разрешено, так как позволяет вернуть любое значение
Err раньше времени. Даже если тело этой функции main будет возвращать только ошибки типа std::io::Error
, указав
Box
, эта сигнатура останется корректной, даже если в тело main будет добавлен код, возвращающий другие ошибки.
Когда main функция возвращает
Result<(), E>
, исполняемый файл завершится со значением
0
, если main вернёт
Ok(())
, и выйдет с ненулевым значением, если main вернёт значение
Err
. Исполняемые файлы, написанные на C, при выходе возвращают целые числа: успешно завершённые программы возвращают целое число
0
, а программы с ошибкой возвращают целое число, отличное от
0
. Rust также возвращает целые числа из исполняемых файлов, чтобы быть совместимым с этим соглашением.
Функция main может возвращать любые типы, реализующие трейт std::process::Termination
, в которых имеется функция report
, возвращающая
ExitCode
. Обратитесь к документации стандартной библиотеки за дополнительной информацией о порядке реализации трейта
Termination для ваших собственных типов.
Теперь, когда мы обсудили детали вызова panic!
или возврата
Result
, давайте вернёмся к тому, как решить, какой из случаев подходит для какой ситуации.
panic!
или не panic!
Итак, как принимается решение о том, когда следует вызывать panic!
, а когда вернуть
Result
? При панике код не имеет возможности восстановить своё выполнение. Можно было бы вызывать panic!
для любой ошибочной ситуации, независимо от того, имеется ли способ восстановления или нет, но с другой стороны, вы принимаете решение от имени вызывающего вас кода, что ситуация необратима. Когда вы возвращаете значение
Result
, вы делегируете принятие решения вызывающему коду. Вызывающий код может попытаться выполнить восстановление способом, который подходит в данной ситуации, или же он может решить, что из ошибки в
Err нельзя восстановиться и вызовет panic!
, превратив вашу исправимую ошибку в неисправимую. Поэтому возвращение
Result является хорошим выбором по умолчанию для функции, которая может дать сбой.
В таких ситуация как примеры, прототипы и тесты, более уместно писать код, который паникует вместо возвращения
Result
. Давайте рассмотрим почему, а затем мы обсудим ситуации, в которых компилятор не может доказать, что ошибка невозможна, но вы, как человек, можете это сделать. Глава будет заканчиваться некоторыми общими руководящими принципами о том, как решить, стоит ли паниковать в коде библиотеки.
Примеры, прототипирование и тесты
Когда вы пишете пример, иллюстрирующий некоторую концепцию, наличие хорошего кода обработки ошибок может сделать пример менее понятным. Понятно, что в примерах вызов метода unwrap
, который может привести к панике, является лишь обозначением способа обработки ошибок в приложении, который может отличаться в зависимости от того, что делает остальная часть кода.
Точно так же методы unwrap и expect являются очень удобными при создании прототипа, прежде чем вы будете готовы решить, как обрабатывать ошибки. Они оставляют чёткие маркеры в коде до момента, когда вы будете готовы сделать программу более надёжной.
Если в тесте происходит сбой при вызове метода, то вы бы хотели, чтобы весь тест не прошёл, даже если этот метод не является тестируемой функциональностью. Поскольку вызов panic!
это способ, которым тест помечается как провалившийся, использование unwrap или expect
- именно то, что нужно.
Случаи, в которых у вас больше информации, чем у компилятора
Также было бы целесообразно вызывать unwrap или expect когда у вас есть какая-то другая логика, которая гарантирует, что
Result будет иметь значение
Ok
, но вашу логику не понимает компилятор. У вас по-прежнему будет значение
Result которое
или не panic!
Итак, как принимается решение о том, когда следует вызывать panic!
, а когда вернуть
Result
? При панике код не имеет возможности восстановить своё выполнение. Можно было бы вызывать panic!
для любой ошибочной ситуации, независимо от того, имеется ли способ восстановления или нет, но с другой стороны, вы принимаете решение от имени вызывающего вас кода, что ситуация необратима. Когда вы возвращаете значение
Result
, вы делегируете принятие решения вызывающему коду. Вызывающий код может попытаться выполнить восстановление способом, который подходит в данной ситуации, или же он может решить, что из ошибки в
Err нельзя восстановиться и вызовет panic!
, превратив вашу исправимую ошибку в неисправимую. Поэтому возвращение
Result является хорошим выбором по умолчанию для функции, которая может дать сбой.
В таких ситуация как примеры, прототипы и тесты, более уместно писать код, который паникует вместо возвращения
Result
. Давайте рассмотрим почему, а затем мы обсудим ситуации, в которых компилятор не может доказать, что ошибка невозможна, но вы, как человек, можете это сделать. Глава будет заканчиваться некоторыми общими руководящими принципами о том, как решить, стоит ли паниковать в коде библиотеки.
Примеры, прототипирование и тесты
Когда вы пишете пример, иллюстрирующий некоторую концепцию, наличие хорошего кода обработки ошибок может сделать пример менее понятным. Понятно, что в примерах вызов метода unwrap
, который может привести к панике, является лишь обозначением способа обработки ошибок в приложении, который может отличаться в зависимости от того, что делает остальная часть кода.
Точно так же методы unwrap и expect являются очень удобными при создании прототипа, прежде чем вы будете готовы решить, как обрабатывать ошибки. Они оставляют чёткие маркеры в коде до момента, когда вы будете готовы сделать программу более надёжной.
Если в тесте происходит сбой при вызове метода, то вы бы хотели, чтобы весь тест не прошёл, даже если этот метод не является тестируемой функциональностью. Поскольку вызов panic!
это способ, которым тест помечается как провалившийся, использование unwrap или expect
- именно то, что нужно.
Случаи, в которых у вас больше информации, чем у компилятора
Также было бы целесообразно вызывать unwrap или expect когда у вас есть какая-то другая логика, которая гарантирует, что
Result будет иметь значение
Ok
, но вашу логику не понимает компилятор. У вас по-прежнему будет значение
Result которое
нужно обработать: любая операция, которую вы вызываете, все ещё имеет возможность неудачи в целом, хотя это логически невозможно в вашей конкретной ситуации. Если,
проверяя код вручную, вы можете убедиться, что никогда не будет вариант с
Err
, то вполне допустимо вызывать unwrap
, а ещё лучше задокументировать причину, по которой, по вашему мнению, у вас никогда не будет варианта
Err в тексте expect
. Вот пример:
Мы создаём экземпляр
IpAddr
, анализируя жёстко закодированную строку. Можно увидеть, что
127.0.0.1
является действительным IP-адресом, поэтому здесь допустимо использование expect
. Однако наличие жёстко закодированной допустимой строки не меняет тип возвращаемого значения метода parse
: мы все ещё получаем значение
Result и компилятор все также заставляет нас обращаться с
Result так, будто возможен вариант
Err
, потому что компилятор недостаточно умён, чтобы увидеть, что эта строка всегда действительный IP-адрес. Если строка IP-адреса пришла от пользователя, то она не является жёстко запрограммированной в программе и, следовательно, может
привести к ошибке, мы определённо хотели бы обработать
Result более надёжным способом. Упоминание предположения о том, что этот IP-адрес жёстко закодирован,
побудит нас изменить expect для лучшей обработки ошибок, если в будущем нам потребуется вместо этого получить IP-адрес из какого-либо другого источника.
Руководство по обработке ошибок
Желательно, чтобы код паниковал, если он может оказаться в некорректном состоянии. В
этом контексте некорректное состояние это когда некоторое допущение, гарантия,
контракт или инвариант были нарушены. Например, когда недопустимые,
противоречивые или пропущенные значения передаются в ваш код - плюс один или несколько пунктов из следующего перечисленного в списке:
Не корректное состояние — это что-то неожиданное, отличается от того, что может происходить время от времени, например, когда пользователь вводит данные в неправильном формате.
Ваш код после этой точки должен полагаться на то, что он не находится в не корректном состоянии, вместо проверок наличия проблемы на каждом этапе.
Нет хорошего способа закодировать данную информацию в типах, которые вы используете. Мы рассмотрим пример того, что мы имеем в виду в разделе
“Кодирование состояний и поведения на основе типов”
главы 17.
Если кто-то вызывает ваш код и передаёт значения, которые не имеют смысла, лучше всего вернуть ошибку, если вы это можете, чтобы пользователь библиотеки мог решить,
use std::net::IpAddr; let home: IpAddr =
"127.0.0.1"
.parse()
.expect(
"Hardcoded IP address should be valid"
);
проверяя код вручную, вы можете убедиться, что никогда не будет вариант с
Err
, то вполне допустимо вызывать unwrap
, а ещё лучше задокументировать причину, по которой, по вашему мнению, у вас никогда не будет варианта
Err в тексте expect
. Вот пример:
Мы создаём экземпляр
IpAddr
, анализируя жёстко закодированную строку. Можно увидеть, что
127.0.0.1
является действительным IP-адресом, поэтому здесь допустимо использование expect
. Однако наличие жёстко закодированной допустимой строки не меняет тип возвращаемого значения метода parse
: мы все ещё получаем значение
Result и компилятор все также заставляет нас обращаться с
Result так, будто возможен вариант
Err
, потому что компилятор недостаточно умён, чтобы увидеть, что эта строка всегда действительный IP-адрес. Если строка IP-адреса пришла от пользователя, то она не является жёстко запрограммированной в программе и, следовательно, может
привести к ошибке, мы определённо хотели бы обработать
Result более надёжным способом. Упоминание предположения о том, что этот IP-адрес жёстко закодирован,
побудит нас изменить expect для лучшей обработки ошибок, если в будущем нам потребуется вместо этого получить IP-адрес из какого-либо другого источника.
Руководство по обработке ошибок
Желательно, чтобы код паниковал, если он может оказаться в некорректном состоянии. В
этом контексте некорректное состояние это когда некоторое допущение, гарантия,
контракт или инвариант были нарушены. Например, когда недопустимые,
противоречивые или пропущенные значения передаются в ваш код - плюс один или несколько пунктов из следующего перечисленного в списке:
Не корректное состояние — это что-то неожиданное, отличается от того, что может происходить время от времени, например, когда пользователь вводит данные в неправильном формате.
Ваш код после этой точки должен полагаться на то, что он не находится в не корректном состоянии, вместо проверок наличия проблемы на каждом этапе.
Нет хорошего способа закодировать данную информацию в типах, которые вы используете. Мы рассмотрим пример того, что мы имеем в виду в разделе
“Кодирование состояний и поведения на основе типов”
главы 17.
Если кто-то вызывает ваш код и передаёт значения, которые не имеют смысла, лучше всего вернуть ошибку, если вы это можете, чтобы пользователь библиотеки мог решить,
use std::net::IpAddr; let home: IpAddr =
"127.0.0.1"
.parse()
.expect(
"Hardcoded IP address should be valid"
);