Rust 入门(写给 C++ 开发者)
Rust 与 C++ 目标相似:无垃圾回收、贴近硬件、可写系统软件。差别在于 Rust 把内存安全前移到了编译期——悬垂指针、越界、数据竞争在默认情况下无法通过编译。本篇按 C++ 的心智模型对照展开。示例在 WSL rustc/cargo 1.75 编译运行验证。
安装与第一个程序
# 推荐 rustup(官方多版本管理器);发行版 apt 的版本通常滞后
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo --version
cargo new hello && cd hello # 生成 Cargo.toml + src/main.rs
cargo run # 编译并运行
cargo build --release # 优化构建(对应 -O2)
cargo test # 运行内置测试框架
Cargo.toml 之于 Rust 就是 CMakeLists.txt + 依赖清单:
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8" # 一行加依赖,cargo build 时自动拉取
变量与类型:默认不可变
let x = 5; // 默认不可变(const 的语感,但有类型推导)
let mut y = 10; // mut 才可以改
y += 1;
let z: f64 = 2.5; // 显式类型;推导写 let z = 2.5;
let (a, b) = (1, "text"); // 解包元组
const MAX: u32 = 100_000; // 编译期常量;数字分隔符与 C++14 相同
所有权:与 C++ 移动语义的本质区别
C++ 的移动是把"搬走后的对象"留在一个合法但未指定的状态,编译器不阻止你继续用它。Rust 的规则更严格:
let s1 = String::from("hello");
let s2 = s1; // 所有权移动(move),等价 C++ 的 std::move
// println!("{}", s1); // ← 编译错误:s1 已失效,不存在"移动后可用"
let s3 = s1.clone(); // 真正深拷贝要显式写
let n1 = 5;
let n2 = n1; // 整数是 Copy 类型,按位复制,n1 仍可用
三条所有权规则:
- 每个值有唯一的所有者(变量);
- 同一时刻只能有一个所有者;
- 所有者离开作用域,值被释放(对应 C++ 的 RAII,析构函数这里叫
Drop)。
函数传参与返回同样移动所有权——所以下面的代码无法编译,这是新手遇到的第一堵墙:
fn length(s: String) -> usize { s.len() }
let s = String::from("hi");
// let n = length(s); // 所有权交进函数后,s 在外层就没了
// println!("{}", s); // ← 编译错误
借用:不交出所有权的访问
不想转移所有权时,用引用(借用):
fn length(s: &String) -> usize { s.len() } // 只读借用
let s = String::from("hi");
let n = length(&s); // 借出去,s 依然可用
println!("{} {}", s, n); // 合法
借用规则(编译期检查,这就是"没有数据竞争"的来源):
- 任意数量的不可变借用
&T,或者恰好一个可变借用&mut T; - 二者不能同时存在——有写者时禁止读者,数据竞争在编译期被判死刑。
对比 C++:const T& ≈ &T;T& ≈ &mut T,但 C++ 不检查"写者独占",Rust 检查。代价是要改变习惯:遍历中修改集合这类操作需要重新设计(例如先收集下标再改)。
enum 与 match:比 switch 强得多的分支
enum Shape {
Circle(f64), // 变体可以携带数据
Rectangle(f64, f64),
Point,
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => 3.14159 * r * r, // 解构出携带的值
Shape::Rectangle(w, h) => w * h,
Shape::Point => 0.0, // 必须穷尽所有变体
}
}
match 要求覆盖全部情况(少一个变体编译失败),还能匹配区间、元组、多重模式。Option<T>(替代空指针)和 Result<T, E>(替代错误码/异常)是两个最重要的标准库枚举:
let maybe: Option<i32> = Some(3); // 有值:Some(3);无值:None
match maybe {
Some(v) => println!("got {}", v),
None => println!("nothing"), // 编译器强制处理 None 分支——空指针解引用在此绝迹
}
错误处理:Result 与 ? 运算符
Rust 没有异常。可能失败的操作返回 Result<T, E>(成功 Ok,失败 Err,语义同 C++ 的 std::expected):
use std::num::ParseIntError;
use std::fs;
fn read_number(path: &str) -> Result<i32, ParseIntError> {
let text = fs::read_to_string(path)?; // Err 时立即向上返回,Ok 时解出值
let n = text.trim().parse::<i32>()?; // ? 链式传播,层层少写代码
Ok(n)
}
? 运算符就是 std::expected monadic 风格的语言级内置——错误传播从模板技巧变成了一等语法。
集合与迭代器
let mut v = vec![1, 2, 3]; // Vec<T> ≈ std::vector
v.push(4);
let doubled: Vec<i32> = v.iter() // 迭代器 ≈ C++20 ranges 管道
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect(); // [4, 8]
for x in &v { println!("{}", x); } // &v 借用遍历;for x in v 则移动
内置测试
fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3); // assert_eq!/assert_ne!/assert!
}
}
cargo test 自动发现并运行——不需要第三方测试框架(对应 GoogleTest 与 pytest)。
与 C++ 概念对照表
| C++ | Rust | 备注 |
|---|---|---|
std::move 后原对象状态未指定 |
move 后编译器禁止使用 | 安全性的来源 |
| RAII / 析构函数 | RAII / Drop trait |
思想一致 |
const T& / T& |
&T / &mut T |
Rust 强制写者独占 |
std::unique_ptr |
默认就是独占所有权 | 不需要智能指针 |
std::shared_ptr |
Rc<T> / Arc<T> |
Arc = 原子引用计数 |
std::optional / std::expected |
Option<T> / Result<T, E> |
语言级支持 + ? |
| 模板 / concepts | 泛型 / trait | trait ≈ concepts + 接口 |
| 继承 + 虚函数 | trait 对象 / 泛型 | Rust 没有继承,组合优先 |
#include / 模板实例化 |
crate + 模块系统 | 无头文件,编译模型完全不同 |
| gtest / pytest | 内置 #[test] |
标准库自带 |
| CMake + vcpkg | cargo | 一条命令管构建、测试、依赖、发布 |
常见坑
| 问题 | 说明 |
|---|---|
String vs &str |
String 是拥有所有权的字符串(≈ std::string),&str 是字符串切片(≈ string_view);函数参数优先收 &str |
| 整型溢出 | debug 构建直接 panic,release 静默回绕——跨平台代码别依赖回绕行为,显式用 checked_add 等 |
| 未使用变量警告 | let _x = ... 或 _ = ... 显式忽略 |
| 与 C++ 互操作 | unsafe + FFI 块;安全边界在 unsafe 段内由程序员负责 |
| 克隆成瘾 | 到处 .clone() 能让编译器闭嘴,但违背学习目的——先想清楚所有权该归谁 |
继续走
- 官方入门书 The Rust Book(有中文翻译);
- Rust by Example 边读边跑;
- 练手思路:把熟悉的小工具用 Rust 重写一遍,重点体会所有权如何改变接口设计。