一句话总结
所有权(Ownership)是 Rust 在编译期管理内存的规则,替代了 Go 的 GC:每块内存有且只有一个所有者,所有者离开作用域时内存自动释放.
堆类型赋值/传参会移动(Move) 所有权使原变量失效;栈上的简单类型则因实现了 Copy 而按位复制.这是整个 Rust 的立身之本.
从一个 Go 程序的困惑出发
在 Go 里这段代码再普通不过:
|
"翻译"成 Rust,却会被编译器拦下:
|
同样是赋值,为什么 Rust 报错?答案就是所有权.理解了这个差异,就推开了 Rust 的大门.
Go 的赋值像复印文件:复印件和原件各自独立,GC 之后统一清理.
Rust 的 let s2 = s1 像移交唯一的纸质合同:合同只有一份,交给 s2 后,s1 手里就空了,再去看 s1 自然不行.
所有权三条规则
整个系统建立在三条规则上,先记住,后面所有现象都由此推导:
- Rust 中每个值都有一个所有者(owner).
- 同一时刻,值有且只有一个所有者.
- 当所有者离开作用域(scope),值被自动释放(drop).
第三条最关键:
|
没有 GC,也没有手动 free.释放点由作用域在编译期确定,运行时零额外开销--这是 Rust "零成本抽象"的第一个体现.
drop 像自动的 defer Close
它有点像 Go 的 defer file.Close():离开作用域自动清理.区别在于--Go 要手动写 defer,且只针对显式声明的资源;Rust 对所有拥有堆资源的值自动插入 drop,无需操心.
需要自定义清理逻辑时,可以实现 Drop trait(后续会遇到).
栈,堆与移动的本质
要理解为什么 String 赋值会"移动",得看清它的内存布局.String 分两部分:
|
栈上是一个"胖指针"(指向堆的指针 + 长度 + 容量),真正的字符内容在堆上.let s2 = s1 时,Rust 只复制栈上那三个字段,不复制堆数据(否则大字符串赋值会很昂贵).
于是 s1 和 s2 的指针指向同一块堆内存.若两者各自在离开作用域时 drop 一次,同一块内存就被释放两次(double free)--经典内存漏洞.Rust 的解法干脆:让 s1 失效,只有 s2 拥有这块数据.这个过程叫移动(Move),不是浅拷贝.
移动语义保证"任何时刻,一块堆内存只有一个所有者负责释放它".
这正是 Rust 在没有 GC 的前提下,杜绝二次释放与悬垂指针的核心机制.
Copy:为什么整数不会被移动
立刻试探:整数赋值是不是也会失效?
|
整数没被移动.因为 i32 这类类型完全在栈上,大小固定,复制廉价,没有堆数据要管理.Rust 为它们实现了 Copy trait:赋值时按位复制一份,原值依然有效.
常见的 Copy 类型:
- 所有整数,浮点,布尔,字符(
char) - 仅由
Copy类型组成的元组,如(i32, bool) - 不可变引用
&T(下一篇详述)
而 String,Vec<T> 这类管理堆内存的类型,刻意没有实现 Copy,因此走移动.
判断口诀与 Go 的微妙差异
值是否"纯栈上,可廉价复制",决定它走 Copy 还是 Move.
对照 Go:Go 的 int 赋值是值拷贝(类似 Copy),但 Go 的 slice,map 赋值是共享底层(既非 Copy 也非 Move,而是共享 + GC 兜底).Rust 不允许这种"悄悄共享"--要么独占所有权(Move),要么显式借用(下一篇).
Clone:需要副本时显式深拷贝
如果确实想要两份独立的堆数据,用 .clone() 显式深拷贝.Rust 强制深拷贝必须显式,正是为了让"这里有一次昂贵的内存复制"在代码里清晰可见:
|
最佳实践:不要用 clone 来消灭借用报错
新手遇到所有权报错时,常无脑 .clone() 绕过--这能编译通过,但可能掩盖了本该用借用解决的问题,带来不必要的内存复制.
正确顺序是:先想"能不能借用(下一篇)",借用解决不了再考虑 clone.把 clone 当成"我确实需要一份独立副本"的明确表达,而非"编译器闭嘴"的万能药.
函数调用中的移动
移动不只发生在赋值,把值传给函数同样移交所有权:
|
函数也能通过返回值把所有权还回来:
|
这套"传进去,再传回来"很快会让人厌烦--只想读一下数据,却要交还所有权.这正是借用登场的理由,也是下一篇的主题.
与 Go 的整体对照
| 维度 | Go | Rust |
|---|---|---|
| 内存回收 | 运行时 GC,自动但有停顿 | 所有权 + drop,编译期确定,零运行时开销 |
| 赋值 String | 共享底层字节,GC 兜底 | 移动所有权,原变量失效 |
| 赋值 int | 值拷贝 | Copy,值拷贝(行为一致) |
| 深拷贝 | 需手动逐字段复制 | 显式 .clone() |
| 传参 | 默认值拷贝;传指针则共享 | 默认移动;借用见下一篇 |
快速回顾
- 三规则:一值一主,唯一所有者,所有者离开作用域即 drop(编译期确定,零开销).
- 移动(Move):堆类型赋值/传参转移所有权,原变量失效,杜绝二次释放.
- Copy:纯栈上,可廉价复制的类型(整数等)赋值不失效;堆类型刻意不实现 Copy.
- Clone:显式深拷贝;不要用它来掩盖本该借用的问题.
- 函数传参:传值即移动所有权,可通过返回值交还--繁琐之处正由借用解决.
动手练习
- 移动报错:创建一个
String,赋值给另一变量后尝试使用原变量,完整读懂编译器的 "moved value" 报错. - Copy 验证:把上题的
String换成i32,确认不再报错,并解释 Copy 与 Move 的区别. - 函数移动:写一个接收
String(取得所有权)的函数,在main中调用后尝试再用该变量,观察报错. - Clone 实验:用
.clone()让上题的变量在传参后仍可用,并思考这次 clone 是否真的必要. - 所有权往返:写一个"传入 String,处理后再返回"的函数,体会这种所有权往返的繁琐(为下一篇借用做铺垫).