别把 Rust 写成 TypeScript:从"万物皆对象"到"数据即契约"的思维重构
前言:当 TypeScript 开发者第一次遇见 Rust
想象一下这个场景:你是一个写了五年 TypeScript 的前端工程师,自认为对类型系统已经了如指掌。interface、generic、conditional type——这些玩意儿你都能玩出花来。然后某天,你心血来潮,决定学一学 Rust,毕竟这门语言在前端圈子里越来越火,Webpack 的继任者 Rspack 用它,SWC 编译器用它,甚至 Node.js 的一些底层模块也开始用 Rust 重写了。
你翻开《The Rust Programming Language》,看到第一章就懵了:所有权(Ownership)?借用(Borrowing)?生命周期(Lifetime)?这些概念在你的 TypeScript 世界里压根不存在。你写的第一行 Rust 代码充满了 &、mut、 lifetimes 'a,编译器报错的信息比你写的代码还长。
别担心,你不是一个人。Rust 和 TypeScript 虽然都打着"类型安全"的旗号,但它们看待世界的角度完全不同。TypeScript 是在 JavaScript 的基础上打补丁,而 Rust 则是一套全新的编程哲学。本文的任务,就是帮你完成这场思维重构——从"万物皆对象"到"数据即契约"。
为什么 Rust 需要你换一套脑子?
在 TypeScript/JavaScript 的世界里,你写代码的方式大概是这样的:
// TypeScript:我想怎么传就怎么传,想怎么改就怎么改
function processUser(user: User) {
const clone = { ...user }; // 等等,这个clone是深拷贝还是浅拷贝?
clone.name = "Alice"; // 改吧,反正原对象又不会被影响...大概
return clone;
}
// 等等,这个函数会修改原对象吗?
// 这个返回值会被谁持有?
// 我要不要担心内存泄漏?
在 TypeScript 里,这些问题要么不存在(JavaScript 帮你做了垃圾回收),要么被运行时错误暴露出来(undefined is not a function)。你习惯了这种"先跑再说"的开发节奏,类型检查只是辅助工具,真正的约束来自于测试和线上事故。
但 Rust 选择了一条完全不同的路。Rust 工程师相信:编译时能解决的问题,就不要留给运行时。 如果你能在编译期确保"这个数据不会被并发修改"、"那块内存一定被正确释放"、"这个空指针根本不存在",那为什么要等到程序跑起来再发现问题?
这就是 Rust 的核心哲学:数据即契约。当你把一个值传给函数,当你在线程间共享数据,当你从 Option 里取值——每一步都有明确的规则,编译器会逐条检查。如果你违反了契约,代码就编译不过,就这么简单。
1.2 效率与安全的双重追求
Rust 另一个让 TypeScript 开发者困惑的设计目标是:零成本抽象(Zero-Cost Abstractions)。
在 TypeScript 里,你用 class 封装一些方法,运行时会有额外的开销。你用 Proxy 做响应式,性能会打折扣。你用 Reflect.metadata 做装饰器,这些元数据会占用内存。抽象往往意味着取舍,你用起来方便,但性能会付出代价。
Rust 拒绝这种取舍。它的设计目标是:如果你不需要额外的运行时特性,就不应该为此付出任何性能代价。一个 trait 对象可能听起来很"高级",但它编译后的机器码可能和一个手写的 switch 语句完全一样快。这就是"零成本"的意思——你得到的抽象是免费的午餐。
1.3 借用检查器:那个强迫症晚期的代码审查员
终于要介绍本文的灵魂人物了:Borrow Checker(借用检查器)。
如果说 Rust 编译器是一个代码审查员,那 Borrow Checker 就是其中最严格、最执拗的那个。它每天的工作就是检查你的代码有没有违反"所有权规则"。它不管你的业务逻辑有多巧妙,不管你的算法有多高效,它只关心一件事:你有没有正确地管理内存?
它的审查标准大概是这个样子:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 等等,s1 被"移动"到 s2 了
println!("{}", s1); // 编译错误!s1 已经无效了
}
在 TypeScript 开发者眼里,这简直匪夷所思:s1 明明还是"hello"啊,为什么不能打印?但 Rust 的规则是:当你把 s1 赋值给 s2,s1 就"搬家"了,它不再拥有那个字符串的所有权。就像你把房子的钥匙给了别人,原来的主人就不能再进屋了一样。
Borrow Checker 就是这么轴。它不会变通,不会"大概看看",不会"这次就算了"。要么你遵守规则,要么你不通过编译。刚开始你可能会觉得它是个疯子,但等你真正理解它的用意,就会发现:它是你的朋友,只是表达方式比较激烈。
核心模式对比——这些地方,Rust 和 TypeScript 长得像,但骨子里完全不一样
2.1 对象 vs 结构体:继承的诱惑与组合的艺术
TypeScript 的 Class:熟悉的老朋友
在 TypeScript 里,你大概这样写代码:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
speak(): void {
console.log(`${this.name} barks`);
}
fetch(): void {
console.log(`${this.name} fetches the ball`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Buddy barks
dog.fetch(); // Buddy fetches the ball
这段代码对 TypeScript 开发者来说再自然不过了。Dog extends Animal,子类继承父类的一切,然后添加自己的特性。多态、封装、继承——面向对象的三大支柱,你从 Java/C# 转过来的,对这套东西门儿清。
Rust 的 Struct + Trait:另一种解法
然后你看到 Rust 的写法,整个人都不好了:
// 定义一个结构体——但这只是数据容器,没有行为
struct Animal {
name: String,
}
// 定义一个 Trait——这才是"行为"的抽象
trait Speak {
fn speak(&self);
}
// 为 Animal 实现 Speak trait
impl Speak for Animal {
fn speak(&self) {
println!("{} makes a sound", self.name);
}
}
// 定义 Dog 结构体
struct Dog {
name: String,
breed: String,
}
// 为 Dog 也实现 Speak
impl Speak for Dog {
fn speak(&self) {
println!("{} barks", self.name);
}
}
// Dog 还有自己的特殊方法
impl Dog {
fn fetch(&self) {
println!("{} fetches the ball", self.name);
}
}
fn main() {
let dog = Dog {
name: String::from("Buddy"),
breed: String::from("Golden Retriever"),
};
dog.speak();
dog.fetch();
}
What? 结构体是结构体,行为是行为,它们是分开的?这不是"封装"被破坏了吗?
但等你真正用起来,就会发现这套设计的好处:
第一,组合比继承更灵活。 在 TypeScript 里,一个类只能有一个父类。你想同时"是动物"又是"可序列化的"又是"可比较的"?抱歉,TypeScript 的类不支持多继承。但在 Rust 里,一个结构体可以实现多个 trait:
use std::fmt::{Debug, Display};
struct MyData {
value: i32,
}
// 同一个结构体可以实现多个 trait
implDebugfor MyData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MyData {{ value: {} }}", self.value)
}
}
impl Display for MyData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
implPartialEqfor MyData {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
第二,trait 可以给第三方类型扩展行为。 想象一下你想给 Vec
trait Averageable {
fn average(&self) -> f64;
}
impl<T> Averageable forVec<T>
where
T: std::ops::Add<Output = T> + Div<usize, Output = f64> + Copy + Default,
{
fn average(&self) -> f64 {
ifself.is_empty() {
return0.0;
}
let sum: T = self.iter().fold(T::default(), |acc, &x| acc + x);
// 这里简化处理,实际需要类型转换
self.len() asf64
}
}
这种"为已有类型添加方法"的能力,在 TypeScript 里只有内置的 prototype 扩展才能做到,但 Rust 用 trait 实现得更干净、更类型安全。
第三,运行时多态的代价更清晰。 在 TypeScript 里,父类引用指向子类对象是零成本的(JavaScript 的鸭式类型)。但在 Rust 里,如果你需要真正的运行时多态,需要显式使用 Box
// 静态分发——编译时就确定调用哪个实现
fn static_dispatch(dog: Dog) {
dog.speak();
}
// 动态分发——运行时通过 vtable 确定调用哪个实现
fn dynamic_dispatch(animal: Box<dyn Speak>) {
animal.speak();
}
静态分发性能更好(内联优化),动态分发更灵活(可以装不同类型)。你选择用哪种,代码里一眼就能看出来。
2.2 垃圾回收 vs 所有权:为什么你不能像 JS/Python 那样随心所欲
动态语言的"幸福生活"
在 JavaScript 或 Python 里,你大概这样写:
# Python:随便传,反正有 GC 帮我收拾
def process_list(data):
# data 是原始列表的引用
# 我可以修改它
data.append(100)
# 也可以创建新的
new_data = data + [200]
return new_data
# 调用
original = [1, 2, 3]
result = process_list(original)
# original 现在是 [1, 2, 3, 100] 吗?
# result 是 [1, 2, 3, 100, 200] 吗?
# 鬼知道,除非我跑一遍代码
GC(Garbage Collection)帮你管理内存,但它的代价是运行时开销和不确定性。你不知道 data.append() 会不会触发一次 major GC,你不知道这个函数返回后 original 还能不能用(取决于函数内部有没有修改)。这些问题都要靠文档、靠约定、靠测试来避免。
Rust 的"较真":所有权、借用与生命周期
Rust 选择了另一条路:在编译期就确定谁拥有这块内存,谁可以借用,借多久。
规则一:每个值有一个所有者(Owner)。
let s = String::from("hello"); // s 是所有者
规则二:值只能有一个所有者。当所有者离开作用域,值就被drop了。
{
let s = String::from("hello"); // s 拥有这个字符串
// s 在这里有效
} // s 离开作用域,字符串被释放
规则三:赋值/传参可以转移所有权(move),也可以借用(borrow)。
// Move:所有权转移
let s1 = String::from("hello");
let s2 = s1; // s1 "移动"到 s2,s1 不再有效
// println!("{}", s1); // 编译错误!
// Borrow:借用,不转移所有权
let s1 = String::from("hello");
let s2 = &s1; // s2 借用 s1,不拥有
println!("{} {}", s1, s2); // s1 仍然有效
规则四:借用有规则——可变借用和不可变借用不能同时存在。
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不可变借用
v.push(4); // 可变借用——编译错误!
// 因为 push 可能导致 vector 重新分配内存,first 就变成悬垂指针了
println!("{}", first);
这些规则看起来很繁琐,但它们解决的是真问题:数据竞争(Data Race)和悬垂指针(Dangling Pointer)。这两个 bug 是 C/C++ 程序员的噩梦,Rust 在编译期就帮你消灭了。
Clone:遇到困难就打退堂鼓
既然所有权这么麻烦,有没有简单办法?有,Clone。
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,s1 和 s2 各有一份数据
println!("{} {}", s1, s2); // 都有效
clone() 就是那个"遇到困难就打退堂鼓"的家伙。它不做复杂的借用分析,不考虑什么"也许可以共享访问",它只会:拷贝!拷贝!拷贝! 虽然这会让你多花一些内存和 CPU,但至少代码能跑起来。
新手 Rust 程序员往往会在遇到借用错误时下意识地用 clone() 来"解决"问题。这招确实管用——代码编译通过了。但如果你满屏都是.clone(),那你可能没有真正理解所有权。clone() 应该是深思熟虑后的选择,而不是遇到编译错误时的第一反应。
什么时候该 move,什么时候该 borrow?
作为 TypeScript 开发者的你,可能还是会困惑:我到底什么时候该传值(move),什么时候该传引用(borrow)?
经验法则:
小数据默认 Copy,大数据默认 Borrow。 对于
i32、bool这种实现了Copytrait 的类型,直接传值就行,编译器会帮你复制。对于String、Vec、Box<T>这种 heap 数据,默认 borrow。函数内部需要修改数据,用
&mut:
fn sort_vec(v: &mut Vec<i32>) {
v.sort();
}
let mut data = vec![3, 1, 2];
sort_vec(&mut data);
- 函数只是读取数据,用
&:
fn find_max(v: &[i32]) -> Option<i32> {
v.iter().max().copied()
}
let data = vec![3, 1, 2];
if let Some(max) = find_max(&data) {
println!("Max: {}", max);
}
- 需要返回新数据,但不想转移所有权,用返回值:
fn transform(s: &str) -> String {
format!("transformed: {}", s)
}
let original = String::from("hello");
let result = transform(&original);
// original 仍然是有效的!
println!("{}", original);
错误处理:Promise 的亲戚,但更强大
TypeScript 的 try-catch:事后补救
在 TypeScript 里,错误处理大概是这样:
async function fetchUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
thrownewError(`HTTP ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// 只能笼统地 catch,所有错误都混在一起
console.error("Failed to fetch user:", error);
throw error; // 重新抛出,或者吞掉
}
}
这种错误处理的问题在于:类型系统不知道这个函数可能失败。你在调用 fetchUser() 的时候,IDE 不会提醒你处理错误。你可以选择不 catch,然后祈祷线上不要出问题。
TypeScript 后来引入了 unknown 类型,让 catch 到的 error 至少类型安全一些,但核心问题没解决:函数签名没有表达"可能失败"这个事实。
Rust 的 Result/Option:把失败当成正常流程
Rust 的错误处理则完全不同。它把"可能失败"编码进了类型系统:
use std::num::ParseIntError;
// Option<T>:值可能不存在
fn find_user(id: &str) -> Option<i32> {
// ... 找不到就返回 None
Some(42)
}
// Result<T, E>:操作可能失败
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.parse::<i32>()
}
fn main() {
// 使用 find_user
match find_user("123") {
Some(id) => println!("User ID: {}", id),
None => println!("User not found"),
}
// 使用 parse_number
match parse_number("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Parse error: {}", e),
}
// 链式调用——这是 Rust 错误处理的精髓
let result = find_user("123")
.and_then(|id| Some(id * 2))
.filter(|&id| id > 50)
.map(|id| format!("User #{}", id));
println!("{:?}", result); // Some("User #84")
}
Result
use std::fs::File;
use std::io::{self, Read};
// 链式调用的威力
fn read_username() -> Result<String, io::Error> {
letmut file = File::open("username.txt")?; // ? 操作符:失败就返回
letmut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// 等价的 try-catch 版本(虽然 Rust 不这么写)
fn read_username_verbose() -> Result<String, io::Error> {
let file = match File::open("username.txt") {
Ok(f) => f,
Err(e) => returnErr(e),
};
letmut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
? 操作符是 Rust 错误处理的灵魂。它大概是这样工作的:
如果是 Ok(v),解开并返回 v
如果是 Err(e),立即从当前函数返回 Err(e)
这比 TypeScript 的 try-catch 更清晰、更类型安全。调用者看一眼函数签名,就知道这个函数可能失败、可能返回什么错误。
错误类型的组合
Rust 还支持自定义错误类型,以及用 thiserror 或 anyhow 等 crate 来简化错误处理:
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("User not found: {0}")]
UserNotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(#[from] std::num::ParseIntError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
fn get_user_age(id: &str) -> Result<i32, AppError> {
let id = id.parse::<i32>().map_err(|_| AppError::UserNotFound(id.to_string()))?;
// ... 业务逻辑
Ok(25)
}
这样,错误类型形成了一个树形结构,每种错误都有清晰的语义和上下文。
最佳实践方案——实战中的思维转变
前端场景:Wasm、SWC、Rspack 中的数据处理
作为前端开发者,你学习 Rust 很可能是为了参与这些项目:Webpack 的 Rust 替代品 Rspack、TypeScript 编译器 SWC、Yew 和 Leptos 等 Web 前端框架。它们都用 Rust 实现,以获得比 JavaScript/TypeScript 更高的性能。
但这里有一个陷阱:前端开发者习惯性地会过度包装数据。
避坑一:不要用 Class 包装一切
在 TypeScript 里,你可能这样写:
class User {
constructor(
public readonly id: number,
public name: string,
private email: string
) {}
greet(): string {
return `Hello, ${this.name}!`;
}
}
Rust 风格的等价代码大概是:
// 只用结构体存储数据
struct User {
id: u64,
name: String,
email: String,
}
// 用 impl 块定义方法
impl User {
fn new(id: u64, name: &str, email: &str) -> Self {
User {
id,
name: name.to_string(),
email: email.to_string(),
}
}
fn greet(&self) -> String {
format!("Hello, {}!", self.name)
}
}
或者,如果你只需要临时的"只读视图",用元组结构体更轻量:
struct UserId(u64);
struct UserName(String);
fn process_user(id: UserId, name: UserName) {
// 编译期保证 id 和 name 不会被混淆
}
避坑二:zero-copy 传输数据
在 Wasm 场景里,数据在 JavaScript 和 Rust 之间传输是有成本的。如果你在两边都复制一份数据,性能损耗会很可观。
Rust 的 zerocopy crate 和字节级别操作让你可以直接解析/序列化二进制数据,不需要中间对象:
use zerocopy::{FromBytes, FromZeroes, Unaligned};
#[repr(C)]
#[derive(FromBytes, FromZeroes, Unaligned)]
struct Pixel {
r: u8,
g: u8,
b: u8,
a: u8,
}
// 直接从字节数组解析,不需要任何中间分配
fn decode_pixels(bytes: &[u8]) -> &[Pixel] {
// 字节数组的前 N 个字节就是 Pixel 数组
let (pixels, _) = bytes.split_at(bytes.len() / std::mem::size_of::<Pixel>());
// 这是不安全的,但有zerocopy的保证
// 实际使用中建议用 ref_cast 或其他安全 API
unsafe {
std::slice::from_raw_parts(
pixels.as_ptr() as *const Pixel,
pixels.len(),
)
}
}
AI 场景:Zero-copy 模型预处理
AI 工程师写的 Rust 代码往往是数据处理管道:从磁盘读取模型权重、做 inference 前后的数据转换、批量处理输入数据。
这里的性能瓶颈往往是内存分配和拷贝。
问题:过度包装的张量
Python 风格的张量处理:
// 糟糕的做法:每个操作都创建新的 Vec
fn preprocess_bad(data: &[f32]) -> Vec<f32> {
let normalized: Vec<f32> = data.iter().map(|x| x / 255.0).collect();
let shifted: Vec<f32> = normalized.iter().map(|x| x - 0.5).collect();
let scaled: Vec<f32> = shifted.iter().map(|x| x * 2.0).collect();
scaled
}
这段代码创建了三个中间 Vec,造成了三次内存分配和拷贝。对于 GB 级别的模型数据,这简直是灾难。
更好的做法:用 Iterator 链和 slice 操作
// 好一点的做法:用 iterator 链,避免中间 allocation
fn preprocess_better(data: &[f32]) -> impl Iterator<Item = f32> + '_ {
data.iter()
.map(|x| x / 255.0)
.map(|x| x - 0.5)
.map(|x| x * 2.0)
}
但 iterator 只能顺序处理。如果要做批量 SIMD 操作或者需要原地修改:
最佳实践:原地操作 + 零拷贝视图
// 最佳做法:在原地修改,不需要额外内存
fn preprocess_inplace(data: &mut [f32]) {
for pixel in data.iter_mut() {
*pixel = (*pixel / 255.0 - 0.5) * 2.0;
}
}
// 如果需要"只读视图"和"可变视图"共存,用不同的 slice
fn process_batch(data: &[f32], output: &mut [f32]) {
// data 和 output 是完全独立的 buffer
// 编译期保证你不会搞混
for (i, &val) in data.iter().enumerate() {
output[i] = (val / 255.0 - 0.5) * 2.0;
}
}
3.3 思维框架总结
| 维度 | TypeScript 思维 | Rust 思维 |
|---|---|---|
| 数据封装 | 类包含数据和行为 | 结构体存数据,trait 定义行为 |
| 继承 vs 组合 | extends 单继承 | 实现多个 trait |
| 内存管理 | GC + 运行时检查 | 所有权 + 编译期检查 |
| 错误处理 | try-catch(运行时) | Result/Option(编译期) |
| 函数签名 | 不表达"可能失败" | 类型签名明确表达所有可能性 |
| 性能优化 | profiler 找热点 | 编译期保证 + zero-cost abstraction |
避坑指南:初学者最容易犯的 3 个"把 Rust 写成动态语言"的典型错误
错误一:滥用 .clone()
症状:代码能编译,但满屏都是 .clone(),内存占用高得离谱。
错误示例:
fn bad_example(items: Vec<String>) -> Vec<String> {
items.clone().into_iter().filter(|s| s.len() > 3).collect()
}
fn another_bad(names: &Vec<String>) -> Vec<String> {
names.clone().iter().map(|s| format!("Hello, {}", s)).collect()
}
正确做法:理解所有权,按需借用。
// 不需要 clone!直接消费所有权
fn good_example(items: Vec<String>) -> Vec<String> {
items.into_iter().filter(|s| s.len() > 3).collect()
}
// 用 &str 而不是 &Vec<String>,更通用
fn better_signature(names: &[String]) -> Vec<String> {
names.iter().map(|s| format!("Hello, {}", s)).collect()
}
错误二:用 &mut 全局状态代替模块化
症状:为了避免借用冲突,把所有数据塞进 Mutex 或 RefCell,代码充满 .lock().unwrap()。
错误示例:
use std::cell::RefCell;
struct AppState {
users: RefCell<Vec<String>>,
config: RefCell<Config>,
}
impl AppState {
fn add_user(&self, user: String) {
self.users.borrow_mut().push(user); // 运行时借用检查
}
}
正确做法:重新设计 API,让数据流更清晰。
// 函数式风格:输入 -> 输出
fn add_user(users: Vec<String>, new_user: String) -> Vec<String> {
let mut users = users;
users.push(new_user);
users
}
// 或者用更明确的所有权转移
fn process_and_return(mut data: Data) -> Result<Data, Error> {
data.transform()?;
Ok(data)
}
错误三:用 unwrap() 和 expect() 掩盖错误
症状:unwrap() 满天飞,panic 是家常便饭。
错误示例:
fn parse_config_bad() -> Config {
let file = File::open("config.json").unwrap(); // panic if error
let contents = read_to_string(file).unwrap();
serde_json::from_str(&contents).unwrap() // panic if error
}
正确做法:正确传播错误,让调用者决定如何处理。
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("Failed to read config file: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse config: {0}")]
Parse(#[from] serde_json::Error),
}
fn parse_config() -> Result<Config, ConfigError> {
let contents = std::fs::read_to_string("config.json")?;
let config: Config = serde_json::from_str(&contents)?;
Ok(config)
}
Rust 惯用法自测表
检查一下你的 Rust 代码够不够 "Rustacean":
一、所有权与借用
| 检查项 | ✅ Rustacean | ❌ 反模式 |
|---|---|---|
| 数据传递 | 函数接收和返回数据时,明确区分所有权转移(T)、借用(&T)、可变借用(&mut T) | 不考虑所有权,用 .clone() 解决一切 |
| 可变性与共享 | 优先不可变(let),需要修改时才加 mut | 默认 let mut,或者全部用全局可变状态 |
| 生命周期 | 函数签名中显式标注生命周期参数('a),表达引用的有效期 | 忽略生命周期,让编译器"猜" |
| Rc/Arc/Box | 根据需要选择:单线程共享用 Rc,多线程共享用 Arc,动态分发用 Box |
遇到借用冲突就套 RefCell,遇到多线程就套 Mutex |
二、错误处理
| 检查项 | ✅ Rustacean | ❌ 反模式 |
|---|---|---|
| 错误类型 | 定义了有意义的错误类型(enum),错误信息包含上下文 | 所有错误都用 String 或 Box |
| 传播 | 用 ? 操作符传播错误,函数签名返回 Result | 用 .unwrap() 或 .expect() 硬解 |
| 组合 | 用 map、andthen、okor、unwrap_or 等组合方法处理 Option/Result | 用嵌套的 match 或 if let |
| 边界 | 在 API 边界处将错误转换为用户友好的消息 | 让内部错误类型泄漏到公共 API |
三、类型设计
| 检查项 | ✅ Rustacean | ❌ 反模式 |
|---|---|---|
| Struct vs Tuple | 用 struct 命名字段,用元组结构体表示"类型标签",用单元结构体表示"单例" | 滥用 HashMap |
| Trait 设计 | trait 职责单一(Interface Segregation),有默认实现则提供默认实现 | 一个 trait 塞太多方法,或者滥用 derive 生成不需要的方法 |
| 类型别名 | 为复杂泛型起别名(如 type Result |
泛型参数列表长得离谱 |
| Newtype | 用新类型(struct UserId(u64))防止类型混淆 | 所有 ID 都是 u64,混在一起 |
四、代码风格
| 检查项 | ✅ Rustacean | ❌ 反模式 |
|---|---|---|
| 命名 | 变量/函数用 snake_case,类型用 PascalCase,常量全大写下划线分隔 | 混用 JS/Python/Java 的命名风格 |
| 注释 | 写 // TODO: 和 // Note: 说明决策,文档注释 /// 或 //! 说明 API | 写"这行代码做了什么"的废话注释 |
| 模块化 | 按功能拆分模块(mod),按职责组织文件 | 一个 main.rs 或 lib.rs 写三千行 |
| Cargo.toml | 依赖列表整洁,[features] 定义清楚,dev-dependencies 和正式依赖分开 | 所有 crate 塞进 dependencies |
五、性能意识
| 检查项 | ✅ Rustacean | ❌ 反模式 |
|---|---|---|
| Iterator | 用 iterator chain 代替显式循环,避免中间 allocation | 每次 .map().collect() 都创建新 Vec |
| 预分配 | 知道数据大小时用 Vec::with_capacity() 预分配 | 不断 push 导致多次扩容 |
| Inline | 热路径用 #[inline] 提示编译器内联 | 过度抽象导致无法内联 |
| SIMD | 需要极致性能时用 std::simd 或 packed_simd | 以为 Rust 不能做 SIMD |
结语:拥抱约束,获得自由
写 Rust 代码的过程,就像和一个极度较真的同事合作。这位同事不关心你的产品经理催得多紧,不关心你的算法有多巧妙,它只关心一件事:你的代码会不会导致内存安全问题?
一开始你可能会觉得它烦人。每次编译错误都在打断你的思路,每次 borrow 检查失败都让你想砸键盘。但等你真正理解它的逻辑,就会发现:它帮你找到的每一个问题,都是真实存在的 bug。CVE 统计里有多少漏洞是 use-after-free、多少是 data race、多少是 null pointer dereference?Rust 的 borrow checker 就是为了消灭这些问题而生的。
从 TypeScript 到 Rust 的思维转变,本质上是从"运行时自由"到"编译时契约"的转变。你失去的是"先跑再说"的便利,得到的是编译即上线的信心。
所以,下次你的代码被 borrow checker 拒绝时,不要骂它。深呼吸,理解它的逻辑,然后重新设计你的代码。这不是 Rust 在为难你,这是 Rust 在保护你。
祝你在这场思维重构中收获满满,写出越来越多真正 "Rustacean" 的代码!
文档信息
版权声明:可自由转载(请注明转载出处)-非商用-非衍生
发表时间:2026年4月15日 11:18