跳转到内容

B.1 位置表达式与值表达式

你已经见过下面几种写法:

  • point.x = 10;
  • values[0] = 10;
  • *reference = 10;
  • let value = expression。

这些写法会先撞上一个直觉:表达式应该是“算出一个值”的东西,而 point.x = 10 这样的赋值看起来只是“做了一件事”,更像语句,怎么会是表达式?

因为在 Rust 里,“表达式”的范围比这个直觉宽。if 是表达式,花括号块是表达式,赋值也是表达式——它算出的值是 ()。反过来,上面列表里的 let 是语句,不是表达式;expression 那个位置上的才是表达式。“是不是表达式”本身没有区分度;有区分度的是求值的结果:求值时,有的表达式指向一个可以被读取、修改或借用的位置,有的表达式算出一个值。

Rust 把前一类叫作位置表达式(Place Expression),把后一类叫作值表达式(Value Expression)。这个区分能解释赋值、可变借用、解引用和字段移动为什么有不同的要求,也能解释为什么有的位置读得走值,有的读不走。

“表达式产生值”的直觉并没有全错。当一个位置表达式出现在需要值的地方,读取发生,它也就算出了一个值——直觉里的表达式,见到的都是这一层。

位置表达式是写明“程序里哪个位置”的表达式。变量名最直接;其余的写法都是从别的位置“走到”目标位置。位置不是地址——它存不进变量、传不了参、比不了大小,你能做的是读取它、往它写入、从它借出引用。

Rust 里能成为位置的形式一共五种:变量名、字段、索引、解引用、静态变量。下面一个一个看,最后回到它们的共同点。

count 指向这个绑定所代表的位置,mut 决定这个位置能不能写入:

//! Uncomment the marked line and compile to get error[E0384].
fn main() {
let mut count = 10;
count = 20;
let fixed = 10;
// fixed = 20; // error[E0384]: cannot assign twice to immutable variable `fixed`
println!("{count} {fixed}");
}
在 Rust Playground 中运行

count 是可变绑定,count = 20 写回原位置。fixed 没有 mut,写回被拒绝:error[E0384]: cannot assign twice to immutable variable `fixed` 。

point.x 从 point 的位置走到它的 x 字段。赋值写回的、&mut 借出的,都是这个字段位置:

struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 1, y: 2 };
point.x = 10;
let y = &mut point.y;
*y = 20;
println!("{}, {}", point.x, point.y);
}
在 Rust Playground 中运行

let mut point 让 point 的字段可以通过这个绑定被修改。&mut point.y 又从字段位置借出一段独占的可变访问权,*y = 20 把新值写回这个位置。

values[0] 从 values 按索引走到一个元素。索引表达式可以出现在赋值左侧,也可以作为可变借用的来源:

fn main() {
let mut values = vec![1, 2, 3];
values[0] = 10;
let first = &mut values[1];
*first += 20;
println!("{values:?}");
}
在 Rust Playground 中运行

这一步和字段访问不同:走哪个元素是运行时才算出来的,索引表达式能成为位置,并不意味着访问一定成功。

对 Vec 这类库类型,索引位置不是编译器直接摸到的内存,而是方法调用的语法糖(syntactic sugar):values[0] = 10 相当于 *values.index_mut(0) = 10——index_mut 做越界检查,返回一个 &mut i32,解引用再把它变成位置:

use std::ops::IndexMut;
fn main() {
let mut values = vec![1, 2, 3];
values[0] = 10;
*values.index_mut(1) = 20;
println!("{values:?}");
}
在 Rust Playground 中运行

两种写法做的是同一件事。这个视角也解释了越界 panic 为什么发生在运行时:检查写在 index_mut 的函数体里,编译器无法提前给出保证。

解引用:沿引用走到指向的位置

Section titled “解引用:沿引用走到指向的位置”

*reference 沿一个引用走到它指向的位置。上面字段那节的示例里,*y = 20 就是这个形式:y 是 &mut i32,*y 回到了 point.y 那个位置,所以写回的是 point.y 的值。

智能指针是否支持这种写法,取决于它是否提供了合适的 DerefMut 实现——和索引走的是同一条路:方法返回引用,解引用得到位置。

静态变量像变量名一样,直接指一个位置,只是这个位置全局可见,程序运行期间一直存在。它能读、能借,但移不出值——原因在「得到位置,不等于能拿走值」一节展开。

五种形式的共同点是两层约束:

  1. 表达式必须能指向一个位置;
  2. 这个位置必须允许当前操作——赋值需要可变位置,可变借用还要满足读写排他。

五种形式可以单独用,也可以串成一条链,从一个绑定出发,经过索引和字段访问,落到具体的位置:

