5375 字
27 分钟
Rust学习笔记:所有权

Rust 把”内存什么时候释放”这个问题,从运行期的程序员负担和 GC 负担,搬到了编译期的类型系统里。这套机制叫所有权(ownership)

我自己是 C++ 背景,对 C++11 引入的右值引用和移动语义只是模糊地用过几次。这篇笔记就以”C++ 程序员视角”为主线,把 Rust 的所有权、借用、智能指针,和 C++ 里大致对应的概念逐一对照。

1. 前言:为什么需要所有权?#

主流的内存管理大致三种范式:

  1. 手动管理:C、C++ 早期风格,malloc/freenew/delete。性能最好,但容易写出泄漏、悬垂指针、double free。
  2. GC:Java、Go、Python 这一派。程序员不用管释放,运行时帮你扫。代价是停顿、内存占用、以及”对象什么时候真的没了”不可预测。
  3. 所有权:Rust 的路线。编译器在编译期通过静态规则保证内存安全,运行期没有 GC。

Rust 的设计目标可以一句话概括:内存安全 + 零成本抽象 + 不要 GC。这三个目标互相拉扯,最终落点就是所有权系统。

直觉上,所有权 ≈ 把 C++ 里靠 RAII + 编码规范实现的东西,做成语言强制规则。RAII 思想 Rust 几乎照搬,但 Rust 多走了一步:用编译器替你检查那些 RAII 没法强制的事。

2. 所有权的三条规则#

Rust 的所有权可以浓缩成三句话:

  1. 每个值有且仅有一个所有者(owner)
  2. 同一时刻只能有一个所有者
  3. 所有者离开作用域时,值被自动 drop
fn main() {
{
let s = String::from("hello"); // s 是 "hello" 的所有者
println!("{}", s);
} // 作用域结束,s 被 drop,堆上的 "hello" 被释放
// 此处再用 s 编译错误:s 已不在作用域
}

C++ 里写类似代码是这样:

{
std::string s = "hello"; // 构造
std::cout << s << "\n";
} // 作用域结束,~string() 释放堆内存

两者看起来一样:都靠”离开作用域 → 析构”来释放资源,这就是 RAII。区别在于:

  • C++ 不强制”唯一所有者”。一个对象可以被多个变量持有副本,也可以被裸指针四处指引用,规则全靠程序员自觉。
  • Rust 把”唯一所有者”做成了编译期硬规则,违反就编译不过。

3. Move 语义:Rust vs C++#

这是整套所有权机制最核心、也最容易被 C++ 程序员误读的部分。

3.1 Rust 的 move(默认行为)#

fn main() {
let s1 = String::from("hello"); // s1 拥有堆上的 "hello"
let s2 = s1; // 所有权 move 到 s2
// println!("{}", s1); // ❌ 编译错误:value borrowed here after move
println!("{}", s2); // ✅ 现在 s2 是所有者
}

赋值 let s2 = s1 在 Rust 里默认就是 move(除非 s1 的类型实现了 Copy)。move 之后,编译器把 s1 标记为”已移动”,再使用它直接编译错误

底层上发生了什么?String 实际是 (ptr, len, cap) 三元组放在栈上,数据在堆上。move 只是把这三个字段按位拷给 s2,堆上的 "hello" 一个字节都没动。原本 s1 持有的指针现在归 s2,于是 s1 必须失效——否则两个变量持有同一个堆指针,离开作用域时会被 free 两次(double free)。

3.2 C++ 的 move(需要显式 std::move)#

#include <iostream>
#include <string>
#include <utility>
int main() {
std::string s1 = "hello";
std::string s2 = std::move(s1); // 显式 move
std::cout << s2 << "\n"; // ✅ s2 拥有 "hello"
std::cout << "s1.size() = "
<< s1.size() << "\n"; // ⚠️ 合法,但值未定义
return 0;
}

