🧭 编程学习站

Rust 入门 + 进阶训练

公网版 · 手机/电脑自适应

🐣 纯新手先看这一框(30 秒秒懂)

1

Rust 适合做什么?系统编程(操作系统、嵌入式)、高性能服务端、命令行工具、WebAssembly。它和 C/C++ 抢地盘,但没有垃圾回收、内存还绝对安全

2

为什么要学它?编译期就帮你挡掉空指针、数据竞争、内存泄漏这些最难的 bug。新手劝退率高,但一旦入门,写出来的程序极其可靠。

3

怎么跑起来?本站「动手实验室」选 Rust 点运行即可;本地装 rustup,用 cargo run 一键编译运行。

4

新手最常踩的坑?① 折磨人的「所有权」报错;② 变量默认不可变,改值要加 mut;③ println! 末尾有感叹号(它是宏);④ 字符串字面量是 &str 不是 String

1. Hello World 与 cargoRust 的入口与构建工具

Rust 程序入口是 main 函数。构建用 cargo(Rust 的 npm + git)。新建项目:cargo new hello && cd hellocargo 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
}
干货shadowing 和 mut 不同:shadowing 是「重新声明一个新变量」,旧变量被遮蔽;mut 是「同一个变量允许改值」。

3. 数据类型标量 + 复合

标量:有符号/无符号整数(i8..i128u8..u128isize/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]);
}
易错点数组越界在 Rust 里不会编译期全拦,但运行时会触发 panic(比 C 直接读乱内存安全多了)。

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 离开作用域,内存被释放
核心这和 Java/Python 的「引用赋值」完全不同。Rust 默认是移动语义:赋值不是复制,而是把资源「搬走」。想复制要用 .clone()
秒懂类比像把一本书递给别人——书只有一本,递过去后你就没了。C++ 的 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;安全做法是用 matchunwrap_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
}
核心Rust 的「Send/Sync」标记 trait 在编译期就保证:不能把非线程安全的数据塞进线程。这是 Rust 并发「无畏」的根基。

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

② 所有权移动

③ 引用借用求长度

④ 结构体方法

⑤ 迭代器:奇数平方和

💡 连老师都不会教你的干货(工业级技巧)

1. 所有权三规则每值有唯一所有者 / 同时刻仅一个所有者 / 离开作用域即 drop。理解它是看懂一切报错的前提。
2. NLL 非词法生命周期Rust 2018 起借用检查基于「实际使用范围」而非词法块,很多以前要另起变量的写法现在直接能过。
3. Box / Rc / Arc 怎么选单所有者堆分配用 Box;单线程多所有者共享只读用 Rc;多线程共享用 Arc
4. dyn Trait 的动态分发Box<dyn Trait> 支持异构集合但走 vtable 有运行时开销;泛型 <T: Trait> 是静态分发、零成本但代码膨胀。按「是否需要异构」选。
5. unsafe 的边界只能做:解裸指针、调 unsafe 函数、读可变静态、实现 unsafe trait、访问 union 字段。能用 safe 就别碰它。
6. 零成本抽象迭代器、泛型、trait 在 release 下编译出的汇编与手写循环几乎一致——抽象不付运行时代价。
7. Result vs panic可预期错误用 Result(强制处理);程序无法继续的 bug 才 panic!
8. Send / Sync决定类型能否跨线程移动 / 被多线程共享。标准库已为大多数类型标好,编译器据此在编译期拦住数据竞争。
9. cargo workspace多 crate 大项目用 workspace 共享 Cargo.lock 与依赖,避免重复编译。
10. #[derive] 派生宏#[derive(Debug, Clone, PartialEq)] 让编译器自动生成样板代码,比手写可靠。
11. miri 检测 UBcargo +nightly miri test 能跑出普通编译器查不到的未定义行为,是底层库的护身符。
12. ? 运算符与 From? 不仅提前返回,还会用 From trait 自动把错误类型转换,多层调用不必手动 map_err。
13. clippy 静态检查cargo clippy 比编译器更挑刺,能抓出惯用法误用和性能隐患,CI 里必跑。
14. feature flag 条件编译#[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:&strString 区别?
    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 强制你处理「没有」的情况,比直接崩溃或返回垃圾值安全。
本 Rust 课程遵循 Rust 2021 edition 常用子集,手机与电脑端自适应,配套本站运行器(rustc 1.82)练习效果最佳。 公网参考:rust-lang.org · doc.rust-lang.org · rustlings(交互练习)。
🛠 在线练习