struct Person {
name: String,
}
fn main() {
let mut people = [
Person { name: String::from("Alice") },
Person { name: String::from("Bob") },
];
people[0].name = String::from("Alicia");
let first_name = &mut people[0].name;
*first_name = String::from("Al");
println!("{}", people[0].name);
}
在 Rust Playground 中运行

people[0].name 先用索引选中数组里的一个元素,再用字段访问选中这个元素的 name 字段。赋值写回的是链末端的字段位置,&mut 借用的也是这个位置,而不是整个数组。链上任何一环失败——比如索引越界——整个表达式就取不到位置。

从 people 出发,一步索引、一步字段,落到 people[0].name;把索引换成越界的 [2],链断在这一环,整个表达式取不到位置。
people[0].name 怎么落到一个位置双轨:上面是表达式 people[0].name 的三段,下面是它导航的嵌套结构。每一步点亮一段、走进结构一层;第三步落到 name 字段——赋值与 &mut 作用的位置。第四步把索引换成 [2]:越界,链断在这一环,整个表达式取不到位置。表达式 · 每段各走一步结构people[0].name数组 · 绑定[0]Alice落点[1]Bob[2]越界 · 没有第 3 个元素

值表达式负责计算结果,不代表一个可以继续写入的存储位置。常见例子包括:

  • 字面量,如 42;
  • 算术运算,如 x + y;
  • 函数调用,如 String::from("hello");
  • 方法调用,如 values.len();
  • 结构体构造,如 Point { x: 1, y: 2 }。

例如,String::from("hello") 产生一个新的 String 值。let text = ... 再把这个值绑定到 text。let 不是把某个现成位置改成字符串;它建立了一个新的绑定。

这个区别也解释了为什么不能把任意表达式放在赋值左侧:42 = 10 没有可写入的位置,x + 1 = 10 也没有。

「值表达式不代表存储位置」只说了一半。计算结果总要有地方落:直接绑定给变量时,值落进变量的位置;当场使用时——比如对它取引用——值先落进一个临时位置(temporary)。把两半合起来,模型才是完整的:

每个值都落在某个位置里。位置表达式指向一个已经存在的位置;值表达式算出新值,落进为它准备的位置——变量绑定,或一个临时位置。

临时位置通常只活到所在语句结束。extended 那行是例外:

fn main() {
let text = String::from("hello");
let existing = &text; // borrows an existing place
let extended = &String::from("temp"); // borrows a fresh temporary
println!("{existing} {extended}");
}
在 Rust Playground 中运行

existing 借的是 text 这个已经存在的位置。extended 借的是 String::from 刚算出来的结果——它先落进临时位置,& 再指向它;绑定让这个临时位置活到了整个块结束。这条延长规则什么时候适用,属于后面的章节;这里只需要知道,引用一个值表达式的结果没有被禁止,代价是临时位置的存活时间要跟上来。

existing 借的是已经存在的位置 text;String::from("temp") 的结果先落进一个临时位置,& 再指向它,绑定把这个临时位置的存活拉长到块结束。
值表达式的结果落进临时位置两种借用。上面 existing 借的是已经存在的位置 text,活满整块;下面 String::from("temp") 先算出一个值,落进临时位置,extended 借它,这个临时位置从本语句结束被延长到块结束。块 { … }existingtexthelloextended本语句结束String::from("temp")临时位置temp被 let 绑定延长

位置表达式自己不产生值。当它出现在需要值的地方——赋值右侧、函数参数、println! 里——读取才会发生:从位置里取出值,交给这个上下文。

let target = source;

source 是位置表达式,let 需要一个值,于是读取发生。读出来的结果怎么处理,由类型决定:

  • i32 实现了 Copy,按位复制一份,原位置照旧可用;
  • String 没有实现 Copy,读取变成移动——值被取走,原位置作废,释放责任随之转移;
  • 想留下原来的绑定,就显式调用 .clone() 再造一份。

所以位置/值和复制/移动不是两组要分开背的概念,而是同一个动作的前后两半:位置表达式回答「从哪里取值」,类型决定「取走之后,原来的位置怎么办」。

字段本身也是位置。字段类型不是 Copy 时,从字段位置取出它会转移这个字段的值:

struct Pair(String, String);
fn main() {
let pair = Pair(String::from("left"), String::from("right"));
let left = pair.0;
println!("{left}");
println!("{}", pair.1);
}
在 Rust Playground 中运行

pair.0 中的 String 被移动给了 left,所以不能再次读取 pair.0。pair.1 是另一个字段,没有被移动,仍然可以使用。

这叫部分移动(partial move)。它不是“把整个结构体都移动走”,而是只从结构体中的某个位置取走了非 Copy 字段。结构体是否还能整体使用,要看剩余字段和具体操作。