C++ 的规则截然不同:

  • 默认赋值是拷贝(copy ctor / copy assignment)。需要 std::move(...) 把左值显式转成右值,才会触发移动构造/移动赋值。
  • move 之后,原对象处于标准说的 “valid but unspecified” 状态。意思是:还能调析构、还能再赋值给它,但读它的值是未定义结果。
  • 编译器不会报错你访问 moved-from 对象,这类 bug 全靠程序员(或 clang-tidy 这类静态分析)自觉避免。

3.3 两者并排对比#

下面这张图把”move 前后”的内存与变量状态对比画出来:

move semantics: Rust vs C++

RustC++
默认赋值语义movecopy
需要显式 move 吗?不需要需要 std::move
moved-from 后能不能用?用即编译错误合法,但状态未定义
谁来保证 use-after-move 不出 bug?编译器程序员
是否需要写 move ctor?不需要类型设计者要写

一句话总结这一节:Rust 把 C++ 里的”移动语义”做成了默认行为,并且用编译期检查替你管住了 use-after-move。

4. Copy vs Move:什么时候不是 move?#

并不是所有 Rust 类型在赋值时都会 move。如果类型实现了 Copy trait,赋值就变成按位拷贝,原变量仍然可用:

fn main() {
let x: i32 = 5;
let y = x; // i32 实现了 Copy,这里是按位拷贝
println!("{} {}", x, y); // ✅ x 和 y 都能用
}

哪些类型自动是 Copy?规则简单:

  • 所有简单标量:整数、浮点、boolchar
  • 全部由 Copy 类型组成的元组、数组、结构体(且类型本身没有手写 Drop)。
  • 不可变引用 &T 也是 Copy(注意:&mut T 不是)。

这里的 Drop 是 Rust 的一个 trait,类似 C++ 的析构函数。当值离开作用域时,Rust 会自动调用 drop——如果类型实现了 Drop,就执行用户自定义的清理逻辑(例如释放堆内存、关闭文件句柄)。

struct MyResource {
name: String,
}
impl Drop for MyResource {
fn drop(&mut self) {
println!("释放资源:{}", self.name); // 类似 C++ 析构函数
}
}
fn main() {
let a = MyResource { name: String::from("A") };
{
let b = MyResource { name: String::from("B") };
} // b 在这里离开作用域 → 打印"释放资源:B"
println!("main 结束前");
} // a 在这里离开作用域 → 打印"释放资源:A"
// 输出顺序:
// 释放资源:B
// main 结束前
// 释放资源:A

CopyDrop 不能共存,原因很直接:Copy 意味着任意按位复制,Drop 意味着离开作用域要做特殊清理——两者叠加会导致同一份资源被 drop 多次。下面这段代码编译器直接拒绝:

#[derive(Clone, Copy)] // ❌ 编译错误:Copy 和 Drop 不能同时实现
struct Bad {
ptr: *mut i32, // 裸指针,假设指向堆上的 i32
}
impl Drop for Bad {
fn drop(&mut self) {
unsafe { drop(Box::from_raw(self.ptr)); } // 释放堆内存
}
}
// 如果 Bad 能被 Copy,那么:
// let x = Bad { ... };
// let y = x; // 按位复制,x 和 y 持有同一个 ptr
// // 离开作用域时 x 和 y 各 drop 一次 → double free → 崩溃

反过来,只要持有堆资源、或者实现了 Drop,就一定不是 Copy。所以 StringVec<T>Box<T>File 这些默认是 move。

对比 C++:默认所有类型都是 copy(编译器自动生成 copy ctor)。要禁用 copy,得显式 T(const T&) = delete;。哲学上正好相反——Rust 默认”动一动权属”,C++ 默认”复制一份”。

5. 函数调用中的所有权转移#

函数参数和返回值同样遵循 move 规则。

fn take_ownership(s: String) {
println!("我拿到了:{}", s);
} // s 在这里离开作用域,被 drop
fn give_back(s: String) -> String {
println!("处理中:{}", s);
s // 把所有权再 move 出去(return)
}
fn main() {
let a = String::from("hello");
take_ownership(a);
// println!("{}", a); // ❌ a 已经被 move 进 take_ownership
let b = String::from("world");
let b = give_back(b); // 把 b 借进去,又拿回来
println!("{}", b); // ✅ b 还活着,因为又 move 回来了
}

