🐣 纯新手先看这一框(30 秒秒懂)
Rust 适合做什么?系统编程(操作系统、嵌入式)、高性能服务端、命令行工具、WebAssembly。它和 C/C++ 抢地盘,但没有垃圾回收、内存还绝对安全。
为什么要学它?编译期就帮你挡掉空指针、数据竞争、内存泄漏这些最难的 bug。新手劝退率高,但一旦入门,写出来的程序极其可靠。
怎么跑起来?本站「动手实验室」选 Rust 点运行即可;本地装 rustup,用 cargo run 一键编译运行。
新手最常踩的坑?① 折磨人的「所有权」报错;② 变量默认不可变,改值要加 mut;③ println! 末尾有感叹号(它是宏);④ 字符串字面量是 &str 不是 String。
1. Hello World 与 cargoRust 的入口与构建工具
Rust 程序入口是 main 函数。构建用 cargo(Rust 的 npm + git)。新建项目:cargo new hello && cd hello,cargo run 自动编译运行。
fn main() {
// println! 是宏(结尾 !),往标准输出打印并换行
println!("你好,Rust!");
println!("1 + 2 = {}", 1 + 2); // {} 是占位符
}
{} 像 C 的 %d 占位符,按顺序填入后面的值;println! 的 ! 说明它是宏不是普通函数。! 会报 "expected function, found macro",这是 Rust 新手最高频的低级错误。2. 变量与可变性默认不可变是 Rust 的价值观
默认变量不可变(immutable)。要改值必须显式写 mut。还有 shadowing(重影):同名变量可重新绑定,类型还能变。
fn main() {
let x = 5; // 不可变
// x = 6; // ❌ 编译错误:cannot assign twice to immutable
let mut y = 5;
y = 6; // ✅ 加了 mut 才能改
let s = "hello";
let s = s.len(); // shadowing:s 从 &str 变成 usize,合法
println!("s = {}", s); // 输出 5
}
mut 不同:shadowing 是「重新声明一个新变量」,旧变量被遮蔽;mut 是「同一个变量允许改值」。3. 数据类型标量 + 复合
标量:有符号/无符号整数(i8..i128、u8..u128、isize/usize)、浮点(f32/f64)、布尔、字符(char,4 字节 Unicode)。复合:元组 tuple、数组 [T; N]。
fn main() {
let a: i32 = -10;
let b: u8 = 200; // 无符号,不能存负数
let c = 3.14; // 默认 f64
let tup: (i32, f64, char) = (500, 6.4, 'R');
let (x, y, z) = tup; // 解构
let arr = [1, 2, 3, 4, 5];
println!("{} {} {} arr[0]={}", x, y, z, arr[0]);
}
4. 所有权三规则Rust 的灵魂,必须吃透
Rust 不用 GC 也能保证内存安全,靠的就是所有权(ownership)。三条铁律:
- 每个值都有唯一的所有者(owner);
- 同一时刻只能有一个所有者;
- 所有者离开作用域,值被自动
drop(释放)。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 移动(move):s1 的所有权转移给 s2
// println!("{}", s1); // ❌ s1 已失效,报错 "borrow of moved value"
println!("{}", s2); // ✅
} // s2 离开作用域,内存被释放
.clone()。std::move 就是类似思路。5. 引用与借用不拿走所有权也能用
用 & 借引用(borrow),用完不销毁原值。规则:要么有多个不可变引用 &T,要么有一个可变引用 &mut T,二者不共存。
fn main() {
let s = String::from("hi");
let len = calc_len(&s); // 借引用,不转移所有权
println!("'{}' 长度 {}", s, len); // s 还能用
}
fn calc_len(s: &String) -> usize {
s.len() // 通过 & 读取,但不能修改
}
6. 字符串:String 与 &str新手最困惑的一对
String:堆上拥有所有权的可变字符串(String::from("x"))。&str:字符串切片,借来的只读视图(字面量"hello"就是&'static str)。
fn main() {
let owned: String = String::from("hello");
let slice: &str = &owned; // &String 可自动转 &str
let lit: &str = "world";
let mut s = String::from("a");
s.push_str("b"); // 可变
println!("{} {} {}", owned, slice, lit);
}
&str(接受两者),而不是 &String,这样调用更灵活。7. 结构体与方法用 struct 组织数据
结构体用 struct 定义,方法放在 impl 块里。第一个参数 self 代表实例。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 { self.width * self.height }
fn new(w: u32, h: u32) -> Rectangle { Rectangle { width: w, height: h } }
}
fn main() {
let r = Rectangle::new(3, 4);
println!("面积 {}", r.area());
}
8. 枚举与 matchRust 的 enum 比 C 强太多
Rust 枚举可携带数据,配合 match 穷尽匹配。标准库 Option<T> 和 Result<T,E> 就是靠它实现「无空指针」。
enum Shape { Circle(f64), Rectangle(f64, f64) }
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => 3.14 * r * r,
Shape::Rectangle(w, h) => w * h,
}
}
fn main() {
let c = Shape::Circle(2.0);
println!("面积 {}", area(&c));
}
Option<T> 就是「可能有值也可能没有」的类型安全写法,强制你处理 None,从此告别空指针异常。9. 集合 Vec / HashMap动态数组与字典
use std::collections::HashMap;
fn main() {
let mut v = vec![1, 2, 3];
v.push(4);
let third = v.get(2); // 返回 Option<&i32>
println!("第三个={:?}", third);
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
println!("a={}", map.get("a").unwrap_or(&0));
}
get 返回 Option,直接用 v[2] 越界会 panic;安全做法是用 match 或 unwrap_or。10. 错误处理 Result可恢复错误用 Result
Result<T, E> 要么是 Ok(v) 要么是 Err(e)。? 运算符:遇到 Err 直接提前返回,遇到 Ok 解包继续。
use std::fs::File;
fn read_file() -> Result<String, std::io::Error> {
let mut f = File::open("x.txt")?; // 出错就返回 Err
let mut s = String::new();
std::io::Read::read_to_string(&mut f, &mut s)?;
Ok(s)
}
fn main() {
match read_file() {
Ok(c) => println!("内容: {}", c),
Err(e) => println!("读取出错: {}", e),
}
}
panic!;可预期的错误(文件不存在、网络超时)一律用 Result。11. 生命周期标注告诉编译器引用活多久
多数情况编译器能自动推导生命周期(NLL,非词法生命周期)。只有返回引用、且来源不明确时才需要手写 'a。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let r = longest(&s1, "hi");
println!("更长的是 {}", r);
}
'a 不表示「活多久」,而是「x、y、返回值三者寿命取最短的那个」。它只是给编译器一个约束关系。12. 泛型与 trait零成本抽象的基础
trait 类似 Java 的接口,定义「能做什么」。泛型 + trait bound 让代码既通用又高效。
trait Summary { fn headline(&self) -> String; }
struct News { title: String }
impl Summary for News {
fn headline(&self) -> String { format!("新闻: {}", self.title) }
}
fn notify<T: Summary>(item: &T) { println!("{}", item.headline()); }
fn main() {
let n = News { title: String::from("Rust 1.8 发布") };
notify(&n);
}
13. 闭包与迭代器函数式风格的精华
闭包用 |参数| 表达式。迭代器延迟计算,链式调用零成本(编译后和手写循环一样快)。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let sum: i32 = v.iter()
.filter(|&x| x % 2 == 1) // 取奇数
.map(|x| x * x) // 平方
.sum(); // 求和
println!("奇数平方和 = {}", sum); // 1+9+25=35
}
sum/collect/for 才真正消费,这叫「惰性求值」。14. 智能指针Box / Rc / RefCell
| 类型 | 用途 |
|---|---|
Box<T> | 单所有者、把值放堆上(如递归类型) |
Rc<T> | 单线程下多个所有者共享只读数据 |
RefCell<T> | 运行时借用检查,允许内部可变性 |
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("共享"));
let b = Rc::clone(&a); // 引用计数 +1,不是深拷贝
println!("计数={} 值={}", Rc::strong_count(&a), b);
}
15. 并发恐惧数据竞争?编译器不让你犯
用 Arc<T>(原子引用计数,多线程共享)+ Mutex<T>(互斥锁)在线程间共享可变状态。
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..3 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("结果={}", *counter.lock().unwrap()); // 3
}
16. 宏与项目结构写出像样的工程
println!、vec! 都是宏(代码生成)。真实项目用 cargo 管理模块:mod 声明模块,pub 暴露接口。
// 声明宏示例:用 macro_rules! 定义一个 my_vec!
macro_rules! my_vec {
( $( $x:expr ),* ) => { { let mut v = Vec::new(); $( v.push($x); )* v } };
}
fn main() {
let v = my_vec![1, 2, 3];
println!("{:?}", v);
}
src/main.rs(入口)+ src/lib.rs(库)+ tests/(集成测试)。用 cargo test 跑测试。🧪 动手实验室(点 ▶ 直接看结果)
下面每个程序代码已预填,打开会自动运行一次;点 ▶ 运行 看结果,框内可直接改。
① Hello World
② 所有权移动
③ 引用借用求长度
④ 结构体方法
⑤ 迭代器:奇数平方和
💡 连老师都不会教你的干货(工业级技巧)
Box;单线程多所有者共享只读用 Rc;多线程共享用 Arc。Box<dyn Trait> 支持异构集合但走 vtable 有运行时开销;泛型 <T: Trait> 是静态分发、零成本但代码膨胀。按「是否需要异构」选。Result(强制处理);程序无法继续的 bug 才 panic!。Cargo.lock 与依赖,避免重复编译。#[derive(Debug, Clone, PartialEq)] 让编译器自动生成样板代码,比手写可靠。cargo +nightly miri test 能跑出普通编译器查不到的未定义行为,是底层库的护身符。? 不仅提前返回,还会用 From trait 自动把错误类型转换,多层调用不必手动 map_err。cargo clippy 比编译器更挑刺,能抓出惯用法误用和性能隐患,CI 里必跑。#[cfg(feature = "x")] 按编译开关启用代码,一套源码出精简版 / 完整版。自测题(点击展开答案)
- Q1:
let s1 = String::from("x"); let s2 = s1;后还能用 s1 吗?A:不能。这是移动(move),所有权转移给 s2,s1 失效。想复制用s1.clone()。 - Q2:变量默认不可变,怎么让它可改?A:声明时加
mut,如let mut x = 5;。 - Q3:
&str和String区别?A:String是堆上拥有所有权的可变字符串;&str是只读借用(字面量就是它)。函数参数优先用&str。 - Q4:
?运算符做什么?A:遇到Err直接提前返回;遇到Ok解包取值继续。只可用于返回Result的函数。 - Q5:Rust 靠什么在「无 GC」下保证内存安全?A:所有权 + 借用检查。编译期就确定每个值的生命周期和唯一所有者,离开作用域自动 drop。
- Q6:多线程共享可变计数,最小正确组合是?A:
Arc<Mutex<T>>——Arc 负责多线程共享引用计数,Mutex 负责互斥修改。 - Q7:为什么
Vec::get(i)返回Option而非直接给元素?A:索引可能越界,Option强制你处理「没有」的情况,比直接崩溃或返回垃圾值安全。