Move语言 引用

许多编程语言都支持引用引用是指向变量(通常是内存中的某个片段)的链接,你可以将其传递到程序的其他部分,而无需移动变量值。

引用(标记为&)使我们可以使用值而无需拥有所有权。

我们修改一下上面的示例,看看如何使用引用。

module M {
    struct T { value: u8 }
    // ...
    // ...
    // instead of passing a value, we'll pass a reference
    public fun value(t: &T): u8 {
        t.value
    }
}

我们在参数类型 T 前添加了&符号,这样就可以将参数类型T转换成了 T 的引用&T。

Move 支持两种类型的引用:不可变引用 &(例如&T)和可变引用 &mut(例如&mut T)。

不可变的引用允许我们在不更改值的情况下读取值。可变引用赋予我们读取和更改值的能力。

module M {
    struct T { value: u8 }

    // returned value is of non-reference type
    public fun create(value: u8): T {
        T { value }
    }

    // immutable references allow reading
    public fun value(t: &T): u8 {
        t.value
    }

    // mutable references allow reading and changing the value
    public fun change(t: &mut T, value: u8) {
        t.value = value;
    }
}

现在,让我们看看如何使用升级后的模块 M。

script {
    use {{sender}}::M;

    fun main() {
        let t = M::create(10);

        // create a reference directly
        M::change(&mut t, 20);

        // or write reference to a variable
        let mut_ref_t = &mut t;

        M::change(mut_ref_t, 100);

        // same with immutable ref
        let value = M::value(&t);

        // this method also takes only references
        // printed value will be 100
        0x1::Debug::print<u8>(&value);
    }
}

使用不可变引用(&)从结构体读取数据,使用可变引用(&mut)修改它们。通过使用适当类型的引用,我们可以更加安全的读取模块,因为它能告诉代码的阅读者,该变量是否会被修改。

Borrow 检查

Move 通过"Borrow 检查"来控制程序中"引用"的使用,这样有助于防止意外出错。为了理解这一点,我们看一个例子。

module Borrow {
    struct B { value: u64 }
    struct A { b: B }

    // create A with inner B
    public fun create(value: u64): A {
        A { b: B { value } }
    }

    // give a mutable reference to inner B
    public fun ref_from_mut_a(a: &mut A): &mut B {
        &mut a.b
    }

    // change B
    public fun change_b(b: &mut B, value: u64) {
        b.value = value;
    }
}
script {
    use {{sender}}::Borrow;

    fun main() {
        // create a struct A { b: B { value: u64 } }
        let a = Borrow::create(0);

        // get mutable reference to B from mut A
        let mut_a = &mut a;
        let mut_b = Borrow::ref_from_mut_a(mut_a);

        // change B
        Borrow::change_b(mut_b, 100000);

        // get another mutable reference from A
        let _ = Borrow::ref_from_mut_a(mut_a);
    }
}

上面代码可以成功编译运行,不会报错。这里究竟发生了什么呢?首先,我们使用 A 的可变引用(&mut A)来获取对其内部 struct B 的可变引用(&mut B)。然后我们改变 B。然后可以再次通过 &mut A 获取对 B 的可变引用。

但是,如果我们交换最后两个表达式,即首先尝试创建新的 &mut A,而 &mut B 仍然存在,会出现什么情况呢?

let mut_a = &mut a;
let mut_b = Borrow::ref_from_mut_a(mut_a);

let _ = Borrow::ref_from_mut_a(mut_a);

Borrow::change_b(mut_b, 100000);

编译器将会报错:

    ┌── /scripts/script.move:10:17 ───
    │
 10 │         let _ = Borrow::ref_from_mut_a(mut_a);
    │                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid usage of reference as function argument. Cannot transfer a mutable reference that is being borrowed
    ·
  8 │         let mut_b = Borrow::ref_from_mut_a(mut_a);
    │                     ----------------------------- It is still being mutably borrowed by this reference
    │

该代码不会编译成功。为什么?因为 &mut A 已经被 &mut B 借用。如果我们再将其作为参数传递,那么我们将陷入一种奇怪的情况,A 可以被更改,但 A 同时又被引用。而 mut_b 应该指向何处呢?

我们得出一些结论:

  1. 编译器通过所谓的"借用检查"(最初是Rust语言的概念)来防止上面这些错误。编译器通过建立"借用图",不允许被借用的值被"move"。这就是 Move 在区块链中如此安全的原因之一。
  2. 可以从引用创建新的引用,老的引用将被新引用"借用"。可变引用可以创建可变或者不可变引用,而不可变引用只能创建不可变引用。
  3. 当一个值被引用时,就无法"move"它了,因为其它值对它有依赖。

取值运算

可以通过取值运算*来获取引用所指向的值。

取值运算实际上是产生了一个副本,要确保这个值具有 Copy ability。

module M {
    struct T has copy {}

    // value t here is of reference type
    public fun deref(t: &T): T {
        *t
    }
}

取值运算不会将原始值 move 到当前作用域,实际上只是生成了一个副本。

有一个技巧用来复制一个结构体的字段:就是使用*&,引用并取值。我们来看一个例子:

module M {
    struct H has copy {}
    struct T { inner: H }

    // ...

    // we can do it even from immutable reference!
    public fun copy_inner(t: &T): H {
        *&t.inner
    }
}

通过使用*&(编译器会建议这样做),我们复制了结构体的内部值。

引用基本类型

基本类型非常简单,它们不需要作为引用传递,缺省会被复制。当基本类型的值被传给函数时,相当于使用了copy关键字,传递进函数的是它们的副本。当然你可以使用move关键字强制不产生副本,但是由于基本类型的大小很小,复制它们其实开销很小,甚至比通过引用或者"move"传递它们开销更小。

script {
    use {{sender}}::M;

    fun main() {
        let a = 10;
        M::do_smth(a);
        let _ = a;
    }
}

也就是说,即使我们没有将a作为引用传递,该脚本也会编译。我们也无需添加copy,因为 VM 已经帮组我们添加了。

下一章:Move语言 泛型

泛型对于 Move 语言是必不可少的,它是 Move 灵活性的重要来源。泛型是具体类型或其他属性的抽象替代品。实际上,泛型允许我们只编写单个函数,而该函数可以应用于任何类型。这种函数也被称为模板 —&mdas ...