A common example of using return
as an expression, and especially the ability of it to take on any type you want (technically because it’s the “never” type “!
” which can coerece into any other type) is with a desugared ?
-operator. I.e. you’re writing a function returning some Option<…>
type, and inside you want to unwrap some Option<…>
type and return early in case it was None
.
With ?
operator it would be e.g.
fn first_as_string(numbers: &[i32]) -> Option<String> {
let option_first: Option<&i32> = numbers.get(0);
let first: &i32 = option_first?;
Some(first.to_string())
}
The relevant line, let first: &i32 = option_first?;
, is desugared (ignoring fancy Try
trait generalizations) into
let first: &i32 = match option_first {
Some(n) => n,
None => return None,
};
This states that, in case None
is encountered, at least syntactically the code states that the value of return None
is assigned to first
. Of course, such a value is never evaluated; but the type-checker must still handle it somehow, and that’s by giving it the “never” type, (read: “the expression will never have a value”), which the type checker will allow to be coerced to &i32
, too.
Never type is a little bit more magical, as it interacts with reachability analysis that can also make the return value of blocks without final expressions turn into !
. E.g. let’s print something in the None
case, too, using a block-style match arm:
let first: &i32 = match option_first {
Some(n) => n,
None => {
println!("returning early!");
return None
}
};
(full code in playground)
Now, without reachability analysis, the block
{
println!("returning early!");
return None;
}
would have type ()
, resulting in type errors. E.g. if we “outsmart” the analysis with a if true
, we’d get such an error
error[E0308]: mismatched types
--> src/main.rs:5:17
|
5 | None => {
| _________________^
6 | | println!("returning early!");
7 | | if true { return None };
8 | | }
| |_________^ expected `&i32`, found `()`
If the value of the expression were to be used directly, it’d be this instead:
let first: &i32 = match option_first {
Some(n) => n,
None => {
println!("returning early!");
return None // <- no semicolon!!
}
};
however, blocks whose end is unreachable are made to have !
type themselves. You can see unreachable analysis also creating warnings if further code is present:
let first: &i32 = match option_first {
Some(n) => n,
None => {
println!("returning early!");
return None;
println!("returned early!");
}
};
warning: unreachable statement
--> src/main.rs:8:9
|
7 | return None;
| ----------- any code following this expression is unreachable
8 | println!("returned early!");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
|
= note: `#[warn(unreachable_code)]` on by default
= note: this warning originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
And you can see the same behavior reproduced with a function returning !
-type (e.g. by panicking) like
fn never_returns() -> ! { panic!() }
let first: &i32 = match option_first {
Some(n) => n,
None => {
println!("returning early!");
never_returns();
println!("returned early!");
}
};
which compiles fine (so the block is !
-type, not ()
-type), and also gives the unreachable code warning like above.