Rust 从函数式语言中借鉴了 let 关键字创建变量。 Let 创建的变量一般称为绑定(Binding),它表明了标识符(Identifier)和值(value)之间建立的一种关联关系。
Rust 位置表达式和值表达式
Rust 中的表达式一般可以分为位置表达式(Place Expression)和值表达式(Value Expression)。
位置表达式
位置表达式即表示内存位置的表达式。分为
- 本地变量
- 静态变量
- 解引用(*expr)
- 数组索引(expr[expr])
- 字段引用(expr.field)
- 位置表达式组合 通过位置表达式可以对某个数据单元的内存进行读写。主要是进行写操作,也就是位置表达式可以被赋值的原因。
值表达式
除了位置表达式以外的表达式就是值表达式。 值表达式一般值引用了某个存储单元地址中的数据。相当于数据值,只能进行读操作。
从语义角度讲, 位置表达式代表了数据持久性数据,值表达式代表了临时数据。位置表达式一般有持久状态,值表达式一般为字面量或为表达式求值过郑重创建的临时值。
表达式的求值过程在不同的上下文中会有不同的结果。求值上下文也分为位置上下文(Place Context)和值上下文(Value Context)
位置上下文
- 赋值或者复合赋值语句左侧的操作数
a = b + c其中 a 就是位置上下文 - 一元表达式的独立操作数。
- 包含隐式借用(引用)的操作数。
- match 判别式或 let 绑定右侧在使用 ref 模式匹配的时候也是位置上下文。 除此以外都是值上下文。值表达式不能出现在位置上下文中。示例如下:
pub fn temp() -> i32 {
return 1;
}
fn main () {
let x = &temp(); // temp 函数调用是一个无效的位置表达式
temp() = *x; // error 报错
}
函数 temp 的调用放到了赋值语句左边的位置上下文中,此时编译器报错,因为 temp 函数调用的是一个无效的位置表达式,它是值表达式。
不可绑定与可变绑定
使用 let 关键字声明的位置表达式默认不可变,为不可变绑定,简单来讲,就 是 let 声明的变量不可变,类似于 JavaScript 中的 const 关键字。
fn main () {
let a = 1; // a 不可变
// a = 2; immutable and error
let mut b = 2; // 增加可变声明
b = 3;
}
通过 mut 关键字,可以声明可变的位置表达式,即可变绑定,可变绑定可以正常修改和赋值。
从语义上来讲,let 默认声明的不可变绑定只能对相应的存储单元进行读取,而 let mut 声明的可变绑定则是可以对相应的存储单元进行写入的。
所有权与引用
当位置表达式出现在值上下文中时,该表达式会把内存地址转移给另外一个位置表达式,这其实是所有权转移。
fn main () {
let place1 = "hello";
let place2 = "hello".to_string();
let other = place1;
println!("{:?}", other);
let other = place2;
println("{:?}", other) // error
}
上面代码中使用 let 声明了两个绑定, place1 和 place2。然后将 place1 赋值给新的变量 other。 因为 place1 是一个位置表达式,出现在了赋值操作符的右侧,即一个值上下文内,所以 place1 会将内存地址转移给 other。同理 将 place2 复制给新声明的 other ,place2 的内存地址同样会转移给 other。
第二次声明 other 将 place2 地址赋值给 other 时,会出现报错,意为该处使用了已经移动的值,之所以会出现这种区别,其实是和底层内存安全管理有关系。这两种行为虽然有差别,都是 Rust 为了安全刻意为之。
在语义上,每个变量绑定实际上都拥有该存储单元的所有权,这种转移内存地址的行为就是所有权(OwnerShip) 的转移,在 Rust 中移动(Move)语义,那种不转移的情况实际上是一种复制(Copy)语义。Rust 无 GC,所以完全靠所有权来进行内存管理。
在开发中,一般不需要转移所有权。Rust 提供了引用操作符(&),可以直接获取表达式的存储单元地址,即内存为止。可以通过该内存位置对内存进行读取。
fn main () {
let a = [1,2,3];
let b = &a; // 获取内存地址,相当于指针
println!("{:p}", b);
let mut c = vec![1,2,3];
let d = &mut c;
d.push(4);
println!("{:?}", d);
}
上面代码中,b 以 & 的方式获取 a 的内存地址,这种方式不会引起所有权转移,因为使用引用操作符已经将赋值表达式右侧变成了位置上下文,它知识共享内存位置,通过 println! 宏指定{:p} 格式,可以打印 b 的指针地址,也就是内存地址。
通过 let mut 声明动态常数数组c ,通过 &mut 可获取 c 的可变引用,赋值给 d, 对于 d 的 push 操作,插入新的元素 4。要获取可变引用,必须先声明可变绑定。
不论是&a 还是 &mut c 都相当于对于 a 和 c 所有权的借用,因为 a 和 c 还依旧保留他们的所有权,所以引用也称之为借用。