let left = pair.0; 把 .0 的 String 移交给 left,原地留下一个失效空位;.1 没有被移动,仍然可用。
部分移动只拿走一个字段左边是绑定 left,右边是带两个 String 字段的 pair。把 .0 移出给 left 之后,这个字段变成虚线空位,.1 仍然填着、仍然可用。之后读 pair.0 会失败,读 pair.1 仍然可以。let left = pair.0;leftStringpair.0String.1Stringpair.0:值已移出,不能再读pair.1:没有被动过,仍可以用

解引用能得到位置,但位置里的值不一定属于你。text 是所有者时,r 只有一段共享的读取权:

//! Uncomment the marked line and compile to get error[E0507].
fn main() {
let text = String::from("hello");
let r = &text;
// let s = *r; // error[E0507]: cannot move out of `*r` which is behind a shared reference
println!("{r}");
}
在 Rust Playground 中运行

取消注释那一行,编译器拒绝:error[E0507]: cannot move out of `*r` which is behind a shared reference。*r 是位置,读取也想发生,但移动需要接手值的所有权,而 r 只有借用。想要一份完整的值,只能显式 r.clone()。

静态变量是同一族。它是全局可见的位置,能读、能借,但移出一次,全局状态就丢了值:

//! Uncomment the marked line and compile to get error[E0507].
static TEXT: String = String::new();
fn main() {
// let s = TEXT; // error[E0507]: cannot move out of static item
println!("{TEXT}");
}
在 Rust Playground 中运行

同样被 error[E0507] 拒绝:cannot move out of static item `TEXT` 。

有一个例外:Box。*boxed 移出值是允许的:

fn main() {
let boxed = Box::new(String::from("hello"));
let s = *boxed;
println!("{s}");
}
在 Rust Playground 中运行

Box 承诺堆上的值只有这一个所有者,解引用移出只是把这份所有权换个名字拿,不会出现第二个释放责任。这是语言对 Box 的特殊处理,其他智能指针没有这个待遇——所以它们要靠 DerefMut 借出写回权,而不是直接移出。

同一个「把值移走」,三个来源三种结局:共享引用和静态变量被 E0507 拒绝,Box 放行。
同一个移出动作,三种结局三行。`let s = *r` 在共享引用上被 E0507 拒绝;`let s = TEXT` 在静态变量上被 E0507 拒绝;`let s = *boxed` 在 Box 上通过,值移出给 s。let s = *r;r: &String · 共享引用E0507只有共享借用,移不出let s = TEXT;TEXT: static String · 静态变量E0507静态变量,移不出let s = *boxed;boxed: Box<String> · Boxs值移出,s 拿到所有权

同一个表达式在不同上下文中,编译器会要求不同的能力:

  • 赋值左侧需要可写的位置;
  • &mut 后面需要可以独占借用的位置;
  • 读取位置时,可能复制它的值,可能移动它的值,也可能被拒绝移出;
  • & 借用位置时,不会接手位置中值的所有权;
  • 解引用可以把一个引用或智能指针带回它指向的位置。

这些能力仍然受第一篇的规则限制。一个位置存在,不代表它总能被修改、移动或借用。

reference 的类型是 &mut Point,但 reference.x 可以直接编译:

struct Point {
x: i32,
y: i32,
}
impl Point {
fn show(&self) {
println!("({}, {})", self.x, self.y);
}
}
fn main() {
let mut point = Point { x: 1, y: 2 };
let reference = &mut point;
reference.x = 10;
reference.show();
}
在 Rust Playground 中运行

字段访问会自动解引用(auto-deref):reference.x 和 (*reference).x 指向同一个位置,编译器替你补上了 * 这一步。沿着引用读字段、写字段,都不需要先手动解引用。

方法调用同理。reference.show() 的接收者由编译器调整,不需要手写成 (*reference).show()。5.3 的 &self 方法用起来没有额外的解引用负担,就是这条规则在起作用。

4.1 讲 let、mut、Copy、Move 和 Clone 时,先区分“绑定”和“绑定中的值”。位置表达式把这个模型再往前推进一步:

变量、字段、索引和解引用都可以成为值的来源,但它们是否能被写入,取决于可变性和借用关系。

这也是结构体方法、切片索引和 DerefMut 能够接回所有权模型的原因。

“位置”是语义上的存储位置,不是对机器布局的承诺。编译器可能把值放进寄存器、消除存储,或者合并访问;只要程序的可观察行为符合 Rust 规则即可。

链的“一环一环走”也是语义上的分解,不是运行期的执行顺序。编译器在编译期沿链算出最终位置,字段和常量索引通常折叠成一次地址计算——people[0].name 的写回,往往就是对 people 的地址加一个常量偏移。链上真正产生运行期动作的是指针环节:沿引用解引用要读一次指针,Vec 的索引要先过 index_mut 的越界检查。

位置表达式也不等于“永远有一个合法地址”。索引可能越界,解引用可能违反借用关系,位置中的值也可能已经被移动。表达式的类别只说明它参与了哪种语义,不替你证明操作一定成功。