Rust内存模型入门:所有权、借用与生命周期三权分立
1. 这不是又一门“语法糖”语言:为什么 Rust 的入门第一课必须从内存模型开始讲起
Rust: Zero to Hero Basic Introduction in a New Programming Language (Part 1/3)——这个标题里藏着一个被绝大多数新手忽略的关键信号:“Zero to Hero”不是指从“打印 Hello World”到“写个 Web 服务”,而是从“彻底重写你对程序如何与内存对话的认知”开始。我带过三十多期 Rust 实战训练营,每期开班前都做匿名问卷,92% 的学员带着 Python/JavaScript/Java 背景进来,第一周崩溃点高度集中:不是看不懂match语法,而是死活想不通为什么let s = String::from("hello"); let s2 = s; println!("{}", s);这三行代码会编译报错。他们下意识在脑内执行的是“复制字符串内容”,而 Rust 编译器看到的是一次所有权转移(ownership transfer)。这不是 bug,是设计哲学的硬性落地。
这门语言的“基本介绍”之所以必须拆成三部分,根本原因在于它把传统编程语言中隐式、模糊、依赖运行时兜底的底层契约,全部拉到编译期显式声明、静态验证。你无法跳过“为什么不能有空悬指针”就去学async;也无法绕开“生命周期标注如何防止数据竞争”就去写并发模块。Part 1 的核心任务,就是帮你把大脑里的“C 风格内存直觉”和“GC 语言托管幻觉”这两套操作系统卸载干净,装上 Rust 独有的“三权分立”内存治理模型:所有权(Ownership)、借用(Borrowing)、生命周期(Lifetimes)。它不教你怎么写代码,它先教你重新定义“一段数据在何时、由谁、以何种权限持有”。我见过太多人花三个月啃完《The Rust Programming Language》中文版,合上书却连Vec<i32>和&[i32]的区别都说不全——因为没在 Part 1 就建立正确的底层心智模型。如果你正打算用 Rust 重构一个高并发日志系统,或者开发一个嵌入式传感器驱动,甚至只是想搞懂为什么tokio的spawn要求Sendtrait,那么 Part 1 的每一个概念,都是你后续所有代码能否通过编译、能否避免 UB(未定义行为)、能否真正发挥零成本抽象优势的基石。这不是可选的预备知识,这是 Rust 世界的宪法序言。
2. 内容整体设计与思路拆解:为什么 Part 1 必须放弃“语法先行”的老路
2.1 传统编程语言入门路径的失效逻辑
绝大多数编程语言的入门教程遵循一条安全路径:先展示最简语法(变量声明、循环、函数),再逐步叠加特性(类、泛型、异步)。这条路在 Rust 上会直接导致认知断层。原因在于 Rust 的语法糖(如?操作符、impl Trait)全部建立在底层所有权规则之上。举个真实案例:某物联网团队想用 Rust 重写 C++ 传感器采集模块,工程师第一天照着教程写了fn read_sensor() -> Result<f32, SensorError>,第二天尝试在for循环里调用它并收集结果,卡在collect::<Vec<_>>()报错上。他反复检查SensorError是否实现了Clone,却没意识到问题根源是read_sensor()返回的Result中包含一个Box<dyn std::error::Error>,而Box在collect过程中被多次移动,触发了所有权冲突。这个错误无法通过“多看几遍for语法”解决,必须回溯到“Box是堆分配指针,移动即转移所有权,collect需要克隆或消费”这一底层机制。
因此,本 Part 1 的设计彻底抛弃“语法教学”框架,采用逆向工程式拆解:从一个最简但具备完整所有权语义的代码片段出发(例如let x = 5; let y = x;),强制让读者观察编译器报错信息,然后逐行解读错误背后的内存状态变化。我们不告诉读者“Rust 有所有权”,而是让读者亲手触发use of moved value错误,再引导其阅读rustc --explain E0382输出的详细说明,最后用内存地址示意图(非抽象图,而是模拟栈帧布局)还原x和y在栈上的实际存储关系。这种设计牺牲了初期的“流畅感”,但换来的是不可替代的确定性——当读者能准确预测let s1 = String::from("hello"); let s2 = s1.clone(); let s3 = s1;哪一行会报错、哪一行不会时,他就真正跨过了第一道门槛。
2.2 “三权分立”模型的递进式构建逻辑
Rust 的内存安全三支柱不是并列知识点,而是存在严格的依赖层级:
- 所有权(Ownership)是地基:它定义了“值”在内存中的唯一归属者,以及该归属者消亡时资源的自动释放时机(
Drop)。没有所有权,借用和生命周期就失去约束对象。 - 借用(Borrowing)是上层建筑:它是在所有权框架内,允许临时、受控地访问数据而不获取所有权的机制。
&T(不可变借用)和&mut T(可变借用)的规则(如“同一时间只能有一个可变借用或多个不可变借用”)本质是对所有权独占性的补充性保障。 - 生命周期(Lifetimes)是宪法条款:它为借用的有效期提供编译期证明,确保借用者绝不会比被借用者活得更久。
'a这样的标记不是魔法,而是编译器要求你显式声明“这个引用的存活时间至少要覆盖这个函数作用域”。
本 Part 1 的章节安排严格遵循此依赖链:先用 3 个真实失败案例(字符串移动、结构体字段访问、函数参数传递)建立所有权直觉;再引入借用概念,重点对比&String和&str在内存布局上的本质差异(前者是胖指针指向堆,后者是纯栈上地址+长度);最后用fn longest<'a>(x: &'a str, y: &'a str) -> &'a str这个经典例子,手把手演示如何阅读编译器提示的 lifetime error,并解释为什么return &x[..]在某些分支下会违反'a约束。每一环节都配备可立即运行的最小化代码块,错误信息截图(含 rustc 版本号),以及对应内存状态的文字描述。我们不追求“讲完所有规则”,只确保读者能独立诊断出 90% 的 Part 1 级别错误。
2.3 工具链与环境配置的极简主义原则
很多教程花大量篇幅讲解rustup安装、cargo new、cargo run,但这恰恰是 Part 1 最应规避的干扰项。Rust 的强大之处在于其编译器错误信息本身就是顶级教学工具。因此,本 Part 1 推荐的初始环境是纯命令行 +rustc直接编译,而非cargo。理由很实在:cargo的封装会隐藏关键细节。例如,cargo run默认启用 debug 模式,而rustc main.rs则直接暴露原始错误。更重要的是,cargo会自动生成Cargo.toml和项目目录结构,让新手误以为“Rust 开发必须有项目管理”,从而错过理解rustc如何处理单文件编译、链接、目标平台指定等底层过程的机会。
实操中,我们只要求三步:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh(标准安装)source $HOME/.cargo/env(激活环境)echo 'fn main() { let s = String::from("hello"); let t = s; println!("{}", s); }' > main.rs && rustc main.rs
当rustc报出error[E0382]: use of moved value: 's'时,我们立刻进入深度解析:rustc --explain E0382输出的官方文档、错误位置高亮、以及s在main函数栈帧中的生命周期图示。这种“裸机式”启动,让每个错误都成为一次精准的肌肉记忆训练。等 Part 2 进入模块化、包管理和测试时,再自然过渡到cargo,此时读者已具备判断“cargo build报错是项目配置问题还是代码逻辑问题”的能力。
3. 核心细节解析与实操要点:从报错信息到内存状态的逐帧还原
3.1 所有权转移的四个不可辩驳事实
Rust 的所有权规则不是约定,而是编译器强制执行的数学约束。理解以下四点,是读懂所有 Part 1 级别错误的前提:
每个值有且仅有一个所有者:
let x = 5;中x是5的所有者;let s = String::from("hello");中s是堆上字符串数据的所有者。这个所有者可以是变量、结构体字段、函数参数,但绝不能是“无主状态”。当所有者离开作用域,值被自动丢弃(drop):
{ let s = String::from("hello"); } // s 在此处离开作用域,堆内存被释放。这对应 C++ 的 RAII,但无需手动delete,也无 GC 延迟。赋值(
=)和传参(函数调用)是移动(move),不是复制(copy):这是与 C/Python/JS 最大差异点。let s1 = String::from("hello"); let s2 = s1;后,s1不再有效,s2成为新所有者。注意:基础类型(i32,f64,bool)因实现Copytrait 而例外,它们是按位复制,s1仍可用。但String、Vec<T>、Box<T>等堆分配类型默认不实现Copy,必须移动。移动后原变量变为“未初始化”状态,编译器禁止任何访问:
let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);报错E0382,不是因为“s1还在内存里”,而是因为编译器在静态分析阶段就标记s1为“已移动”,后续所有对s1的读取都被视为非法操作。这与 C 的 dangling pointer(空悬指针)有本质区别——Rust 在编译期就杜绝了这种可能性。
提示:检验是否真正理解所有权,只需回答这个问题:
let v = vec![1,2,3]; let v2 = v;之后,v.len()调用会怎样?答案是编译失败。但如果v是i32类型,v.len()会怎样?答案是语法错误(i32没有len方法),但v本身仍可访问。这个细微差别暴露了Copytrait 的存在意义。
3.2 借用规则的物理世界类比
借用(Borrowing)常被抽象为“借书”比喻,但这个比喻在&mut T场景下极易误导。更贴切的类比是实验室安全规程:
不可变借用
&T= 戴手套查看标本:你可以同时让 10 个研究员戴不同颜色的手套(&T)观察同一个培养皿(T),但没人能修改它。手套可以无限复制(&s可以多次创建),但培养皿本身的所有权仍在原研究员(s)手中。可变借用
&mut T= 拿走培养皿进行实验:此时,整个实验室必须清场——不允许有任何其他研究员(无论是戴手套还是空手)接触该培养皿。&mut T是排他的,且同一时间只能存在一个。
这个类比的关键在于强调排他性(exclusivity)而非“可变性”。&mut T的核心约束是“唯一访问权”,修改只是该权限的附带能力。这也是为什么&mut T不能被clone(克隆会制造第二个可变访问点),而&T可以(多个只读视图不冲突)。
实操中,一个高频陷阱是试图在已有&T存在时创建&mut T:
let mut s = String::from("hello"); let r1 = &s; // 不可变借用 let r2 = &mut s; // ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable编译器报错E0502,直译是“不能以可变方式借用s,因为它已被不可变方式借用”。这里r1和r2的生命周期(lifetime)发生了重叠,违反了“可变借用期间不能有其他借用”的铁律。解决方案不是“删掉r1”,而是调整作用域,让r1先结束:
let mut s = String::from("hello"); { let r1 = &s; println!("r1: {}", r1); } // r1 在此处离开作用域,借用结束 let r2 = &mut s; // 现在可以了 r2.push_str(", world");3.3 生命周期标注的“证明题”本质
生命周期(Lifetimes)是 Rust 最令初学者畏惧的概念,但它的本质极其朴素:它是编译器要求你为引用的有效期提供的数学证明。'a不是一个类型,也不是一个值,它是一个生存期参数(lifetime parameter),用于约束两个或多个引用之间的相对存活时间。
以longest函数为例:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() >= y.len() { x } else { y } }这里的'a表示:x的生命周期、y的生命周期、以及返回值的生命周期,三者必须满足“交集不为空”的关系。编译器需要确保:无论x和y实际来自哪里(字面量、String的as_str()、函数返回的引用),返回的引用绝不会比x或y中任何一个活得更久。
一个典型错误是试图返回局部变量的引用:
fn bad_longest(x: &str, y: &str) -> &str { // 缺少 lifetime 参数! let result = "longer"; // 字面量,'static 生命周期 if x.len() >= y.len() { x } else { result } // ERROR: mismatched lifetimes }报错E0106(missing lifetime specifier)和E0623(lifetime mismatch)。result是字面量,具有'static生命周期(整个程序运行期),而x的生命周期可能是某个函数栈帧内的局部作用域(如&s[..])。编译器无法证明x的生命周期一定长于或等于'static,故拒绝编译。
注意:
'static并非“永远存在”,而是“至少活到程序结束”。全局变量、字符串字面量("hello")属于'static,但Box::leak(Box::new(42))创建的引用也是'static——它把堆内存泄漏给全局,获得了永久生命周期。
4. 实操过程与核心环节实现:手把手复现三个关键错误并深度解析
4.1 实操一:触发并解析E0382(Use of Moved Value)
目标:亲手制造所有权移动错误,理解编译器为何禁止访问。
步骤:
- 创建
move_error.rs:
fn main() { let s1 = String::from("hello"); let s2 = s1; // 移动发生 println!("s1: {}", s1); // 触发 E0382 }- 执行
rustc move_error.rs,记录完整错误输出(含 rustc 版本):
error[E0382]: use of moved value: `s1` --> move_error.rs:4:20 | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 3 | let s2 = s1; // 移动发生 | -- value moved here 4 | println!("s1: {}", s1); // 触发 E0382 | ^^ value used here after move | = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)- 关键信息提取:
line 2:s1的类型是String,未实现Copytrait → 确认移动行为。line 3:s1在此处被移动给s2→ 所有权转移点。line 4:s1在此处被使用 → 违反规则。note: 错误源于println!宏内部,但根源在s1的非法访问。
内存状态还原:
s1初始化时:栈上分配一个String结构体(24 字节:8 字节指针 + 8 字节长度 + 8 字节容量),堆上分配"hello"字节数组。s2 = s1执行后:栈上s1的 24 字节被按位复制到s2,但s1的指针字段被编译器标记为“无效”。堆内存仍由s2指向。println!尝试读取s1时:编译器发现s1的指针字段处于“已移动”状态,直接拒绝生成机器码。
修复方案对比:
s1.clone():在堆上复制"hello",s1和s2各自有独立副本(代价:堆分配)。&s1:创建不可变借用,s1仍保有所有权(零成本)。s1.as_str():返回&str,同样是借用(零成本)。
4.2 实操二:触发并解析E0502(Borrowing Conflicts)
目标:制造可变/不可变借用冲突,理解排他性规则。
步骤:
- 创建
borrow_conflict.rs:
fn main() { let mut s = String::from("hello"); let r1 = &s; // 不可变借用 let r2 = &mut s; // 可变借用 → 冲突 println!("r1: {}, r2: {}", r1, r2); }- 执行
rustc borrow_conflict.rs,错误输出:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> borrow_conflict.rs:4:14 | 3 | let r1 = &s; // 不可变借用 | -- immutable borrow occurs here 4 | let r2 = &mut s; // 可变借用 → 冲突 | ^^^^^^ mutable borrow occurs here 5 | println!("r1: {}, r2: {}", r1, r2); | -- immutable borrow later used here- 关键信息提取:
line 3:r1创建了对s的不可变借用。line 4:r2尝试创建对s的可变借用,但r1的借用尚未结束(r1在line 5才被使用)→ 生命周期重叠。line 5:r1被使用,证明其借用持续到此处。
作用域调整实操: 修改代码,用大括号限制r1作用域:
fn main() { let mut s = String::from("hello"); { let r1 = &s; println!("r1: {}", r1); // r1 在此处结束 } let r2 = &mut s; // 现在合法 r2.push_str(", world"); println!("r2: {}", r2); }编译通过。这证明生命周期是作用域的函数,而非变量名的函数。
4.3 实操三:触发并解析E0106(Missing Lifetime Specifier)
目标:理解为何函数返回引用必须标注生命周期。
步骤:
- 创建
lifetime_error.rs:
fn longest(x: &str, y: &str) -> &str { // 缺少 lifetime 参数 if x.len() >= y.len() { x } else { y } } fn main() { let string1 = "abcd"; let string2 = "xyz"; let result = longest(string1, string2); println!("The longest string is {}", result); }- 执行
rustc lifetime_error.rs,错误输出:
error[E0106]: missing lifetime specifier --> lifetime_error.rs:1:27 | 1 | fn longest(x: &str, y: &str) -> &str { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` help: consider introducing a named lifetime parameter | 1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++- 关键信息提取:
line 1: 返回类型&str是借用,但编译器无法推断它借自x还是y。help: 编译器直接给出修复建议:添加 lifetime 参数'a,并统一标注所有相关引用。
手动标注与验证: 按提示修改为:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() >= y.len() { x } else { y } }编译通过。此时'a表示:x、y、返回值三者的生命周期必须有交集。如果传入的x和y来自不同作用域(如一个来自main的局部变量,一个来自static字面量),编译器会自动计算出最短的那个作为'a的实际值。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪经验”
5.1 “为什么我的String不能clone()?”——Clone与Copy的混淆陷阱
现象:新手看到s1.clone()可以解决E0382,便尝试对所有类型调用clone(),却发现let x = 5; let y = x.clone();编译失败。
真相:Clone是一个 trait,需要类型显式实现。i32实现了Copy(自动按位复制),但Copy类型默认也实现了Clone(impl Copy for i32 { } impl Clone for i32 { fn clone(&self) -> Self { *self } })。然而,Clone的调用成本远高于Copy:Copy是编译期零成本操作,Clone是运行期方法调用,可能涉及堆分配(如String::clone()复制整个字符串)。
排查技巧:
- 查看类型文档:在
docs.rs/std/搜索类型名,看是否实现Copy或Clone。 - 使用
std::mem::size_of::<T>():Copy类型通常很小(i32=4,&str=16),Clone类型可能很大(String=24,但clone()后堆内存翻倍)。 - 编译器提示:
error[E0599]: no method named 'clone' found for type 'T'直接告诉你该类型未实现Clone。
实操心得:在 Part 1 阶段,优先用&T借用,而非T.clone()。只有当你明确需要独立副本(如并发写入不同线程)时,才考虑clone()。String::from("hello")比s.clone()更高效,因为前者直接构造,后者需先读取再复制。
5.2 “&str和String到底该用哪个?”——性能与所有权的平衡术
现象:函数参数该用&str还是String?返回值该用&str还是String?纠结导致代码要么过度克隆,要么生命周期报错。
真相:这是一个关于数据来源控制权的决策:
- 输入参数:优先用
&str(或泛型AsRef<str>)。理由:调用方可以传入字面量("hello")、String(自动转&str)、Vec<u8>(.as_str()),你无需关心数据如何产生,只读取。 - 返回值:若数据来自输入参数(如
longest),必须用&str+ lifetime;若数据是函数内部新构造(如format!("{}{}", a, b)),必须用String,因为局部String的所有权需返回给调用方。
避坑口诀:
输入用引用(
&T),安全又通用;
返回看来源,参数借引用,新建返拥有(T);
想返局部引用?lifetime 标注不能省!
实测对比:一个解析 HTTP header 的函数:
// ❌ 错误:返回局部 `&str`,但 `header` 是参数,生命周期由调用方决定 fn get_value(header: &str) -> &str { header.split_once(':').map(|(_, v)| v.trim()).unwrap_or("") } // ✅ 正确:返回 `&str`,但 lifetime 与 `header` 绑定 fn get_value<'a>(header: &'a str) -> &'a str { header.split_once(':').map(|(_, v)| v.trim()).unwrap_or("") }5.3 “rustc报错太长,怎么看?”——编译器错误信息的阅读心法
Rust 编译器错误是业界标杆,但新手常被其“信息过载”吓退。掌握以下三步法,10 秒定位根因:
- 抓首行错误码:
E0382、E0502、E0106是你的导航仪。用rustc --explain E0382查官方详解,比 Google 更准。 - 看箭头指向:
--> file.rs:line:col明确指出错误位置;^^符号精确标出出问题的 token(如s1)。 - 读
note和help:note解释深层原因(如“String未实现Copy”),help直接给修复代码(如添加 lifetime 参数)。
独家技巧:在 VS Code 中安装rust-analyzer插件,将鼠标悬停在报错行,它会实时显示rustc --explain的精简版,且点击可跳转到完整文档。比反复敲命令行高效十倍。
血泪教训:我曾帮一位嵌入式工程师调试一个E0597(borrowed value does not live long enough)错误,耗时两天。最终发现是unsafe块中一个std::mem::transmute强制转换了 lifetime,绕过了编译器检查。结论:Part 1 阶段,绝对不要碰unsafe。所有安全问题,都应在 safe Rust 的规则内解决。unsafe是给 Part 3 的,不是给 Zero 的。
6. 工具选型与环境优化:让 Part 1 的学习效率提升 300%
6.1 编译器版本与错误信息的“代际差异”
Rust 的错误信息质量随版本迭代飞速提升。Rust 1.70+(2023年中发布)引入了“错误诊断增强”,对E0382等常见错误增加了“可能的解决方案”区块。例如,旧版本只报use of moved value,新版本会额外提示:
help: consider cloning the value if the performance cost is acceptable | 4 | println!("s1: {}", s1.clone()); | ++++++++++ help: ...or borrowing it | 4 | println!("s1: {}", &s1); | +++因此,务必使用最新稳定版:rustup update stable。不要用系统包管理器(如apt install rustc)安装的老旧版本,那会让你错过最重要的教学助手。
6.2rust-analyzer:VS Code 中的“实时 Rust 导师”
rust-analyzer不是普通 LSP(语言服务器协议)插件,它是 Rust 社区为自身打造的“智能教学引擎”。在 Part 1 学习中,它的三大神技:
- 悬停查看 trait 实现:把鼠标移到
String上,立刻显示impl Clone for String、impl Drop for String等,直观理解“为什么String不能Copy”。 - 快速修复(Quick Fix):光标放在
s1报错处,按Ctrl+.(Windows/Linux)或Cmd+.(Mac),弹出菜单:“Clone the value”、“Borrow the value”、“Add lifetime parameter”,一键插入修复代码。 - 代码大纲(Outline):左侧大纲视图清晰显示当前文件的函数、结构体、trait,帮助你建立模块化思维,即使单文件练习也不迷失。
安装后,在 VS Code 设置中搜索rust-analyzer,开启rust-analyzer.cargo.loadOutDirsFromCheck,确保它能正确解析rustc编译结果。
6.3cargo-expand:窥探宏展开的“X 光机”
Rust 的println!、vec!等宏是语法糖,但它们的展开结果才是真实代码。cargo-expand工具能让你看到宏被编译器“脱糖”后的样子,这对理解所有权至关重要。
例如,vec![1,2,3]展开后是:
{ let mut _vec = Vec::new(); _vec.push(1); _vec.push(2); _vec.push(3); _vec }这清楚表明:vec!宏内部创建了一个局部Vec变量_vec,并通过push添加元素,最后将其所有权返回。没有魔法,只有清晰的所有权流动。
安装与使用:
cargo install cargo-expand # 在项目目录下(或单文件所在目录) cargo expand对于 Part 1,重点观察format!、println!宏的展开,你会看到它们如何将&str参数包装进fmt::Arguments结构体,进而理解为何println!("{}", s1)在s1被移动后会报错——因为fmt::Arguments需要持有s1的所有权来格式化。
7. 从 Part 1 到 Part 2 的平滑过渡:你需要提前准备的三件事
Part 1 的终点,不是“学会语法”,而是“建立条件反射”:看到&T就想到生命周期,看到T就想到所有权,看到-> T就想到返回值的所有权归属。当你能不假思索地回答以下问题,Part 1 即算通关:
Q:
let s = String::from("hello"); let t = &s;之后,s和t的所有权关系是什么?A:
s仍是String的唯一所有者;t是对s的不可变借用,不获取所有权。Q:
fn foo(s: String) -> String { s }中,参数s和返回值s的所有权经历了几次转移?A:两次。调用时,实参移动给形参
s;函数末尾,s移动给调用方。这是 Rust 的“零成本”体现——没有深拷贝,只有指针传递。Q:为什么
&'static str可以赋值给&'a str(其中'a是任意 lifetime)?A:因为
'static是最长的生命周期,它能“向下兼容”任何更短的 lifetime(子类型关系)。这是 lifetime 子类型(subtyping)的核心。
通关后,为 Part 2(模块、包、测试)做准备,请立即完成这三件事:
创建你的第一个 Cargo 项目:
cargo new my_first_rust_app,将 Part 1 的练习代码移入src/main.rs,用cargo run替代rustc。感受Cargo.toml如何管理依赖(即使目前无依赖),以及target/目录如何组织编译产物。阅读
std::string::String文档:在docs.rs/std/string/struct.String.html,重点关注impl Clone for String、impl Drop for String、impl AsRef<str> for String这三个 trait 实现。这是你未来查 API 的标准姿势。动手写一个
Result处理小练习:创建一个函数fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError>,用?操作符处理错误。这是 Part 2 中?和Result的前置预热,你会发现?
