Rust中分离函数的意义是什么?

21

我已经在Stack Overflow上阅读了几篇答案,并收集了以下用例:

  • 当函数 panic!
  • 当函数中有无限循环时

但是我仍然不清楚为什么我们需要像这样定义函数:

fn func() -> ! {
    panic!("Error!");
}

如果它能够像这样工作(没有感叹号)那么它将以同样的方式工作:

fn func() {
    panic!("Error!");
}

同时,为什么我们需要在具有无限循环的函数中使用!?看起来这个签名没有带来任何实际的用途信息。

2个回答

37
这些签名的主要区别在于!可以强制转换为任何其他类型,因此与任何其他类型兼容(由于不会执行该代码路径,我们可以假设它是我们需要的任何类型)。当我们有多个可能的代码路径时,例如if-elsematch,这很重要。
例如,考虑以下(可能编造的,但希望足够清晰)代码:
fn assert_positive(v: i32) -> u32 {
    match v.try_into() {
        Ok(v) => v,
        Err(_) => func(),
    }
}

func 被声明为返回 ! 时,该函数能够成功编译。 如果我们省略返回类型,则 func 将被声明为返回 (),并且编译会失败

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

您还可以将其与Result::unwrap的定义进行比较:

pub fn unwrap(self) -> T {
    match self {
        Ok(t) => t,
        Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
    }
}

在这里,unwrap_failed 返回 !,因此它将与Ok情况下返回的任何类型统一。

2
对我来说,感觉 ! 让我们可以达到类似动态类型的效果。但同时,这样是不行的:fn func() -> ! { 123 },尽管你说过 __! 可以强制转换为任何其他类型,因此与任何其他类型兼容__。 - Oleksandr Novik
11
你试图做相反的事情 - 强制将默认整数类型 i32 转换为 ! 类型。这显然行不通。在这里,“兼容性”是单向的 - ! 类型(根据定义不存在)的“值”可以用于任何其他类型所期望的地方,反之则不行。 - Cerberus
5
它不能像动态类型一样工作的原因是 "!" 是一个承诺,即函数永远不会返回。由于这个原因,我们可以假设它将返回我们想要的任何类型;这永远不会成为问题,因为它永远不会返回。这就是为什么你的例子不被允许的原因。因为它实际上不是动态类型。 - Glenn Willen
6
这与动态类型无关。"!"是一种底部类型,而不是动态类型,即它是一种子类型,是其他所有类型的子集。 - Jörg W Mittag

14
编译器知道任何遵循发散表达式 (相对于评估顺序) 的内容都是无法到达的。它可以利用这些信息来避免误判本地变量是否初始化时出现假负。考虑以下示例:
use rand; // 0.8.4

fn main() {
    let foo;
    if rand::random::<bool>() {
        foo = "Hello, world!";
    } else {
        diverge();
    }
    println!("{foo}");
}

fn diverge() {
    panic!("Crash!");
}

我们声明了一个变量foo,但是我们只在if表达式的一个分支中对其进行了初始化。这将导致以下错误:

error[E0381]: borrow of possibly-uninitialized variable: `foo`
  --> src/main.rs:10:15
   |
10 |     println!("{foo}");
   |               ^^^^^ use of possibly-uninitialized `foo`

然而,如果我们像这样更改diverge函数的定义:

fn diverge() -> ! {
    panic!("Crash!");
}

那么代码可以成功编译。编译器知道如果执行else分支,它将永远无法到达println!,因为diverge()会发散。因此,else分支不初始化foo并不是一个错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接