函数
参数的传递
参数传递时,需要考虑两个问题:
- 数据传递的方式
- 是否修改数据
数据传递的方式有四种:
- 拷贝 copy
- 引用 ref
- 转移 move
- 指针 ptr
而是否修改数据,又要考虑两种情况:
- 是否修改函数外的数据
- 是否修改函数内的数据
为什么要做这种区分?
举个例子,比如Rust的参数传递方式就不是固定的,而是根据数据类型自己定义:
- 如果数据实现了Copy接口,则默认是拷贝传递。例如基础数据类型(int, bool),以及实现了Copy接口的struct。
- 如果数据没有实现Copy接口,则默认是转移传递。
在转移的情况下,如果传入的数据是不可变的,考虑到已经转移进函数了,外面的数据本身失效了,所以函数内就可以选择修改了!因此下面两种情况在Rust里都是合法的:
rust
fn foo(mut vec: Vec<i32>) -> Vec<i32> {
// 这里v已经转移了,所以如果参数指定了mut,那就可以修改了!
vec.push(4);
vec
}
#[test]
fn test_fn_mut() {
let v = vec![1, 2, 3];
let v2 = foo(v);
assert_eq!(v2, vec![1, 2, 3, 4]);
}可以看到,这里通过转移,即实现了可以利用函数的计算从旧值的到新值,同时又不需要拷贝操作,也能把mut的范围精确控制在foo函数内部。虽然不太直观,但是确实做到了性能和安全的平衡。
因此,在转移操作中,参数是需要区分是否可修改的。
当然,如果我们不用转移,而是用引用,那上面的例子就可以不需要mut了:
rust
fn foo(vec: &Vec<i32>) -> Vec<i32> {
let mut v = vec.clone();
v.push(4);
v
}这里传入时确实没有多一步拷贝,但是由于函数外的v还需要保留, 就只能新建一份数据来进行计算了。这里的效率并不是最高的。
还有一种办法,那就是用可修改的引用,意思是“我会在函数里修改你的内容”。这时候mut的“可修改”,既是对内的声明,也是对外的声明:
rust
let mut v = vec![1, 2, 3];
fn foo(vec: &mut Vec<i32>) {
vec.push(4);
}注意,这时候由于直接修改的就是外面的数组,因此可以不需要返回。
这样做,效率确实达标了,但是要求v必须是可变的。这样就没法精确地控制mut的范围了。
这3个问题不是完全独立的,所以需要探讨他们组合关系:
| 拷贝 | 修改 | 转移 | 说明 | Rust | C | C++ |
|---|---|---|---|---|---|---|
| √ | × | × | 值传递,数据拷贝之后再传进去 | 由数据类型本身决定:如果数据impl Copy,则会自动拷贝 | 默认行为 | 默认行为 |
| × | × | √ | 转移所有权 | 默认行为,除非数据是Copy类型 | 不支持 | move关键字 |
| × | × | × | 借用,不可修改 | &引用参数 | 不直接支持,近似于const *指针 | const &引用 |
| × | √ | × | 可变引用 | &mut引用 | 不直接支持,近似于指针参数 | &引用 |
| × | √ | √ | 转移之后,内部可以修改 | mut | 不支持 | move |
其他的几种组合,其实是不存在的:
| 拷贝 | 修改 | 转移 | 说明 |
|---|---|---|---|
| √ | × | √ | 拷贝之后,数据和外面的数据是独立的,因此不需要修改和转移 |
| √ | √ | √ | 拷贝之后,数据和外面的数据是独立的,因此不需要修改和转移 |
| √ | √ | × | 拷贝之后,数据和外面的数据是独立的,因此不需要修改和转移 |
这样,就覆盖到了所有可能的组合。
总结一下,我们需要种不同类型的参数标注:
- 拷贝传递。copy
- 转移传递。move
- 借用传递。ref
- 可变引用。mut
如果选择一种默认情况不需要加标注,就可以节省一个关键字。 问题在于选哪个呢?
C/C++默认是拷贝传递。因此后三种情况都需要标注。
Rust默认是move/copy(根据数据类型决定),因此它用&和&mut来表示后两种情况。