Skip to content

函数 ​

参数的传递 ​

参数传递时,需要考虑两个问题:

  1. 数据传递的方式
  2. 是否修改数据

数据传递的方式有四种:

  1. 拷贝 copy
  2. 引用 ref
  3. 转移 move
  4. 指针 ptr

而是否修改数据,又要考虑两种情况:

  1. 是否修改函数外的数据
  2. 是否修改函数内的数据

为什么要做这种区分?

举个例子,比如Rust的参数传递方式就不是固定的,而是根据数据类型自己定义:

  1. 如果数据实现了Copy接口,则默认是拷贝传递。例如基础数据类型(int, bool),以及实现了Copy接口的struct。
  2. 如果数据没有实现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个问题不是完全独立的,所以需要探讨他们组合关系:

拷贝修改转移说明RustCC++
√××值传递,数据拷贝之后再传进去由数据类型本身决定:如果数据impl Copy,则会自动拷贝默认行为默认行为
××√转移所有权默认行为,除非数据是Copy类型不支持move关键字
×××借用,不可修改&引用参数不直接支持,近似于const *指针const &引用
×√×可变引用&mut引用不直接支持,近似于指针参数&引用
×√√转移之后,内部可以修改mut不支持move

其他的几种组合,其实是不存在的:

拷贝修改转移说明
√×√拷贝之后,数据和外面的数据是独立的,因此不需要修改和转移
√√√拷贝之后,数据和外面的数据是独立的,因此不需要修改和转移
√√×拷贝之后,数据和外面的数据是独立的,因此不需要修改和转移

这样,就覆盖到了所有可能的组合。

总结一下,我们需要种不同类型的参数标注:

  1. 拷贝传递。copy
  2. 转移传递。move
  3. 借用传递。ref
  4. 可变引用。mut

如果选择一种默认情况不需要加标注,就可以节省一个关键字。 问题在于选哪个呢?

C/C++默认是拷贝传递。因此后三种情况都需要标注。

Rust默认是move/copy(根据数据类型决定),因此它用&和&mut来表示后两种情况。

Released under the MIT License.