Rust中为何允许对临时值的数据成员执行赋值操作?
为什么这段Rust代码中的赋值操作是被允许的?
问题复现
首先看你给出的第一段可编译代码:
#[derive(Copy, Clone)] pub struct X { pub a: u8, } fn main() { let x = X { a: 0 }; { x }.a = 5; assert!(x.a == 5); // 断言可以通过,你之前预判的错误并不存在 }
你最初的疑问是:这里是不是在给临时值赋值?同时你发现下面两种写法无法通过编译:
({ x.a }) = 5; let mut y: u8 = 0; ({ y }) = 5;
你提到自己曾在写unsafe代码时遇到相关困惑:为了缩窄unsafe块范围,你写了如下代码,一度以为这是bug:
unsafe { *some_struct }.mem1 = 5;
而没有选择你当时认为更稳妥的写法:
unsafe { *some_struct.mem1 = 5; }
核心原因:Rust的位置表达式与值表达式区分
Rust的表达式严格分为两类,这是理解这个行为的核心:
- 位置表达式(Place Expression):代表一块真实存在的内存位置,有对应的内存地址,可以被写入、被借用,也就是常说的“左值”。
- 值表达式(Value Expression):代表一个具体的值,可能是寄存器里的临时值、也可能是计算产生的中间结果,没有固定的可写入内存地址,也就是常说的“右值”,不能出现在赋值号左侧。
为什么{ x }.a = 5合法?
- 块表达式的语义规则:如果块的最后一个表达式是位置表达式,且块内没有对这个位置做移动、拷贝产生新值的操作,整个块表达式本身也是位置表达式,直接指向原表达式对应的内存位置,不会产生临时值。
这里x是栈上X实例的绑定,本身是位置表达式;块{ x }没有任何额外操作,所以整个块是指向原x变量内存的位置表达式,不是临时拷贝。 - 对结构体的位置表达式做字段访问
.a,得到的依然是位置表达式,指向结构体对应字段的内存地址。 - 赋值号左侧要求必须是位置表达式,因此这里的赋值直接修改原
x的a字段,断言可以正常通过,不存在“给临时值赋值”的问题。
为什么后两种写法不合法?
这两种写法的块返回的都是值表达式,不符合赋值左值要求:
- 对于
({ x.a }) = 5:x.a是u8类型,实现了Copytrait。当它作为块的最后一个表达式时,上下文要求返回一个值,因此会触发拷贝,产生一个临时的u8值,整个块是值表达式,无法被赋值。 - 对于
({ y }) = 5:同理,y是实现了Copy的u8类型,块返回y时会自动拷贝出临时值,属于值表达式,不能放在赋值号左侧。
关于你提到的unsafe代码的误区
你当时写的unsafe { *some_struct }.mem1 = 5不仅不是bug,反而是完全正确的写法:
- 块表达式(包括unsafe块)的优先级高于字段访问运算符
.,因此代码会先执行unsafe块内的*some_struct解引用操作,得到指向指针目标结构体的位置表达式,整个unsafe块返回这个位置,不会产生临时结构体拷贝。 - 后续的
.mem1 = 5直接修改原结构体的对应字段,逻辑完全符合预期,同时把unsafe操作限制在了最小范围内,是非常规范的写法。
反而你当时想改成的unsafe { *some_struct.mem1 = 5; }才是有bug的写法:Rust中.运算符优先级高于解引用运算符*,这段代码实际等价于unsafe { *(some_struct.mem1) = 5; },也就是先访问some_struct的mem1字段再解引用,和你“先解引用结构体指针、再访问字段”的预期逻辑完全相反。
关键规则总结
- 赋值号左侧必须是位置表达式,代表可写入的内存位置。
- 块表达式的类型(位置/值)由最后一行表达式决定,只要最后一行是位置表达式且没有触发拷贝/移动,整个块就是位置表达式。
Copy类型在值上下文中会自动拷贝产生临时值,此时表达式从位置表达式转为值表达式。- 涉及指针解引用、字段访问的unsafe代码要特别注意运算符优先级:
.优先级高于*,混写时要么用块包裹解引用操作(就是你最初的写法),要么手动加括号(*ptr).field明确优先级,不要为了缩窄unsafe块写出有逻辑错误的代码。
内容的提问来源于stack exchange,提问作者nicolai
相关产品推荐
相关产品推荐