对于 Copy 类型则是另一回事:

fn add_one(n: i32) -> i32 { n + 1 }
fn main() {
let x = 10;
let y = add_one(x); // i32 是 Copy,x 没被 move
println!("{} {}", x, y); // ✅
}

C++ 的对应是:参数按值传递 = copy(除非显式 T&& + std::move),返回大对象有 RVO/NRVO 优化,必要时编译器自动选择 move。Rust 没有 RVO/NRVO 的复杂规则,它本来就是 move,简单干净。

不过函数每次都把所有权传进传出,写起来非常啰嗦。这就引出了下一节——借用

6. 借用(Borrowing):解决”传进去就拿不回来”#

借用的核心想法很朴素:我把值借给你用一下,所有权还是我的。Rust 用 &&mut 两种引用来表达。

fn length(s: &String) -> usize { // 借用,不获取所有权
s.len()
}
fn append_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let s = String::from("hello");
let n = length(&s); // 把 s 不可变借用给 length
println!("{} 长度 {}", s, n); // ✅ s 没被 move
let mut s = String::from("hello");
append_world(&mut s); // 把 s 可变借用给 append_world
println!("{}", s); // ✅ "hello world"
}

借用规则只有两条,但杀伤力极大:

  1. 不可变借用 &T 可以同时有任意多个(读不冲突)。
  2. 可变借用 &mut T 同一时刻最多只有一个,并且不能与任何 &T 共存(写排他)。

这两条规则的本质就是”读写锁的编译期版本”,目标是杜绝数据竞争和迭代器失效一类的问题。

下面是一个会编译错误的例子:

fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // ❌ cannot borrow `s` as mutable because it is also borrowed as immutable
println!("{} {} {}", r1, r2, r3);
}

借用规则用一张图能更直观:

borrowing rules

对比 C++:const T& 大致对应 &TT& 大致对应 &mut T。但 C++ 完全没有借用检查器——你可以同时拥有 100 个 T&,可以 const_castconst 撬掉,可以让引用比被引用对象活得更长(典型的悬垂引用,UB)。Rust 把这一整类问题用借用检查器(borrow checker)一刀切掉了。

顺带一提:C++ 引用必须在初始化时绑定且不能改绑,但 Rust 引用可以被重新赋值(让变量本身绑到另一个引用),这点和 C++ 不一样。

7. 生命周期标注(简介)#

借用必须比被借用者活得短。这是借用检查器的另一条铁律:

fn dangling() -> &String { // ❌ 不能返回栈上局部变量的引用
let s = String::from("oops");
&s
} // s 在这里 drop,返回的引用立刻悬空

当一个函数同时拿多个引用并返回引用时,编译器需要知道返回的引用究竟和哪个参数绑定。这就需要生命周期标注 'a

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string is long");
let result;
{
let s2 = String::from("short");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // ✅ 在 s2 作用域内是合法的
}
// println!("{}", result); // ❌ s2 已 drop,result 可能悬空
}

'a 不是某段时间的实际值,它只是个标签,告诉编译器:“这些引用的有效期必须满足同一个生命周期 'a”。具体生命周期由编译器从调用处推断。

生命周期是 Rust 学习曲线里最陡的一段,本文不展开,留作后续笔记。这里只需要知道:借用检查器除了管”谁能借”,还管”借多久”,多出来的那部分约束用 'a 表达

8. 智能指针:所有权的不同形态#

到目前为止讲的所有权和借用,都是建立在”普通栈变量 + 借用”的基础上的。但很多场景需要更灵活的所有权形态:堆上独占、共享所有权、跨线程共享、运行时借用检查……Rust 把这些用一组智能指针类型表达。对 C++ 程序员来说,这一节大体是熟悉地形,区别在于 Rust 把语义切得更细

8.1 Box<T>std::unique_ptr<T>#

