编码 · 编程语言
Rust 入门教程(零基础)
简短回答
Rust 是一门编译型、静态类型的系统编程语言,速度与 C 和 C++ 相当,却能在编译期杜绝内存错误,并且不需要垃圾回收器。用 rustup 安装,然后运行 cargo new hello 和 cargo run。核心概念包括:所有权与借用(由借用检查器检查)、带 match 的结构体和枚举、Vec 和 HashMap 等集合,以及用 Result 和 ? 运算符处理错误。
Rust 是什么,用来做什么
Rust 起初是 Graydon Hoare 的个人项目,后来由 Mozilla 赞助,并在 2015 年发布 1.0 版本。现在由 Rust Foundation 负责管理。Rust 用于对速度和可靠性都有要求的场景:命令行工具(ripgrep)、Web 服务、Firefox 的浏览器组件、Linux 内核和 Windows 的部分模块、嵌入式设备和 WebAssembly。它的编译器很严格,但作为回报,在安全的 Rust 代码中,释放后使用(use-after-free)和数据竞争这类整类 bug 都不可能出现。
安装 Rust 并运行 Hello, world
从 rustup.rs 用 rustup 安装工具链。它提供 rustc(编译器)和 Cargo(构建工具与包管理器)。创建一个项目:
cargo new hello
cd hello
cargo run
# Hello, world!Cargo 会生成 src/main.rs:
fn main() {
println!("Hello, world!");
}println! 以 ! 结尾,因为它是宏而不是函数。宏在编译期生成代码;这里的格式字符串会在编译时被检查。
变量与基本类型
变量默认不可变;加上 mut 才允许修改。类型通常可以推断,但每个值都恰好有一个静态类型,数值转换需要用 as 显式进行。
| 类型 | 示例 | 说明 |
|---|---|---|
i32, i64, u8, usize | let n: i32 = 42; | 有符号(i)和无符号(u);usize 用于索引 |
f64 | let x = 3.14; | 默认的浮点类型 |
bool | let ok = true; | true 或 false |
char | let c = 'é'; | 一个 Unicode 标量值,4 字节 |
&str | let s = "hi"; | 借用的字符串切片 |
String | String::from("hi") | 拥有所有权、可增长的 UTF-8 文本 |
| 元组、数组 | (1, "a"), [0; 3] | 固定大小 |
fn main() {
let name = "Ada";
let mut count = 7;
count += 1;
let avg = count as f64 / 2.0;
println!("{name} has {count} items, avg {avg}"); // Ada has 8 items, avg 4
let parsed: i32 = "12".parse().unwrap();
println!("{}", parsed + 1); // 13
}流程控制与函数
if、match 和代码块都是表达式:它们会产生值。函数最后一个不带分号的表达式就是它的返回值。
fn square(x: i32) -> i32 {
x * x
}
fn main() {
let score = 72;
let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" };
println!("{grade}"); // B
for i in 0..3 {
print!("{} ", square(i)); // 0 1 4
}
println!();
let mut n = 10;
while n > 1 {
n /= 2;
}
let label = loop {
break "done"; // loop can return a value with break
};
println!("{n} {label}"); // 1 done
}标志性特性:所有权与借用
每个值恰好有一个所有者。所有者离开作用域时,该值就会被丢弃(释放)。把 String 这样的堆上的值赋给另一个变量会发生移动,旧的名字就不能再使用了。若要在不取得所有权的情况下使用某个值,就用引用借用它:同一时间可以有任意多个共享引用 &T,或者恰好一个可变引用 &mut T。
fn length(s: &str) -> usize {
s.len() // borrows, does not take ownership
}
fn add_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let a = String::from("hello");
let b = a; // ownership moves to b
// println!("{a}"); // error: borrow of moved value: `a`
let mut c = b.clone(); // explicit deep copy
add_exclamation(&mut c);
println!("{} {} {}", b, c, length(&c)); // hello hello! 6
}这些规则由借用检查器在编译期强制执行,这就是 Rust 无需垃圾回收器也能保证内存安全的方式。整数等简单类型实现了 Copy,所以会被复制而不是移动。
结构体、枚举与模式匹配
结构体把字段组织在一起,impl 块为其添加方法。枚举的每个变体都可以携带数据,而 match 必须处理每一个变体,这一点由编译器检查。标准的 Option<T> 枚举(Some(value) 或 None)取代了 null。
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("{:?} {}", p, p.distance()); // Point { x: 3.0, y: 4.0 } 5
println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 })); // 6
let nums = [10, 20];
if let Some(v) = nums.first() {
println!("first is {v}"); // first is 10
}
}trait 定义共享的行为,类似于接口:#[derive(Debug)] 让编译器实现 Debug trait,这样 {:?} 就能打印该结构体。
集合:Vec 和 HashMap
use std::collections::HashMap;
fn main() {
let mut nums = vec![3, 1, 2];
nums.push(5);
nums.sort();
println!("{:?} {}", nums, nums.len()); // [1, 2, 3, 5] 4
let evens: Vec<i32> = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).collect();
println!("{:?}", evens); // [4]
let mut ages: HashMap<&str, u32> = HashMap::new();
ages.insert("ann", 31);
ages.insert("bob", 25);
*ages.entry("ann").or_insert(0) += 1;
println!("{:?} {:?}", ages.get("ann"), ages.get("zed")); // Some(32) None
}带闭包(|n| ...)的迭代器,如 filter、map 和 collect,编译出的代码与手写循环一样快。
用 Result 和 ? 处理错误
可能失败的函数返回 Result<T, E>:要么是 Ok(value),要么是 Err(error)。? 运算符会提前把错误返回给调用者,因此错误处理路径保持简短。无法恢复的 bug 使用 panic!。
use std::num::ParseIntError;
fn parse_age(text: &str) -> Result<u32, ParseIntError> {
let age: u32 = text.trim().parse()?; // returns Err early on bad input
Ok(age)
}
fn main() {
match parse_age("41") {
Ok(age) => println!("age {age}"), // age 41
Err(e) => println!("error: {e}"),
}
match parse_age("abc") {
Ok(age) => println!("age {age}"),
Err(e) => println!("error: {e}"), // error: invalid digit found in string
}
}一个完整的小程序:词频统计
把它放进 src/main.rs,然后运行 cargo run -- notes.txt。
use std::collections::HashMap;
use std::{env, fs, process};
fn main() {
let path = env::args().nth(1).unwrap_or_else(|| {
eprintln!("usage: words FILE");
process::exit(1);
});
let text = match fs::read_to_string(&path) {
Ok(t) => t.to_lowercase(),
Err(e) => {
eprintln!("cannot read {path}: {e}");
process::exit(1);
}
};
let mut counts: HashMap<&str, usize> = HashMap::new();
for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
*counts.entry(word).or_insert(0) += 1;
}
let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in top.iter().take(5) {
println!("{n:5} {word}");
}
}HashMap 存放的是借用自 text 的 &str 切片,因此没有复制任何单词;借用检查器保证 text 的存活时间足够长。
新手常见错误
- 在值被移动之后继续使用它。请借用它(
&value),或有意地克隆它。 - 试图用
s[0]对String做索引。字符串是 UTF-8,所以请使用s.chars(),或在字符边界处使用字节切片。 - 持有指向
Vec的引用的同时又向它 push;push 可能会重新分配内存,所以编译器禁止这样做。 - 混淆
String(拥有所有权)和&str(借用)。函数参数接受&str就能同时支持两者。
接下来学什么
阅读《The Rust Programming Language》(俗称 the book,可在 doc.rust-lang.org 免费阅读),并通过 Rust by Example 查看简短的可运行示例。然后深入学习 trait 与泛型、生命周期、用 cargo test 做测试,以及 crates.io 上的 crate,例如用于序列化的 serde 和用于异步程序的 tokio。运行 cargo clippy 可以获得更多代码检查建议。
常见问题
Rust 难学吗?
最初的几周比大多数语言都更难,因为所有权和借用是全新的概念。熟悉之后,编译器会及早发现许多 bug,很多开发者也觉得 Rust 代码更容易维护。
Rust 有垃圾回收器吗?
没有。内存会在其所有者离开作用域时自动释放,这由所有权规则在编译期决定。需要共享所有权时,可以使用引用计数类型 Rc 和 Arc。
什么是 unsafe Rust?
unsafe 块中的代码可以做编译器无法验证的事情,例如解引用裸指针或调用 C 函数。应当谨慎使用,通常会把它封装在安全的 API 之内。
Rust 和 Go 该学哪个?
Go 更简单,学起来更快,在网络服务领域很流行。Rust 提供更强的控制力和性能以及更严格的保证,代价是学习曲线更陡。