Yes, one of the unwrap
is because of the io::Result
and one because of the iterator. Going into detail, looking at lines()
, it returns a custom iterator type, Lines
. That one has an Iterator
implementation that according to the docs has the following type for next()
:
fn next(&mut self) -> Option<std::io::Result<String>>
So your first unwrap
turns this return type of next()
into std::io::Result<String>
, crashing the program if there “was no line” (next()
returned None
, i.e. the lines()
iterator was empty), and the second unwrap
turns that into String
, crashing the program if there was any error while trying to read the file.
Well, kind-of unidiomatic in the sense that you might not want your program to crash that easily. Unless you’re building an application and see no way to recover anyways in which case it can be fine. Well, except you may still want to improve the error message, so those unwraps
are rather uncommon in practice, I guess.
In general, the time when unwrap()
is most appropriate is in situations where the unwrap actually cannot fail, in the sense that failure would indicate a bug in your program, since when hitting a bug a crash with a not-too-fancy error message is probably okay.
The most straightforward alternative to unwrap
is pattern matching to handle the error case differently.
There also is the ?
operator for error propagation, but that doesn’t really work too well on its own yet for handling both Option
and Result
in the same function. You could probably do things like e.g. ...next()?.ok()?
in a function returning Option<...>
or ...next().ok_or("empty file")??
in a Function returning Result<..., Box<dyn Error>>
.
Also if you’re wondering about whether there are any extra differences between the read_line
approach and the lines
approach, you can look at the source code:
impl<B: BufRead> Iterator for Lines<B> {
type Item = Result<String>;
fn next(&mut self) -> Option<Result<String>> {
let mut buf = String::new();
match self.buf.read_line(&mut buf) {
Ok(0) => None,
Ok(_n) => {
if buf.ends_with('\n') {
buf.pop();
if buf.ends_with('\r') {
buf.pop();
}
}
Some(Ok(buf))
}
Err(e) => Some(Err(e)),
}
}
}
What we see is: The lines
iterator uses read_line
. Only an empty file is considered to have “no lines” (next()
returning None
), also the lines
iterator strips the line break, while read_line
doesn’t.