Box<T> 是最简单的智能指针:把一个值放到堆上,唯一所有权

fn main() {
let b: Box<i32> = Box::new(42);
println!("{}", *b); // 解引用得到 42
} // b 离开作用域,堆上 i32 被释放

典型用途:递归类型。因为 enum/struct 的大小必须在编译期确定,而递归类型的大小是无穷的,所以必须借助堆上指针打破:

enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(
Cons(2, Box::new(
Cons(3, Box::new(Nil))))));
// 略
let _ = list;
}

对比 C++ 的 unique_ptr

#include <memory>
struct Node {
int value;
std::unique_ptr<Node> next;
};
int main() {
auto head = std::make_unique<Node>(Node{
1, std::make_unique<Node>(Node{
2, std::make_unique<Node>(Node{3, nullptr})
})
});
return 0;
}

两者语义几乎一一对应:

  • 堆上独占。
  • move 是默认行为(C++ 的 unique_ptr 禁用了 copy)。
  • 析构时自动释放。

差别主要在写法层面:Rust 的 Box 用得更自然,因为 move 本来就是默认行为;C++ 要严守”不要 copy unique_ptr”的纪律。

8.2 Rc<T> / Arc<T>std::shared_ptr<T>#

当一个值需要被多个所有者共享时,用引用计数:

use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("shared"));
println!("count = {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a); // 不是深拷贝,只是计数 +1
println!("count = {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a);
println!("count = {}", Rc::strong_count(&a)); // 3
} // c drop,计数 -1
println!("count = {}", Rc::strong_count(&a)); // 2
} // a、b drop,计数 → 0,String 被释放

注意 Rc::clone(&a) 不是深拷贝整个 String,只是把引用计数 +1,然后返回一个新的 Rc 句柄。Rust 故意没有重载赋值,写 Rc::clone(&a) 比写 a.clone() 更显眼,目的是让 reader 一眼看出”这里有一次计数操作”。

Rc<T> 只能在单线程内用,因为它的计数是非原子的。跨线程要用 Arc<T>

use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("thread {} sees {:?}", i, data);
}));
}
for h in handles { h.join().unwrap(); }
}

对比 C++ 的 shared_ptr

#include <memory>
#include <vector>
int main() {
auto data = std::make_shared<std::vector<int>>(
std::vector<int>{1, 2, 3});
auto copy = data; // 计数 +1
// 跨线程也是同样的 shared_ptr,因为它内部一直是原子计数
}

关键差异

  • C++ 的 shared_ptr 始终使用原子引用计数,不管你是不是在多线程环境。在单线程场景下这就是无谓的性能损耗。
  • Rust 把它切成两种类型Rc<T> 用普通整数计数(快),Arc<T> 用原子计数(线程安全)。编译器靠 Send/Sync trait 检查你有没有把 Rc 跨线程传递——传了就编译错误。
  • 一句话:Rust 把”是否线程安全”做成了类型层面的区分

8.3 Weak<T>std::weak_ptr<T>#

Rc/Arc 共享所有权时,如果两个对象互相 Rc 持有,就会形成循环:strong count 永远归不了零,对象永远不释放。这就是经典的循环引用泄漏

Weak<T> 解决这个问题:它持有一个不计入 strong count 的弱引用。用 Weak 访问对象时要 upgrade(),如果对象已经被释放,得到的就是 None

下图把”正常共享 / 循环引用 / 用 Weak 解开循环”三种情况画在一起:

Rc reference count and Weak

代码层面的标准例子是父子节点树。子节点要能访问父节点,但又不能持有父节点的强引用——否则就形成环。

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 👈 弱引用,避免循环
children: RefCell<Vec<Rc<Node>>>, // 👈 强引用,父持有子
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()), // 暂时没父节点
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]), // 把 leaf 强持有
});
// leaf 用 Weak 指向 branch
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// 通过 upgrade() 访问父节点
println!(
"leaf 的父节点 value = {}",
leaf.parent.borrow().upgrade().unwrap().value
);
}

