这是一个关于Move语言中引用的详细介绍。我会将其翻译成简单易懂的中文,同时保留关键的技术术语:

引用

Move有两种类型的引用:不可变引用&和可变引用&mut。不可变引用是只读的,不能修改底层值(或其任何字段)。可变引用允许通过该引用进行修改。Move的类型系统强制执行所有权规则,以防止引用错误。

引用操作符

Move提供了创建和扩展引用以及将可变引用转换为不可变引用的操作符。这里我们用e: T表示"表达式e的类型为T"。

语法类型描述
&e&T,其中e: TT不是引用类型创建e的不可变引用
&mut e&mut T,其中e: TT不是引用类型创建e的可变引用
&e.f&T,其中e.f: T创建结构体e的字段f的不可变引用
&mut e.f&mut T,其中e.f: T创建结构体e的字段f的可变引用
freeze(e)&T,其中e: &mut T将可变引用e转换为不可变引用

&e.f&mut e.f操作符既可以用于创建新的引用到结构体中,也可以用于扩展现有引用:

let s = S { f: 10 };
let f_ref1: &u64 = &s.f; // 可以
let s_ref: &S = &s;
let f_ref2: &u64 = &s_ref.f // 也可以

多字段的引用表达式只要两个结构体在同一模块中就可以工作:

public struct A { b: B }
public struct B { c : u64 }
fun f(a: &A): &u64 {
    &a.b.c
}

最后,请注意不允许引用的引用:

let x = 7;
let y: &u64 = &x;
let z: &&u64 = &y; // 错误! 无法编译

通过引用读写

可变和不可变引用都可以被读取以产生被引用值的副本。

只有可变引用可以被写入。写入*x = v会丢弃之前存储在x中的值,并用v更新它。

这两种操作都使用类似C的*语法。但请注意,读取是一个表达式,而写入是必须发生在等号左侧的变更。

语法类型描述
*eT,其中e&T&mut T读取e指向的值
*e1 = e2(),其中e1: &mut Te2: Te2更新e1中的值

为了能读取引用,底层类型必须具有copy能力,因为读取引用会创建值的新副本。这条规则防止了资产被复制:

fun copy_coin_via_ref_bad(c: Coin) {
    let c_ref = &c;
    let counterfeit: Coin = *c_ref; // 不允许!
    pay(c);
    pay(counterfeit);
}

相对地:为了能写入引用,底层类型必须具有drop能力,因为写入引用会丢弃(或"删除")旧值。这条规则防止了资源值被销毁:

fun destroy_coin_via_ref_bad(mut ten_coins: Coin, c: Coin) {
    let ref = &mut ten_coins;
    *ref = c; // 错误! 不允许--会销毁10个硬币!
}

freeze推断

可变引用可以在期望不可变引用的上下文中使用:

let mut x = 7;
let y: &u64 = &mut x;

这是因为在底层,编译器会在需要的地方插入freeze指令。这里有更多freeze推断的示例:

fun takes_immut_returns_immut(x: &u64): &u64 { x }

// 在返回值上进行freeze推断
fun takes_mut_returns_immut(x: &mut u64): &u64 { x }

fun expression_examples() {
    let mut x = 0;
    let mut y = 0;
    takes_immut_returns_immut(&x); // 无推断
    takes_immut_returns_immut(&mut x); // 推断为freeze(&mut x)
    takes_mut_returns_immut(&mut x); // 无推断

    assert!(&x == &mut y, 42); // 推断为freeze(&mut y)
}

fun assignment_examples() {
    let x = 0;
    let y = 0;
    let imm_ref: &u64 = &x;

    imm_ref = &x; // 无推断
    imm_ref = &mut y; // 推断为freeze(&mut y)
}

子类型

通过这种freeze推断,Move类型检查器可以将&mut T视为&T的子类型。如上所示,这意味着在任何使用&T值的表达式中,也可以使用&mut T值。这个术语在错误消息中用于简洁地表示在需要&T的地方提供了&mut T。例如:

module a::example {
    fun read_and_assign(store: &mut u64, new_value: &u64) {
        *store = *new_value
    }

    fun subtype_examples() {
        let mut x: &u64 = &0;
        let mut y: &mut u64 = &mut 1;

        x = &mut 1; // 有效
        y = &2; // 错误! 无效!

        read_and_assign(y, x); // 有效
        read_and_assign(x, y); // 错误! 无效!
    }
}

将产生以下错误消息:

错误:

    ┌── example.move:11:9 ───
    │
 12 │         y = &2; // 无效!
    │         ^ 对局部变量'y'的无效赋值
    ·
 12 │         y = &2; // 无效!
    │             -- 类型: '&{integer}'
    ·
  9 │         let mut y: &mut u64 = &mut 1;
    │                    -------- 不是子类型: '&mut u64'
    │

错误:

    ┌── example.move:14:9 ───
    │
 15 │         read_and_assign(x, y); // 无效!
    │         ^^^^^^^^^^^^^^^^^^^^^ 对'a::example::read_and_assign'的无效调用。参数'store'无效
    ·
  8 │         let mut x: &u64 = &0;
    │                    ---- 类型: '&u64'
    ·
  3 │     fun read_and_assign(store: &mut u64, new_value: &u64) {
    │                                -------- 不是子类型: '&mut u64'
    │

目前唯一具有子类型的其他类型是元组

所有权

可变和不可变引用都可以随时被复制和扩展,即使存在同一引用的现有副本或扩展:

fun reference_copies(s: &mut S) {
  let s_copy1 = s; // 可以
  let s_extension = &mut s.f; // 也可以
  let s_copy2 = s; // 仍然可以
  ...
}

这可能会让熟悉Rust所有权系统的程序员感到惊讶,Rust会拒绝上面的代码。Move的类型系统在处理复制时更加宽松,但在写入前确保可变引用的唯一所有权方面同样严格。

引用不能被存储

引用和元组是_唯一_不能作为结构体字段值存储的类型,这也意味着它们不能存在于存储或对象中。在程序执行期间创建的所有引用都会在Move程序终止时被销毁;它们完全是短暂的。这也适用于所有没有store能力的类型:任何非store类型的值必须在程序终止前被销毁。能力,但请注意引用和元组更进一步,从一开始就不允许存在于结构体中。

这是Move和Rust的另一个区别,Rust允许在结构体中存储引用。

我们可以想象一个更复杂、更具表现力的类型系统,允许在结构体中存储引用。我们可以允许在没有store能力的结构体中使用引用,但核心困难在于Move有一个相当复杂的系统来跟踪静态引用安全性。类型系统的这个方面也需要扩展以支持在结构体中存储引用。简而言之,Move的引用安全系统需要扩展以支持存储引用,这是我们在语言演化过程中一直在关注的问题。