Like many programming languages, Rust encourages the developer to handle errors in a certain way. In general, there are two general error handling approaches: using exceptions and via return values. And Rust prefers return values.
In this article we intend to detail the work with errors in Rust. Moreover, we will try to immerse ourselves in error handling from various angles time after time, so that at the end you will have a confident practical idea of ​​how all this fits together.
In a naive implementation, error handling in Rust can look verbose and annoying. We will consider the main stumbling blocks, as well as demonstrate how to make error handling concise and convenient using the standard library.
This article is very long, mainly because we start from the very beginning - considering the types of sums (sum type) and combinators, and then try to consistently explain the Rust approach to error handling. So developers who have experience with other expressive type systems can freely jump from section to section.
Error handling can be viewed as a varied analysis of whether some calculation was performed successfully or not. As will be shown later, the key to the convenience of error handling is to reduce the amount of explicit variable analysis that the developer must perform, while keeping the code easily compatible with other code (composability).
(Translator's note: Variable analysis is one of the most commonly used methods of analytical thinking, which is to consider a problem, question, or some situation from the point of view of each possible specific case. At the same time, considering each such case separately is sufficient to solve the initial question.
An important aspect of this approach to solving problems is that such an analysis should be exhaustive. In other words, when using a variance analysis, all possible cases should be considered.
In Rust, variable analysis is implemented using the syntactic construction of match
. At the same time, the compiler guarantees that such analysis will be exhaustive: if the developer does not consider all possible options for the specified value, the program will not be compiled.)
Preserving the compatibility of the code is important, because without this requirement we could just get panic
whenever we come across something unexpected. ( panic
causes the current thread to interrupt and, in most cases, terminates the entire program.) Here is an example:
// 1 10. // , , true. // false. fn guess(n: i32) -> bool { if n < 1 || n > 10 { panic!(" : {}", n); } n == 5 } fn main() { guess(11); }
If you try to run this code, the program will crash with a message like this:
thread '<main>' panicked at ' : 11', src/bin/panic-simple.rs:6
Here is another, less contrived example. A program that takes a number as an argument doubles its value and prints on the screen.
use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); // 1 let n: i32 = arg.parse().unwrap(); // 2 println!("{}", 2 * n); }
If you run this program without parameters (error 1) or if the first parameter is not an integer number (error 2), the program will end in panic, just as in the first example.
Handling errors in a similar style is like an elephant in a china shop. The elephant will rush in the direction in which he wants, and destroy everything in its path.
unwrap
In the previous example, we stated that the program would simply panic if one of the two conditions for the occurrence of an error is fulfilled, although, unlike the first example, there is no explicit panic
call in the program code. However, the panic
call is built into the unwrap
call.
unwrap
in Rust is like saying: “Give me the result of the calculations, and if an error occurs, just panic and stop the program.” We could just show the source code of the unwrap
function, because it’s pretty simple, but before that we have to deal with the types Option
and Result
. Both of these types have an unwrap
method defined for them.
Option
typeOption
type is declared in the standard library :
enum Option<T> { None, Some(T), }
Option
type is a way to express the possibility of the absence of anything using the Rust type system. Expression of the possibility of absence through a type system is an important concept, since such an approach allows the compiler to require the developer to handle such an absence. Let's take a look at an example that tries to find a character in a string:
// Unicode- `needle` `haystack`. , // . `None`. fn find(haystack: &str, needle: char) -> Option<usize> { for (offset, c) in haystack.char_indices() { if c == needle { return Some(offset); } } None }
Note that when this function finds the corresponding character, it returns not just offset
. Instead, it returns Some(offset)
. Some
is a variant or constructor of a value for type Option
. It can be interpreted as a function of type fn<T>(value: T) -> Option<T>
. Accordingly, None
is also a value constructor, only it has no parameters. It can be interpreted as a function of type fn<T>() -> Option<T>
.
It may seem that we made a lot of noise out of nothing, but this is only half the story. The second half is the use of the find
function we wrote. Let's try to use it to find the extension in the file name.
fn main() { let file_name = "foobar.rs"; match find(file_name, '.') { None => println!(" ."), Some(i) => println!(" : {}", &file_name[i+1..]), } }
This code uses pattern matching to perform variable analysis for the Option<usize>
value returned by the find
function. In fact, variable analysis is the only way to get to the value stored inside Option<T>
. This means that you, as a developer, are required to handle the case when the Option<T>
value is None
, not Some(t)
.
But wait, what about the unwrap
we used ? There was no variable analysis! Instead, the variable analysis was moved inside the
unwrap
method. You can do it yourself if you want:
enum Option<T> { None, Some(T), } impl<T> Option<T> { fn unwrap(self) -> T { match self { Option::Some(val) => val, Option::None => panic!("called `Option::unwrap()` on a `None` value"), } } }
The unwrap
method abstracts variable analysis . This is exactly what makes unwrap
convenient to use. Sorry, panic!
means that unwrap
inconvenient to combine with another code: it is an elephant in a china shop.
Option<T>
In the previous example, we looked at how you can use find
to get the file name extension. Of course, not all file names can be found .
, so there is a possibility that the name of some file does not have an extension. This absence feature is interpreted at the type level through the use of Option<T>
. In other words, the compiler will force us to consider the possibility that the extension does not exist. In our case, we just type a message about it.
Getting the file name extension is a fairly common operation, so it makes sense to put the code into a separate function:
// , , // `.` . // `file_name` `.`, `None`. fn extension_explicit(file_name: &str) -> Option<&str> { match find(file_name, '.') { None => None, Some(i) => Some(&file_name[i+1..]), } }
(Hint: do not use this code. Instead, use the extension
method from the standard library.)
The code looks simple, but its important aspect is that the find
function makes us consider the probability of missing values. This is good, because it means that the compiler will not allow us to accidentally forget about the option when the extension is missing in the file name. On the other hand, every time performing an explicitly variable analysis, just as we did in extension_explicit
, can become a bit tedious.
In fact, variable analysis in extension_explicit
is a very common pattern: if Option<T>
has a certain T
value, then convert it using a function, and if not, just return None
.
Rust supports parametric polymorphism, so it is very easy to declare a combinator that abstracts this behavior:
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A { match option { None => None, Some(value) => Some(f(value)), } }
In fact, the map
defined in the standard library as the Option<T>
method.
Armed with our new combinator, we can rewrite our extension_explicit
method to get rid of the variative analysis:
// , , // `.` . // `file_name` `.`, `None`. fn extension(file_name: &str) -> Option<&str> { find(file_name, '.').map(|i| &file_name[i+1..]) }
There is one more behavior that can often be met - this is the use of the default value in the case where the Option
value is None
. For example, your program may consider that the file extension is rs
if it is actually missing.
It is easy to imagine that this case of alternative analysis is not specific only to file extensions - this approach can work with any Option<T>
:
fn unwrap_or<T>(option: Option<T>, default: T) -> T { match option { None => default, Some(value) => value, } }
The trick is that the default value should be of the same type as the value that can be inside Option<T>
. Using this method is elementary:
fn main() { assert_eq!(extension("foobar.csv").unwrap_or("rs"), "csv"); assert_eq!(extension("foobar").unwrap_or("rs"), "rs"); }
(Note that unwrap_or
declared as an Option<T>
method in the standard library, so we used it instead of the function we declared earlier. Don't forget to also study the more general unwrap_or_else
method).
There is one more combinator that we think is worth paying special attention to: and_then
. It makes it easy to combine various calculations that allow for the possibility of absence . An example is most of the code in this section that is associated with the definition of the extension of a given file name. To do this, we first need to know the name of the file, which is usually extracted from the file path . Although most file paths contain a file name, this is not the case with all file paths. An example is the way .
..
or /
.
Thus, we have defined the task of finding the extension of a given file path . Let's start with an explicit variable analysis:
fn file_path_ext_explicit(file_path: &str) -> Option<&str> { match file_name(file_path) { None => None, Some(name) => match extension(name) { None => None, Some(ext) => Some(ext), } } } fn file_name(file_path: &str) -> Option<&str> { unimplemented!() // }
You might think we could just use a map
combinator to reduce variable analysis, but its type does not quite fit. The fact is that map
accepts a function that only does something with an internal value. The result of such a function always Some
. Instead, we need a method similar to map
, but which allows the caller to pass another Option
. Its general implementation is even simpler than the map
:
fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> Option<A> { match option { None => None, Some(value) => f(value), } }
Now we can rewrite our function file_path_ext
without explicit variable analysis:
fn file_path_ext(file_path: &str) -> Option<&str> { file_name(file_path).and_then(extension) }
Option
type has many other combinators defined in the standard library . It is very useful to review this list and familiarize yourself with the available methods - they will often help you reduce the number of variable analysis. Familiarization with these combinators will pay off also because many of them are defined with similar semantics for the Result
type, which we will discuss later.
Combinators make use of types like Option
more convenient, because they reduce the explicit variable analysis. They also meet the compatibility requirements, as they allow the caller to handle the possibility of no result in their own way. Methods such as unwrap
it impossible, because they will panic when Option<T>
is None
.
Result
typeThe type Result
also defined in the standard library :
enum Result<T, E> { Ok(T), Err(E), }
Type Result
is an advanced version of Option
. Instead of expressing the possibility of absence , as Option
does, Result
expresses the possibility of error . As a rule, errors are necessary to explain why the result of a particular calculation was not obtained. Strictly speaking, this is a more general form of Option
. Consider the following type alias, which in all senses is semantically equivalent to the real Option<T>
:
type Option<T> = Result<T, ()>;
Here, the second parameter of the Result
type Result
fixed and defined by ()
(pronounced as “unit” or “empty tuple”). Type ()
has exactly one value - ()
. (Yes, this is the type and value of this type, which look the same!)
The Result
type is a way to express one of two possible outcomes of a calculation. By convention, one outcome means the expected result or " Ok
", while the other outcome means an exceptional situation or " Err
".
Like Option
, the Result
type has an unwrap
method defined in the standard library . Let's announce it by ourselves:
impl<T, E: ::std::fmt::Debug> Result<T, E> { fn unwrap(self) -> T { match self { Result::Ok(val) => val, Result::Err(err) => panic!("called `Result::unwrap()` on an `Err` value: {:?}", err), } } }
This is actually the same as the Option::unwrap
, except that we added the error value to the panic!
message panic!
. This makes debugging easier, but it forces us to require the parameter type E
(which represents our type of error) to implement the Debug
. Since the vast majority of types must implement Debug
, usually in practice this restriction does not interfere. (Implementing a Debug
for some type simply means that there is a reasonable way to print a readable description of the value of this type.)
OK, let's go to the example.
The standard library Rust allows elementary to convert strings to integers. In fact, it is so simple that it is tempting to write something like:
fn double_number(number_str: &str) -> i32 { 2 * number_str.parse::<i32>().unwrap() } fn main() { let n: i32 = double_number("10"); assert_eq!(n, 20); }
Here you should be skeptical about calling unwrap
. If the string cannot be parsed as a number, you will get a panic:
thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', /home/rustbuild/src/rust-buildbot/slave/beta-dist-rustc-linux/build/src/libcore/result.rs:729
This is pretty unpleasant, and if something like this happened in the library you use, you might be reasonably angry. So we should try to handle the error in our function, and let the caller decide what to do with it. This means the need to change the type that is returned by double_number
. But which one? To understand this, you need to look at the signature of parse
from the standard library:
impl str { fn parse<F: FromStr>(&self) -> Result<F, F::Err>; }
Hmm At least we know that we must use Result
. It is possible that the method could return Option
. In the end, the string is either parsed as a number or not, is it? This is, of course, a reasonable way, but the internal implementation knows why the string did not split as an integer. (This may be an empty string, or incorrect numbers, too large or too short a length, etc.) Thus, using Result
makes sense, because we want to provide more information than just “absence”. We want to say why the conversion failed. You should think in a similar way when faced with the choice between Option
and Result
. If you can provide detailed error information, then you probably should. (We'll talk more about this later.)
Good, but how do we write our return type? The parse
method is generic for all different types of numbers from the standard library. We could (and probably should) also make our function generalized, but for now let's dwell on a specific implementation. We are only interested in the i32
type, so we should FromStr
(do a search in your browser for the string “FromStr”) and look at its associated type Err
. We do this to determine the specific type of error. In this case, this is std::num::ParseIntError
. Finally, we can rewrite our function:
use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { match number_str.parse::<i32>() { Ok(n) => Ok(2 * n), Err(err) => Err(err), } } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } }
Not bad, but we had to write a lot more code! And we are again annoyed by the variable analysis.
Combinators rush to the rescue! Like Option
, Result
has many combinators defined as methods. There is a large list of combinators common between Result
and Option
. And the map
is included in this list:
use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { number_str.parse::<i32>().map(|n| 2 * n) } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } }
All expected methods are implemented for Result
, including unwrap_or
and and_then
. In addition, since Result
has a second type parameter, there are combinators that only affect the error value, such as map_err
(analog map
) and or_else
(analog and_then
).
Result
nicknameIn the standard library, you can often see types like Result<i32>
. But wait, since Result
with two type parameters. How can we get around this by pointing out only one of them? The answer lies in the definition of an alias of the Result
type, which fixes one of the parameters with a specific type. The type of error is usually fixed. For example, our previous example with converting strings to numbers can be rewritten as:
use std::num::ParseIntError; use std::result; type Result<T> = result::Result<T, ParseIntError>; fn double_number(number_str: &str) -> Result<i32> { unimplemented!(); }
Why do we do this? Well, if we have a lot of functions that can return ParseIntError
, then it is much more convenient to define an alias that always uses ParseIntError
, so we will not repeat all the time.
The most notable use of this approach in the standard library is the alias io::Result
. As a rule, it is enough to write io::Result<T>
to make it clear that you are using a type alias from the io
module, and not the usual definition from std::result
. (This approach is also used for fmt::Result
)
unwrap
is not necessarily evilIf you were attentive, you may have noticed that I took a rather tough stance against methods like unwrap
that can cause panic
and interrupt the execution of your program. Basically , this is good advice.
Nevertheless, unwrap
can still be used wisely. The factors that justify the use of unwrap
are somewhat vague, and reasonable people may disagree with me. I will summarize my opinion on this issue:
unwrap
, so here its use is very attractive.panic
also acceptable. The fact is that in this case, a panic will report a bug in your program. This can occur explicitly, for example from an unsuccessful call assert!
, or occur because the array index is outside the allocated memory.This is probably not an exhaustive list. In addition, when using Option
often better to use the expect
method. This method does exactly the same as unwrap
, except that in case of a panic it will print your message. This will allow you to better understand the cause of the error, because a specific message will be displayed, and not just “called unwrap on a None
value”.
My advice comes down to this: use common sense. There are reasons why words like “never do X” or “Y is considered harmful” will not appear in this article. , , , , . , .
, Rust unwrap
, .
, Option<T>
, Result<T, SomeError>
. , Option
, Result
? Result<T, Error1>
Result<T, Error2>
? — , .
Option
Result
, Option
, , Result
. , , .
, . Option
Result
. , ?
:
use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); // 1 let n: i32 = arg.parse().unwrap(); // 2 println!("{}", 2 * n); }
Option
Result
, , , , .
, argv.nth(1)
Option
, arg.parse()
Result
. . Option
Result
, — Option
Result
. , ( env::args()
) , . String
. Let's try:
use std::env; fn double_arg(mut argv: env::Args) -> Result<i32, String> { argv.nth(1) .ok_or("Please give at least one argument".to_owned()) .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string())) } fn main() { match double_arg(env::args()) { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } }
c . -, Option::ok_or
. Option
Result
. , , Option
None
. , , :
fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> { match option { Some(val) => Ok(val), None => Err(err), } }
, — Result::map_err
. , Result::map
, , Result
. Result
k(...)
, .
map_err
, (- and_then
). Option<String>
( argv.nth(1)
) Result<String, String>
, ParseIntError
arg.parse()
String
.
IO — , , Rust. IO .
. , . 2
.
unwrap
, unwrap
. , , , . , , .
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> i32 { let mut file = File::open(file_path).unwrap(); // 1 let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); // 2 let n: i32 = contents.trim().parse().unwrap(); // 3 2 * n } fn main() { let doubled = file_double("foobar"); println!("{}", doubled); }
(: AsRef
, std::fs::File::open
. .)
, :
std::io::Error
. std::fs::File::open
std::io::Read::read_to_string
. ( , Result
, . Result
, , , io::Error
.) std::num::ParseIntError
. , io::Error
. .
file_double
. , , , - . , , , . , i32
, . , i32
- .
, : : Option
Result
? , , Option
. - , None
. , , , . , . , Result<i32, E>
. E
? , . String
. , :
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { File::open(file_path) .map_err(|err| err.to_string()) .and_then(|mut file| { let mut contents = String::new(); file.read_to_string(&mut contents) .map_err(|err| err.to_string()) .map(|_| contents) }) .and_then(|contents| { contents.trim().parse::<i32>() .map_err(|err| err.to_string()) }) .map(|n| 2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {}", err), } }
. , . . file_double
Result<i32, String>
, . : and_then
, map
map_err
.
and_then
, . , : . , and_then
.
map
, Ok(...)
Result
. , , map
Ok(...)
( i32
) 2
. , . map
.
map_err
— , . , , map
, , Err(...)
Result
. — String
. io::Error
, num::ParseIntError
ToString
, to_string
, .
, - . , . : .
return
. return
. return
file_double
, .
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = match File::open(file_path) { Ok(file) => file, Err(err) => return Err(err.to_string()), }; let mut contents = String::new(); if let Err(err) = file.read_to_string(&mut contents) { return Err(err.to_string()); } let n: i32 = match contents.trim().parse() { Ok(n) => n, Err(err) => return Err(err.to_string()), }; Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {}", err), } }
- , , , , , , . match
if let
. , ( ).
? , — , , . , . — .
try!
Rust — try!
. , , , . , , .
`try!:
macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); }
try!
. , , :
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {}", err), } }
map_err
- , try!
, String
. , , map_err
! , - .
, , String
.
String
, , , . , String
.
, String
. , , , . , String
— . , , , . (, , ).
, io::Error
io::ErrorKind
, , , -. , - . (, BrokenPipe
, NotFound
.) io::ErrorKind
, , String
.
, String
, , . , .
- enum
. , io::Error
, num::ParseIntError
, :
use std::io; use std::num; // `Debug` , , `Debug`. // CliError #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), }
. , CliError
, :
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {:?}", err), } }
— map_err(|e| e.to_string())
( ) map_err(CliError::Io)
map_err(CliError::Parse)
. . , String
, enum
, CliError
, , , , .
, , String
, . , . , .
, std::error::Error
std::convert::From
. Error
, From
.
Error
use std::fmt::{Debug, Display}; trait Error: Debug + Display { /// A short description of the error. fn description(&self) -> &str; /// The lower level cause of this error, if any. fn cause(&self) -> Option<&Error> { None } }
, , , . , . , , :
Debug
).Display
).description
).cause
)., Error
Debug
Display
. , Error
. rror
, , - (trait object). Box<Error>
, &Error
. , cause
&Error
, -. Error
-.
use std::io; use std::num; // `Debug` , , `Debug`. // CliError #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), }
: I . , , enum
.
Error
:
use std::error; use std::fmt; impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { // `Display`, // CliError::Io(ref err) => write!(f, "IO error: {}", err), CliError::Parse(ref err) => write!(f, "Parse error: {}", err), } } } impl error::Error for CliError { fn description(&self) -> &str { // `Error`, // match *self { CliError::Io(ref err) => err.description(), CliError::Parse(ref err) => err.description(), } } fn cause(&self) -> Option<&error::Error> { match *self { // `err` // (`&io::Error` `&num::ParseIntError`) // - `&Error`. `Error`. CliError::Io(ref err) => Some(err), CliError::Parse(ref err) => Some(err), } } }
, Error
: description
cause
.
From
trait From<T> { fn from(T) -> Self; }
, ? From
, - ( , « » , ,
Self
). From
— , .
, From
:
let string: String = From::from("foo"); let bytes: Vec<u8> = From::from("foo"); let cow: ::std::borrow::Cow<str> = From::from("foo");
, From
. ? , :
impl<'a, E: Error + 'a> From<E> for Box<Error + 'a>
, , Error
, - Box<Error>
. , .
, , , io::Error
and num::ParseIntError
? Error
, From
:
use std::error::Error; use std::fs; use std::io; use std::num; // let io_err: io::Error = io::Error::last_os_error(); let parse_err: num::ParseIntError = "not a number".parse::<i32>().unwrap_err(); // , let err1: Box<Error> = From::from(io_err); let err2: Box<Error> = From::from(parse_err);
. err1
err2
— -. , , err1
err2
. , err1
err2
, — From::from
. , From::from
.
, , , .
— try!
.
try!
try!
:
macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); }
macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); }
, : From::from
. try!
, .
try!
, , , :
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) }
, map_err
. , — , From
. , From
, Box<Error>
:
use std::error::Error; use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n = try!(contents.trim().parse::<i32>()); Ok(2 * n) }
. - , try!
:
, , , unwrap
.
: Box<Error>
. Box<Error>
, () . , , , String
, , description
cause
, : Box<Error>
. (: , Rust , , ).
CliError
.
try!
, From::from
. Box<Error>
, , .
, , : . , :
use std::fs::File; use std::io::{self, Read}; use std::num; use std::path::Path; // `Debug` , , `Debug`. // CliError #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) }
, map_err
. Why? try!
From
. , From
, io::Error
num::ParseIntError
CliError
. ! CliError
, From
:
use std::io; use std::num; impl From<io::Error> for CliError { fn from(err: io::Error) -> CliError { CliError::Io(err) } } impl From<num::ParseIntError> for CliError { fn from(err: num::ParseIntError) -> CliError { CliError::Parse(err) } }
From
CliError
. . , .
, file_double
:
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n: i32 = try!(contents.trim().parse()); Ok(2 * n) }
, — map_err
. , try!
From::from
. , From
, .
file_double
, - , , , :
use std::io; use std::num; enum CliError { Io(io::Error), ParseInt(num::ParseIntError), ParseFloat(num::ParseFloatError), }
From
:
use std::num; impl From<num::ParseFloatError> for CliError { fn from(err: num::ParseFloatError) -> CliError { CliError::ParseFloat(err) } }
That's all!
, . ( ErrorKind
), ( ParseIntError
). , , . , , .
, Error
. . Error
, ( fmt::Debug
fmt::Display
).
, From
. ( ) . , csv::Error
From
io::Error
byteorder::Error
.
, , Result
, , . io::Result
fmt::Result
.
, Rust. . . , .
unwrap
(- Result::unwrap
, Option::unwrap
Option::expect
). , , , . ( , !)unwrap
. : , , - - !unwrap
, String
, Box<Error + Send + Sync>
(- From
.)From
Error
, try!
.std::error::Error
. , , From
, . (- Rust, From
, .)Option
Result
. , , try!
( and_then
, map
unwrap_or
— ).«The Rust Programming Language». . , , Rust, Rust .
Source: https://habr.com/ru/post/270371/
All Articles