在 `return` 语句末尾加上分号是否有区别?

7

Rust指南中提到:

分号通过丢弃其值并返回unit,将任何表达式转换为语句。

我以为我已经理解了这个概念,直到我进行了一个实验:

fn print_number(x: i32, y: i32) -> i32 {
    if x + y > 20 { return x }      
    x + y 
}

这段代码可以顺利编译。接着,在return语句的结尾添加了一个分号 (return x;)。据我所知,这将把该行转换为一个语句,返回unit数据类型()

不过,最终结果并没有改变。

2个回答

5

通常,在if表达式中的每个分支都应具有相同的类型。如果某个分支的类型未指定,则编译器会尝试找到单一的通用类型:

fn print_number(x: int, y: int) {
  let v = if x + y > 20 {
    3 // this can be either 3u, 3i, 3u8 etc.
  } else {
    x + y // this is always int
  };
  println!("{}", v);
}

在这段代码中,3的类型未指定,但else分支将它强制转换为int类型。
听起来很简单:有一个函数可以将两个或多个类型统一成一个公共类型,如果不可能进行统一,则会报错。但是,如果分支中存在fail!会怎样呢?
fn print_number(x: int, y: int) {
  let v = if x + y > 20 {
    fail!("x + y too large") // ???
  } else {
    x + y // this is always int
  };
  println!("{}", v); // uh wait, what's the type of `v`?
}

我希望fail!不会影响其他分支,毕竟这是一个特殊情况。由于这种模式在Rust中很常见,因此引入了发散类型的概念。没有值的类型是发散的。(根据上下文,它也被称为“未被占用的类型”或“空类型”。不要与“单元类型”混淆,后者只有一个值()。)由于发散类型自然是任何其他类型的子集,编译器得出v的类型就是else分支的类型intreturn表达式在类型检查方面与fail!没有区别。它像fail!一样突然跳出当前执行流(但不会终止任务,谢天谢地)。但是,发散类型不会传播到下一条语句:
fn print_number(x: int, y: int) {
  let v = if x + y > 20 {
    return; // this is diverging
    () // this is implied, even when you omit it
  } else {
    x + y // this is always int
  };
  println!("{}", v); // again, what's the type of `v`?
}

请注意,唯一带分号的语句x;等同于表达式x; ()。通常a; bb具有相同的类型,因此当x不发散时,x; ()具有()类型是非常奇怪的,而当x发散时,它会发散。这就是为什么您的原始代码无法工作的原因。
添加特殊情况似乎很诱人:
  • 为什么不在x发散时使x; ()发散?
  • 当无法推断出整数文字的类型时,为什么不假定每个未指定的整数文字都是uint?(注:过去是这种情况。)
  • 在统一多个trait对象时,为什么不自动查找公共超级trait?
事实上,设计类型系统并不是很难,但验证它要困难得多,我们希望确保Rust的类型系统是面向未来的和长期存在的。如果某些内容确实有用并且被证明对我们的目的是“正确”的,则可能会发生其中一些,但不会立即发生。

我感觉我更理解returnfail!发散函数的工作原理了。我的代码示例在return x处是否有分号都可以编译通过。所以我猜结论是:加上分号也不会有什么区别? - sargas
我编辑了我的代码示例,以更清楚地阐明我的问题。但是,无论是return x还是return x;在最后似乎都做了同样的事情,我不知道在这种情况下使用分号有什么区别。我的编辑是否改变了您的问题背景?(仍在努力理解Rust的低级概念)。 - sargas
1
@sargas:你的修改完全改变了情况。之前,函数的尾部是一个if表达式;现在,函数的尾部是x + y表达式。在两种情况下,return表达式都可能导致早期退出。然而,在你编辑的示例中,if表达式的类型变得无关紧要(我们知道它是(),因为它没有else子句,只有当if子句评估为()时才允许)。话虽如此,就个人而言,如果我用return结束一个if,我不会写一个else,而是把那段代码放在if后面,就像你在你的编辑中所做的那样。 - Francis Gagné
@FrancisGagné 很酷,现在我只需要一个包含你所说内容的答案,这样我就可以接受它了。 - sargas
@sargas: 我已经编辑了我的答案和这个答案(目前待审核)。我删除了一部分答案,因为我认为原作者误读了您的问题(就像我最初做的那样!)并且基本上与您在问题中得出的结论相矛盾(或者可能是否认)。 - Francis Gagné
显示剩余2条评论

3
我是一名有用的助手,可以翻译文本。

我不完全确定我在说什么,但它有点有道理。

还有一个概念要介绍:可达性分析。编译器知道跟在return表达式语句后面的代码是无法到达的。例如,如果我们编译这个函数:

fn test() -> i32 {
    return 1;
    2
}

我们收到以下警告:
warning: unreachable expression
 --> src/main.rs:3:5
  |
3 |     2
  |     ^
  |

编译器可以忽略带有return表达式的"true"分支,仅在确定if表达式类型时考虑"false"分支。

您还可以通过diverging functions查看此行为。Diverging函数是不正常返回的函数(例如,它们总是失败)。尝试将return表达式替换为fail!宏(该宏扩展为调用diverging函数)。实际上,return表达式也被视为diverging;这是前面提到的可达性分析的基础。

但是,如果在return语句后面有一个实际的()表达式,您将收到错误。这个函数:
fn print_number(x: i32, y: i32) -> i32 {
    if x + y > 20 {
        return x;
        ()
    } else {
        x + y
    }
}

给出以下错误:

error[E0308]: mismatched types
 --> src/main.rs:4:9
  |
4 |         ()
  |         ^^ expected i32, found ()
  |
  = note: expected type `i32`
             found type `()`

最后,似乎分歧的表达式(包括return表达式)在后面跟着一个分号时,编译器会以不同的方式处理:该语句仍然是分歧的。

有什么不同吗?如果没有分号,你会有什么区别? - sargas
没有分号,return表达式和分歧表达式似乎与任何表达式都具有类型兼容性,并且即使在它们后面跟着一个分号时,它们似乎仍然保留着这种特殊特性。 - Francis Gagné
这正是他们在IRC频道告诉我的。基本上,对于最终结果来说没有任何区别(至少对于我的例子而言),但添加分号可以让人感到放心 :) - sargas

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