标签搜索

下一代的智能合约编程语言Move(五)

heyuan
2023-03-24 / 0 评论 / 5 阅读 / 正在检测是否收录...

前言

上一篇文章我们了解了Move语言的结构体与类型系统,这篇文章将会介绍Move中的所有权机制。

所有权owership

Move虚拟机实现了类似Rust的所有权系统,有兴趣可以先了解一下Rust中的所有权系统。

每一个变量都有自己的范围,当超出范围时,该范围内的变量也会被丢弃。

我们已经在表达式的相关章节中看到了这种现象,记住一个变量只在自己的范围内生效。每一个包含变量的范围都是所有者,变量可以是在这个范围内通过let定义的,也可以是通过参数传递进这个范围的,在Move中只有函数能将变量传递进一个范围。

每一个变量都只有一个拥有者,这意味着当一个变量被当作参数传递给一个函数时,这个函数就变成了这个变量新的所有者。

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

  fun main() {
    // Module::T是一个结构体
    let a: Module::T = Module::create(10);
    // 这时候变量a离开main函数的范围,进入M::value函数范围内
    M::value(a);

    // 这时候main函数范围内已经没有a这个变量,编译会报错
    M::value(a);
  }
}
复制代码

模块M的实现如下:

module M {
  struct T {value: u8}

  public fun create(value: u8): T {
    T {value}
  }
  //变量t传递给函数value,value函数拥有变量的所有权
  public fun value(t: T): u8 {
    t.value
  }
  // 这时候函数范围结束,变量t被丢弃,不会再存在
}
复制代码

move和copy

首先我们需要了解Move VM是如何工作的,当我们传递参数给一个函数又发生了什么,在VM中有两个字节码指令,一个是MoveLoc,一个是CopyLoc,它们分别可以通过move和copy关键字使用。

当一个变量被传递给其他函数时,它被使用MoveLoc移动,例子如下

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

  fun main() {
    // Module::T是一个结构体
    let a: Module::T = Module::create(10);
    // 这时候变量a离开main函数的范围,进入M::value函数范围内
    M::value(move a);

    //变量a已经被废弃
  }
}
复制代码

move关键字可以省略,这里仅仅为了说明

如果想传递一个值给函数且想保存变量的值可以使用关键字copy。

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

  fun main() {
    // Module::T是一个结构体
    let a: Module::T = Module::create(10);
    M::value(copy a);

    //变量a依然存在
  }
}
复制代码

以上我们通过copy关键字避免了变量被废弃,但是copy会增加内存使用,当copy非常大的数据时代价很大,在区块链中每个字节都会影响执行的代价,为了避免过大的额外开销,可以使用引用。

引用

很多编程语言都实现了引用,引用是变量的链接,通过引用可以将变量传递给程序其他部分而不用传递变量的值。

引用(通过&)可以不需要所有权就可以获取到一个变量

module M {
  struct T {value: u8}

  //传递一个引用而不是传递一个值
  public fun value(t: &T): u8 {
    t.value
  }
}
复制代码

不可变的引用只能读取变量的值,不能改变变量的值,可变的引用可以读写变量的值。

module M {
  struct T {value: u8}
  //返回一个非引用类型的值
  public fun create(value: u8): {
    T {value}
  }
  //不可变的引用只允许读
  public fun value(t: &T): u8 {
    t.value
  }

  // 可变引用允许读写值
  public fun change(t: &mut T, value: u8) {
    t.value = value;
  }
}
复制代码

Borrow检查

Move中通过Borrow检查来控制程序中引用的使用,这样有助于避免出错。

module Borrow {

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

    // 创建一个含有B的A
    public fun create(value: u64): A {
        A { b: B { value } }
    }

    // 获得B的可变引用
    public fun ref_from_mut_a(a: &mut A): &mut B {
        &mut a.b
    }

    // 改变B
    public fun change_b(b: &mut B, value: u64) {
        b.value = value;
    }
}
复制代码
script {
    use {{sender}}::Borrow;

    fun main() {
        // 创建一个A
        let a = Borrow::create(0);

        // 通过A获取B的可变引用
        let mut_a = &mut a;
        let mut_b = Borrow::ref_from_mut_a(mut_a);

        // 改变B
        Borrow::change_b(mut_b, 100000);

        // 获取另一个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 ───
    │
 10let _ = Borrow::ref_from_mut_a(mut_a);
    │                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid usage of reference as function argument. Cannot transfer a mutable reference that is being borrowed
    ·
  8let mut_b = Borrow::ref_from_mut_a(mut_a);
    │                     ----------------------------- It is still being mutably borrowed by this reference
    │

复制代码

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

  • 编译器通过所谓的"借用检查"(最初是Rust语言的概念)来防止上面这些错误。编译器通过建立"借用图",不允许被借用的值被"move"。这就是 Move 在区块链中如此安全的原因之一。
  • 可以从引用创建新的引用,老的引用将被新引用"借用"。可变引用可以创建可变或者不可变引用,而不可变引用只能创建不可变引用。
  • 当一个值被引用时,就无法"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"传递它们开销更小。

最后

这篇文章主要介绍了Move中的所有权,更多文章可以关注公众号QStack。

0

评论

博主关闭了所有页面的评论