为什么函数参数提示《remove this mut》

翻译自《https://www.snoyman.com/blog/2020/05/no-mutable-parameters-in-rust》

在回顾Begin Rust书中的倒数第二章时,出现了一个稍微高级的话题。这个话题引起了我一段时间的兴趣,特别是因为它展示了Rust和Haskell如何处理可变性的一些根本差异。该主题对于本书而言太高了,但是我想提供一个外部资源,以供有好奇心的人参考。就是这里!

让我们逐步构建它。以下程序可以编译吗?

1
2
3
4
5
fn main() {
let x = 5;
x += 1;
println!("x == {}", x);
}

答:不!x是一个不可变的变量,因此不能+= 1在其上使用。解决这个问题很容易:add mut:

1
2
3
4
5
fn main() {
let mut x = 5;
x += 1;
println!("x == {}", x);
}

但是由于加1和打印在我的应用程序中是如此重要(是的,这很讽刺),所以我决定将其提取为自己的函数。告诉我,这段代码可以编译吗?

1
2
3
4
fn add_and_print(x: i32) {
x += 1;
println!("x == {}", x);
}

不,并且出于与第一个示例相同的原因:x是不可变的。修复也很容易:

1
2
3
4
fn add_and_print(mut x: i32) {
x += 1;
println!("x == {}", x);
}

现在,我们的函数add_and_print拥有唯一的参数x,它的类型是i32,并且是可变的。嗯不错。最后,我们可以在 main 中调用此函数。告诉我,该程序可以编译并运行吗?是否和预想的一样没有任何警告吗?

1
2
3
4
5
6
7
8
9
fn main() {
let mut x = 5;
add_and_print(x);
}

fn add_and_print(mut x: i32) {
x += 1;
println!("x == {}", x);
}

答:它会编译,运行并生成输出x == 6。但是,它确实有一个警告:

1
2
3
4
5
6
7
8
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut x = 5;
| ----^
| |
| help: remove this `mut`
|

最初,至少对于我来说,这确实令人惊讶。add_and_print需要接收一个可变变量i32作为其第一个参数。我们为其提供了可变的i32。然后编译器说mutmain中是不必要的。这是什么情况?

我上面的解释有一个错误。函数add_and_print,可能非常令人困惑,不用将可变变量i32作为参数。“但是前面说需要mut的!!!”,而现在又说mut是不必要的。 确实如此。mut详细信息位于函数内部,而不是其类型签名的一部分。这听起来令人困惑,所以让我解释一下。

在Rust中有一个模式,我称之为rust的3条规则。这涵盖了以下事实:在许多情况下,我们最终会得到三个“版本”的事物:

1
2
3
An immutable borrow   # 不可变借用
A mutable borrow # 可变借用
A move # 移动

这可以应用于函数参数,例如:

1
2
3
fn immutable_borrow(x: &String)
fn mutable_borrow(x: &mut String)
fn move(x: String)

请注意,差异完全在冒号之后。冒号后的内容是3个不同的类型,而类型构成了函数签名

但是,示例中的x冒号前的内容对函数签名没有影响。变量名称与函数的签名无关。一旦冒号右边传递一个函数即可决定要调用的内容。

此规则不仅适用于变量名称。也适用于可变变量mutable是rust中变量的一个feature而不是的feature。当使用let mut x = 5时,表示的是 “创建一个叫x的变量,它指向一个5,并且允许用x来对该进行改变。” 如果没有mut,将不再被允许通过变量x对该进行改变。

您可能会有一个直觉的反应,如果您无法修改变量,那你就只能只读它了。一般直觉应该就是这样。但Rust中并非如此。您还可以做另一件事:将移到另一个作用域。add_and_print通过move接受,即使x是一个不可变的变量,我仍然可以移动它所指向的

一旦移动了,那就完全取决于add_and_print如何处理它。即使原始变量是不可变的,也可以将其变为可变的。这是因为在函数调用中本身被传递过来,而不是变量。而值可以根据需要来变为可变的。

因此,该程序的无警告版本为:

1
2
3
4
5
6
7
8
9
fn main() {
let x = 5;
add_and_print(x);
}

fn add_and_print(mut x: i32) {
x += 1;
println!("x == {}", x);
}

实际上,即使没有函数调用,move后为可变的也是可能发生的。例如,您可以在单个函数中将不可变变量“升级”为可变变量:

1
2
3
4
5
6
fn main() {
let x = 5;
let mut y = x;
y += 1;
println!("y == {}", y);
}

“等一下” 您抱怨道,“你不能将不可变的引用“升级”为可变的引用!” ,对于我的解释或许还有其他类似的关于抱怨。我在这里稍加修饰的是: 当涉及到引用时,可变性被转化为值的可变性。那是因为对于类似x: &i32这种形式的引用,x自身并没有任何数值,它引用了一个数字。对于可变或不可变是对引用本身来说的,因为引用自身是一种类型。因此,您不能简单地将不可变引用升级为可变引用。此代码已损坏:

1
2
3
4
5
6
7
8
9
fn main() {
let mut x: i32 = 5;
let y: &i32 = &x;
let z: &mut i32 = y; // 不能简单的将y升级为可变的

*z += 1;

println!("x == {}", x);
}

因此,总结一下:

  • 您可以拥有,或不变引用可变引用
  • 只有2种状态,要么可变的,要么不可变
  • 相似的变量也只有2种状态,要么可变的,要么不可变
  • 当将移动到新变量中(通过 let或函数调用)时,可以更改变量的可变性
  • 函数签名中变量的可变性和名称并不影响函数签名
  • 引用的可变性内置在类型本身中,因此您不能将不可变的引用“升级”为可变的引用

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!