对比 C++ 的 weak_ptr:动机一致,用法相似,都是用来打破 shared_ptr 之间的循环。

#include <memory>
#include <vector>
struct Node {
int value;
std::weak_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
};

8.4 RefCell<T> / Cell<T>:内部可变性#

Rust 的借用检查规则是编译期生效的。但有些场景下,借用规则在静态分析下过不去、但运行时确实不会冲突——这时候需要把检查推迟到运行期。这就是内部可变性(interior mutability)

RefCell<T> 是最常见的工具:它允许通过 &self(不可变引用)去修改内部的值,但内部的”借用”由运行时维护。

use std::cell::RefCell;
struct Counter {
count: RefCell<i32>, // 即使外部只持有 &Counter,内部也能改
}
impl Counter {
fn increment(&self) {
*self.count.borrow_mut() += 1; // 注意是 &self,不是 &mut self
}
fn get(&self) -> i32 {
*self.count.borrow()
}
}
fn main() {
let c = Counter { count: RefCell::new(0) };
c.increment();
c.increment();
c.increment();
println!("{}", c.get()); // 3
// 运行时借用冲突会 panic,而不是编译错误:
let _b1 = c.count.borrow_mut();
// let _b2 = c.count.borrow_mut(); // ⚠️ 运行时 panic: already borrowed
}

Cell<T> 是更轻量的版本,只支持把整个值”换出/换入”,不返回内部引用,所以不会运行时 panic,但只适合 Copy 类型或要 replace 的场景。

C++ 里没有直接对应物。最接近的概念是类成员上的 mutable 关键字——它允许 const 方法修改某个成员,常见于缓存计算结果。但 mutable 完全没有 Rust RefCell 的运行时借用检查。

典型的”共享 + 可变”组合是 Rc<RefCell<T>>:多个所有者共享一份可变数据。

use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push(4);
b.borrow_mut().push(5);
println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
}

跨线程版本是 Arc<Mutex<T>>Arc<RwLock<T>>,思路一样:Arc 管共享所有权,Mutex/RwLock 管并发访问。

8.5 小结#

用途RustC++
堆上独占Box<T>unique_ptr<T>
引用计数(单线程)Rc<T>shared_ptr<T>(原子,偏重)
引用计数(多线程)Arc<T>shared_ptr<T>
弱引用Weak<T>weak_ptr<T>
内部可变性RefCell<T> / Cell<T>无直接对应(mutable 成员勉强类似)
共享 + 可变(单线程)Rc<RefCell<T>>shared_ptr<T>(语言不强制)
共享 + 可变(多线程)Arc<Mutex<T>>shared_ptr<T> + mutex

实际工程里挑哪个,可以参考下面这张决策树:

smart pointer decision tree

9. 实战代码:用所有权改写一个 C++ 树#

把前面所有的概念组合起来,做一个”父子节点都能互相访问”的树结构。这是一个最能体现智能指针组合用法的场景。

C++ 版本#

#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct TreeNode {
std::string name;
std::weak_ptr<TreeNode> parent; // 弱引用避免循环
std::vector<std::shared_ptr<TreeNode>> children;
};
void add_child(const std::shared_ptr<TreeNode>& parent,
const std::shared_ptr<TreeNode>& child) {
child->parent = parent;
parent->children.push_back(child);
}
int main() {
auto root = std::make_shared<TreeNode>();
root->name = "root";
auto a = std::make_shared<TreeNode>();
a->name = "A";
add_child(root, a);
auto b = std::make_shared<TreeNode>();
b->name = "B";
add_child(a, b);
// 从 b 反向走到 root
if (auto p = b->parent.lock()) {
std::cout << "b 的父节点是 " << p->name << "\n"; // A
if (auto pp = p->parent.lock()) {
std::cout << "b 的祖父是 " << pp->name << "\n"; // root
}
}
}

