这期我们来唠唠 Rust 的"核心五大护法",这几个概念是 Rust 世界的地基,理解它们之后,你再看 Rust 就像"武林秘籍看目录",豁然开朗。我会从"前端视角"来比喻,结合 JS/TS 帮你建立联系。
🥇 所有权(Ownership)------Rust 的"内存保安队长"
在 JS 里,你从来没管过内存,new 也好,数组也好,变量用完就等 GC 回收。但 Rust 没 GC,它靠所有权系统来"自动管理内存"。
通俗讲:
在 Rust 里,每块内存只会有一个"所有者变量"。当所有者变量离开作用域,这块内存就自动释放。
举个例子:
rust
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 把所有权给了 s2,自己就不能用了
println!("{}", s1); // ❌ 编译报错:s1 已被移动
}
就像 JS 里的变量 s1 = 'hello'
;你把它"转手给了 s2",但 Rust 会说:"那 s1 就不能用了哈,我要保证你不会用到无效数据"。
编译期强制你安全编程,这就是 Rust 的牛逼之处。
🥈 借用(Borrowing)------变量"出借",但不能"转让"
那你可能说:"我不想把变量完全交出去,只是借它用一下。"这时候就轮到 Rust 的"借用"机制了。
类比 JS:
js
function double(arr) {
return arr.map(x => x * 2);
}
你传的是个引用,对吧?Rust 里也能借用变量,不转移所有权:
rust
fn double(arr: &Vec<i32>) {
for x in arr {
println!("{}", x * 2);
}
}
fn main() {
let v = vec![1, 2, 3];
double(&v); // 借用 v,不移动所有权
println!("{:?}", v); // ✅ OK,v 还能用
}
Rust 区分:
&T
:不可变借用,多个可以共存&mut T
:可变借用,只能有一个
核心思想:你要么多个读者,要么一个作者,不能又读又写,这样就不会数据冲突。
🥉 生命周期(Lifetimes)------借用的"生死时限"
这听起来吓人,其实是为了解决一个问题:你借的东西,不能比原来活得久。
想象下这个 JS 场景:
js
let result;
{
const temp = "hello";
result = temp; // temp 出作用域就挂了,但你还在用 result
}
console.log(result); // ❌ 潜在问题
Rust 编译器会捕捉这种情况,它要你声明:这个借用能活多久。
rust
fn get_first<'a>(s: &'a str) -> &'a str {
&s[0..1]
}
'a
就是生命周期标注,它告诉编译器:传进来的引用,返回值的生命周期不能超过它。
你平时用编译器推断就够了,出问题再显式标注。
🟦 匹配模式(match)------比 switch-case 高级一百倍
JS 有 switch-case
,Rust 有 match
,但它不仅能匹配值,还能解构、匹配类型、绑定变量,非常强。
rust
fn main() {
let x = Some(5);
match x {
Some(1) => println!("one"),
Some(n) => println!("value is {}", n),
None => println!("none"),
}
}
像是类型 + 模式匹配的结合体,有点像 JS 里的:
js
const x = { type: 'Some', value: 5 };
switch (x.type) {
case 'Some': console.log(x.value); break;
case 'None': console.log('none'); break;
}
但 Rust 写法更简洁,特别适合和枚举/错误处理配合使用。
🟥 枚举 + 错误处理(Option / Result)------告别 null、try-catch 的新姿势
Rust 没有 null
、undefined
这些让人崩溃的玩意儿。取而代之的是:
1. Option<T>
表示可能有值,也可能没值:
rust
let name: Option<String> = Some("Tom".to_string());
match name {
Some(n) => println!("Hello, {}", n),
None => println!("No name"),
}
2. Result<T, E>
表示可能成功,也可能失败(比如文件读取、网络请求):
rust
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
match divide(10, 2) {
Ok(val) => println!("Result: {}", val),
Err(e) => println!("Error: {}", e),
}
}
这种做法好在哪?
- 编译器强制你"必须处理错误"
- 再也不会有"调用了 null 的 toString 报错"这种事
是不是很像 TS 的
Either
、Option
泛型类型,但更原生、更好用?
总结:核心概念速查表
概念 | 通俗解释 | JS 类比 |
---|---|---|
所有权 | 谁负责释放内存 | 没有 GC,要自己守规则 |
借用 | 引用变量但不转让 | 像引用参数,JS 默认传引用 |
生命周期 | 借用变量活多久 | 闭包/作用域变量别用晚了 |
匹配模式 | 花式 switch-case | 解构 + 类型判断的组合拳 |
Option / Result | 安全处理空值和错误 | 告别 null、try-catch 的未来式 |