The main difference between these signatures boils down to the fact that ! can coerce into any other type, and thus is compatible with any other type (since this code path is never taken, we can assume it to be of any type we need). It's important when we have multiple possible code paths, such as if-else or match.
For example, consider the following (probably contrived, but hopefully clear enough) code:
fn assert_positive(v: i32) -> u32 {
match v.try_into() {
Ok(v) => v,
Err(_) => func(),
}
}
When func is declared to return !, this function compiles successfully. If we drop the return type, func will be declared as returning (), and the compilation breaks:
error[E0308]: `match` arms have incompatible types
--> src/main.rs:8:19
|
6 | / match v.try_into() {
7 | | Ok(v) => v,
| | - this is found to be of type `u32`
8 | | Err(_) => func(),
| | ^^^^^^ expected `u32`, found `()`
9 | | }
| |_____- `match` arms have incompatible types
You can also compare this with definition for Result::unwrap:
pub fn unwrap(self) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
}
}
Here, unwrap_failed is returning !, so it unifies with whatever type is returned in Ok case.