Rust 版本#

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct TreeNode {
name: String,
parent: RefCell<Weak<TreeNode>>, // 弱引用,避免循环
children: RefCell<Vec<Rc<TreeNode>>>, // 父持有子(强引用)
}
impl TreeNode {
fn new(name: &str) -> Rc<Self> {
Rc::new(Self {
name: name.to_string(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
})
}
}
fn add_child(parent: &Rc<TreeNode>, child: &Rc<TreeNode>) {
// 子节点记录父亲(弱引用)
*child.parent.borrow_mut() = Rc::downgrade(parent);
// 父节点持有子节点(强引用)
parent.children.borrow_mut().push(Rc::clone(child));
}
fn main() {
let root = TreeNode::new("root");
let a = TreeNode::new("A");
let b = TreeNode::new("B");
add_child(&root, &a);
add_child(&a, &b);
// 从 b 反向走到 root
if let Some(p) = b.parent.borrow().upgrade() {
println!("b 的父节点是 {}", p.name); // A
if let Some(pp) = p.parent.borrow().upgrade() {
println!("b 的祖父是 {}", pp.name); // root
}
}
}

并排看下来,两边代码量、结构、思路几乎一样:

  • C++ 用 shared_ptr + weak_ptr;Rust 用 Rc + Weak
  • 修改 children / parent:C++ 直接改成员(成员本身不是 const);Rust 因为只有 &Rc<TreeNode>(外层不可变),必须套一层 RefCell 才能改内部。
  • 安全性:两边都避免了循环引用,但 Rust 还顺便保证了”没有数据竞争”和”没有 use-after-free”——前者是 Send/Sync 管的(这个例子是单线程,没体现),后者是 borrow checker + Weak::upgrade 返回 Option 共同保证的。

一个细节:C++ 的 weak_ptr::lock() 返回 shared_ptr(可能为 nullptr),Rust 的 Weak::upgrade() 返回 Option<Rc<T>>。语义对齐,只是 Rust 用类型系统让”可能为空”显式化了,不能忘记判空。

10. 总结:C++ 程序员视角的 Rust 所有权#

把前面的内容串起来,C++ → Rust 的”心智迁移”大致是这样一张映射:

C++ 里的概念/坑Rust 里如何处理
RAII(析构释放资源)沿用,更彻底
默认 copy,需要 delete 才禁拷默认 move,自动派生 Copy 才会拷
std::move 把左值转右值不需要显式 move,赋值就是 move
use-after-move 是程序员责任编译期检查,违反就 build 失败
T& / const T& 容易悬垂borrow checker + 生命周期管住
unique_ptr<T>Box<T>
shared_ptr<T>(恒为原子)Rc<T>(单线程)+ Arc<T>(多线程)切开
weak_ptr<T>Weak<T>
mutable 成员(const 方法里修改)RefCell<T> / Cell<T>,但有运行时检查
多线程共享 = shared_ptr<T> + mutex 自己组合Arc<Mutex<T>>,类型层面就拆分清楚

最后总结三条心得:

  1. 先按”所有权流向”思考。每个值有唯一所有者,函数调用、赋值、返回都是所有权的移动或借用,写代码时脑子里先画一遍这条流向,再看是否需要借用。
  2. 能借用就别 move。借用是几乎零成本的,move 偶尔需要重新构造,能用 &T/&mut T 就用。
  3. 智能指针是”逃生通道”,不是默认选择RcRefCell 都引入了一些运行时成本(计数、运行时借用检查),普通代码里用栈变量 + 借用 + Box 就已经能搞定大部分场景;只有真的需要”共享所有权”或”绕过编译期借用检查”时再上 Rc<RefCell<T>>

如果你已经熟悉 C++11 的移动语义和智能指针,那么 Rust 所有权对你不是新东西——更像是把那些 “我本来就应该这么做”的规矩 做成了语言层面的强制约束。代价是写代码的时候编译器经常打你的手,但对应的好处是:编译过了,那一类问题基本就不存在了

Rust学习笔记:所有权
https://blog.gzher.com/posts/rust-ownership/
作者
中会
发布于
2026-05-29
许可协议
CC BY-NC-SA 4.0