跳转到内容

引用

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 运算符既可以用于创建新的结构体引用,也可以用于扩展现有引用:

script {
fun example() {
let s = S { f: 10 };
let f_ref1: &u64 = &s.f; // 有效
let s_ref: &S = &s;
let f_ref2: &u64 = &s_ref.f; // 同样有效
}
}

只要两个结构体在同一个模块中,具有多个字段的引用表达式就能正常工作:

module 0x42::example {
struct A { b: B }
struct B { c : u64 }
fun f(a: &A): &u64 {
&a.b.c
}
}

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

script {
fun example() {
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 能力,因为读取引用会创建值的新副本.此规则防止资源值被复制:严格遵循系统指示翻译以下内容:

module 0x42::coin {
struct Coin {} // Note does not have copy
fun copy_resource_via_ref_bad(c: Coin) {
let c_ref = &c;
let counterfeit: Coin = *c_ref; // 不允许!
pay(c);
pay(counterfeit);
}
}

同理:为了能够通过引用写入值,基础类型必须具有 drop 能力,因为写入引用会丢弃(或 “drop” )旧值. 这条规则防止了资源值的意外销毁:

模块 0x42::coin {
struct Coin {} // 注意没有drop能力
fun destroy_resource_via_ref_bad(ten_coins: Coin, c: Coin) {
let ref = &mut ten_coins;
*ref = c; // 不允许——这会销毁10个代币!
}
}

可变引用可以在需要不可变引用的上下文中使用:

script {
fun example() {
let x = 7;
let y: &u64 = &mut x;
}
}

这是因为在底层,编译器会在需要时自动插入freeze指令.以下是更多freeze推断的实际示例:

module 0x42::example {
fun takes_immut_returns_immut(x: &u64): &u64 { x }
// 对返回值进行 freeze 推断
fun takes_mut_returns_immut(x: &mut u64): &u64 { x }
fun expression_examples() {
let x = 0;
let 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 值. 这个术语用在错误信息中,可以简洁地表明在需要 &mut T 的地方提供了 &T.

例如

module 0x42::example {
fun read_and_assign(store: &mut u64, new_value: &u64) {
*store = *new_value
}
fun subtype_examples() {
let x: &u64 = &0;
let y: &mut u64 = &mut 1;
x = &mut 1; // valid
y = &2; // invalid!
read_and_assign(y, x); // 有效
read_and_assign(x, y); // 无效!
}
}

会产生以下错误信息

Terminal window
error:
┌── example.move:12:9 ───
12 y = &2; // invalid!
^ Invalid assignment to local 'y'
·
12 y = &2; // invalid!
-- The type: '&{integer}'
·
9 let y: &mut u64 = &mut 1;
-------- Is not a subtype of: '&mut u64'
error:
┌── example.move:15:9 ───
15 read_and_assign(x, y); // invalid!
^^^^^^^^^^^^^^^^^^^^^ Invalid call of '0x42::example::read_and_assign'. Invalid argument for parameter 'store'
·
8 let x: &u64 = &0;
---- The type: '&u64'
·
3 fun read_and_assign(store: &mut u64, new_value: &u64) {
-------- Is not a subtype of: '&mut u64'

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

可变和不可变引用都可以被复制和扩展 即使已存在相同引用的副本或扩展 :

script {
fun reference_copies(s: &mut S) {
let s_copy1 = s; // 允许
let s_extension = &mut s.f; // 同样允许
let s_copy2 = s; // 仍然允许
// ...
}
}

对于熟悉Rust所有权系统的程序员来说,这可能会令人惊讶,因为Rust会拒绝上述代码. Move 的类型系统在处理 copies 时更为宽松,但在确保可变引用写入前的唯一所有权方面同样严格.

引用和元组是 唯二 不能作为结构体字段值存储的类型,这也意味着它们不能存在于全局存储中.

所有在程序执行期间创建的引用都会在 Move 程序终止时被销毁;它们是纯粹临时性的. 这个不变量对于没有 store 能力 的类型值同样成立,但要注意引用和元组更进一步:从一开始就不允许出现在结构体中.

这是 Move 与 Rust 之间的另一个区别, Rust 允许将引用存储在结构体内部.

目前Move无法支持这一点,因为引用不能被 序列化 ,而 每个Move值都必须是可序列化的 . 这要求源于 Move 的持久化全局存储,它需要通过序列化值来跨程序执行保持数据.结构体可以被写入全局存储,因此它们必须是可序列化的. 可以设想一种更复杂,更具表现力的类型系统,它允许在结构体中存储引用_同时_禁止这些结构体存在于全局存储中. 我们或许可以允许在不具备 store 能力 的结构体内部使用引用,但这并不能完全解决问题:Move语言拥有相当复杂的静态引用安全追踪系统,类型系统的这一方面也需要进行扩展才能支持在结构体中存储引用. 简而言之,Move的类型系统(特别是涉及引用安全的部分)必须扩展才能支持存储引用.不过随着语言的发展,这是我们正在持续关注的方向.