欢迎踏上Rust学习之旅!
第一周:奠定基础 (Week 1: Laying the Foundation)
第1天:环境搭建与 “Hello, World!”
核心概念: 安装Rust工具链 (
rustup
),它包含了编译器rustc
和包管理器Cargo
。Cargo是你的好朋友,用于创建项目、构建、测试和管理依赖。代码示例:
main.rs
文件内容// main函数是每个可执行Rust程序的入口点 fn main() { // println! 是一个宏,用于将文本打印到控制台 println!("Hello, world!"); }
编程练习:
访问
rustup.rs
并按照说明安装Rust。打开终端,运行
cargo new hello_rust
创建一个新项目。进入
hello_rust/src/main.rs
文件,修改打印内容为你自己的问候语。在项目根目录运行
cargo run
,看到输出。
第2天:变量、可变性与数据类型初探
核心概念: Rust中的变量默认是不可变的(immutable)。使用
mut
关键字可以使其变为可变的。Rust是静态类型语言,但编译器通常可以推断出你想要的类型。代码示例:
fn main() { // 默认不可变 let x = 5; println!("The value of x is: {}", x); // x = 6; // <-- 这行会编译错误! // 使用 mut 关键字使其可变 let mut y = 10; println!("The initial value of y is: {}", y); y = 20; println!("The new value of y is: {}", y); }
编程练习: 创建一个程序,声明一个不可变变量
spaces
并赋值为一个字符串。再声明一个同名的可变变量,记录字符串的长度。这种“遮蔽”(Shadowing)是允许的。
第3天:标量数据类型 (Scalar Types)
核心概念: 了解Rust的四种基本标量类型:整数、浮点数、布尔值和字符。
代码示例:
fn main() { // 整数 (Integer) let apples: i32 = 5; // 带类型标注 let bananas = 10u64; // 带类型后缀 // 浮点数 (Floating-Point) let pi: f64 = 3.14159; // 布尔值 (Boolean) let is_rust_fun: bool = true; // 字符 (Character) - 使用单引号 let heart_eyed_cat = '😻'; println!("Apples: {}, Bananas: {}, Pi: {}", apples, bananas, pi); println!("Is Rust fun? {}, Look: {}", is_rust_fun, heart_eyed_cat); }
编程练习: 编写一个程序,进行一些基本的数学运算,比如整数加法和浮点数除法,并将结果打印出来。
第4天:函数 (Functions)
核心概念: 使用
fn
关键字定义函数。函数可以有参数和返回值。表达式的最后一个值若不带分号,则会作为函数的返回值。代码示例:
fn main() { let sum = add_five(10); println!("10 + 5 = {}", sum); } // 定义一个函数,接收一个 i32 参数,返回一个 i32 fn add_five(x: i32) -> i32 { x + 5 // 这是一个表达式,它的值将作为函数的返回值 }
编程练习: 编写一个名为
celsius_to_fahrenheit
的函数,它接收一个f64
类型的摄氏温度作为参数,返回转换后的华氏温度(公式:F=Ctimes1.8+32)。在main
函数中调用它并打印结果。
第5天:控制流 (Control Flow)
核心概念: 学习
if-else
表达式和三种循环:loop
,while
,for
。代码示例:
fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else { println!("number is not divisible by 4 or 3"); } // for 循环 for i in 1..=5 { // `..=` 表示包含5 println!("Looping: {}", i); } }
编程练习: 使用
loop
循环,在循环体中将一个计数器加一,当计数器达到10时,使用break
关键字并从循环中返回计数器的两倍值。
第6天:复合类型:元组(Tuple)和数组(Array)
核心概念: 元组是固定长度、可包含多种类型的集合。数组是固定长度、所有元素必须是相同类型的集合。
代码示例:
fn main() { // 元组 (Tuple) let user_info: (&str, i32, bool) = ("Alice", 30, true); // 解构元组 let (name, age, _is_active) = user_info; println!("User: {}, Age: {}", name, age); // 通过索引访问 println!("First element: {}", user_info.0); // 数组 (Array) let months: [&str; 3] = ["January", "February", "March"]; println!("The first month is: {}", months[0]); }
编程练习: 创建一个元组来存储一个HTTP状态(状态码
u16
,消息&str
),例如(200, "OK")
。然后创建一个包含5个浮点数的数组,计算并打印它们的平均值。
第7天:所有权 (Ownership)
核心概念: 内存由“所有者”管理。当所有者离开作用域,值被清理。值只能有一个所有者。赋值操作会发生“移动”(Move)。
代码示例:
fn main() { // s1 拥有一个 String let s1 = String::from("hello"); // s1 的所有权被“移动”到 s2 let s2 = s1; // println!("s1 is {}", s1); // <-- 这行会编译错误!因为 s1 不再拥有数据 println!("s2 is {}", s2); // s2 现在是所有者,可以被使用 }
编程练习: 创建一个
String
类型的变量s1
。将其赋值给s2
。然后尝试打印s1
,仔细阅读并理解编译器关于“值被移动”(value moved)的错误信息。
第二周:深入所有权与结构化数据
第8天:引用 (References) 与借用 (Borrowing)
核心概念: 如果不想转移所有权,可以创建值的“引用”,这被称为“借用”。引用默认不可变。
&mut
创建可变引用。代码示例:
fn main() { let s1 = String::from("hello"); // calculate_length 函数“借用”了 s1,而不是获取所有权 let len = calculate_length(&s1); // 因为 s1 的所有权没有被移动,所以在这里仍然可以访问它 println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() } // s 在这里离开作用域,但因为它不拥有所引用的数据,所以什么也不会发生
编程练习: 重写第7天的练习。创建一个计算
String
长度的函数,该函数接收String
的引用&String
作为参数。在main
函数中调用此函数后,验证原来的String
变量仍然可用。
第9天:切片 (Slices)
核心概念: 切片允许你引用集合中一部分连续的元素序列,它本身不持有所有权。
代码示例:
fn main() { let sentence = String::from("hello world"); let hello = &sentence[0..5]; // or &sentence[..5] let world = &sentence[6..11]; // or &sentence[6..] println!("First word: {}, Second word: {}", hello, world); }
编程练习: 编写一个函数,它接收一个字符串切片
&str
,并返回它找到的第一个单词的切片。提示:通过遍历字符串中的字节来寻找空格。
第10天:结构体 (Structs)
核心概念: 使用
struct
关键字创建自定义的复合数据类型。代码示例:
// 定义一个结构体 struct Rectangle { width: u32, height: u32, } fn main() { // 创建一个 Rectangle 实例 let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.width * rect1.height ); }
编程练习: 定义一个
User
结构体,包含username
(String
)、email
(String
) 和active
(bool
) 字段。在main
函数中,创建一个User
实例并打印其email
。
第11天:结构体上的方法 (Methods on Structs)
核心概念: 使用
impl
块为结构体定义方法。方法的第一个参数通常是&self
、&mut self
或self
。代码示例:
struct Rectangle { width: u32, height: u32, } // 为 Rectangle 实现方法 impl Rectangle { // `&self` 是 `self: &Rectangle` 的缩写 fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; // 使用点号调用方法 println!("The area is {}", rect1.area()); }
编程练习: 为第10天的
User
结构体实现一个deactivate
方法,它接收一个&mut self
,将active
字段设置为false
。
第12天:枚举 (Enums) 与模式匹配 (Pattern Matching)
核心概念:
enum
允许你定义一个可以枚举出不同可能值的类型。match
控制流运算符必须穷尽所有可能性。代码示例:
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn process_message(msg: Message) { match msg { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y), Message::Write(text) => println!("Text message: {}", text), Message::ChangeColor(r, g, b) => println!("Change color to R:{}, G:{}, B:{}", r, g, b), } } fn main() { process_message(Message::Write(String::from("hello"))); process_message(Message::Quit); }
编程练习: 创建一个
TrafficLight
枚举,包含Red
、Yellow
、Green
三个变体。编写一个函数,接收一个TrafficLight
枚举,使用match
返回每种灯需要等待的时间(u8
类型)。
第13天:Option
枚举
核心概念:
Option<T>
用于处理可能为空的值,避免了null
带来的问题。它有两个变体:Some(T)
和None
。代码示例:
fn find_division_result(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None } else { Some(numerator / denominator) } } fn main() { let result1 = find_division_result(10.0, 2.0); let result2 = find_division_result(10.0, 0.0); match result1 { Some(value) => println!("Result 1: {}", value), None => println!("Result 1: Cannot divide by zero"), } // if let 是 match 的一种简写形式 if let Some(value) = result2 { println!("Result 2: {}", value); } else { println!("Result 2: Cannot divide by zero"); } }
编程练习: 编写一个函数,接收一个整数数组的切片,如果切片不为空,则返回
Some
包含第一个元素的值,否则返回None
。在main
函数中使用match
来处理这个Option
结果。
第14天:包、Crate和模块 (Packages, Crates, and Modules)
核心概念: 使用
mod
和use
来组织代码,将代码分割到不同文件和模块中。代码示例:
// 文件结构: // ├── src // │ ├── main.rs // │ └── front_of_house.rs // src/main.rs mod front_of_house; // 声明 front_of_house 模块,Rust会查找 front_of_house.rs // 使用 use 关键字将 hosting 引入作用域 use front_of_house::hosting; fn main() { hosting::add_to_waitlist(); } // src/front_of_house.rs // 模块默认是私有的,需要用 pub 关键字使其公开 pub mod hosting { pub fn add_to_waitlist() { println!("Added to waitlist!"); } }
编程练习: 将第11天的
User
结构体及其impl
块移动到一个名为models.rs
的独立文件中。然后在main.rs
中通过mod models;
和use models::User;
来使用它。
第三周:集合、错误处理与泛型
第15天:Vector (动态数组)
核心概念:
Vec<T>
是一种可增长的、存储在堆上的集合类型。代码示例:
fn main() { // 创建一个可变的 vector let mut v: Vec<i32> = Vec::new(); // 添加元素 v.push(5); v.push(6); v.push(7); // 访问元素(安全的方式) match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), } // 遍历 vector for i in &v { println!("{}", i); } }
编程练习: 创建一个
Vec<i32>
,向其中添加数字1到5。然后遍历这个vector,将每个元素乘以2,并将结果存储在一个新的vector中。
第16天:字符串 (String)
核心概念:
&str
是字符串切片,String
是堆上分配、可变的字符串。代码示例:
fn main() { let mut s = String::from("foo"); s.push_str("bar"); println!("{}", s); // "foobar" let s1 = String::from("Hello, "); let s2 = String::from("world!"); // + 操作符会获取 s1 的所有权 let s3 = s1 + &s2; println!("{}", s3); // println!("{}", s1); // s1 在这里不再有效 }
编程练习: 创建一个空的
String
,然后使用push_str()
和push()
方法向其中添加文本和字符,最后将其与其他&str
拼接成一句话并打印。
第17天:哈希表 (Hash Maps)
核心概念:
HashMap<K, V>
用于存储键值对。代码示例:
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); // get 方法返回一个 Option<&V> let score = scores.get(&team_name).copied().unwrap_or(0); println!("Blue team score: {}", score); // 遍历 for (key, value) in &scores { println!("{}: {}", key, value); } }
编程练习: 创建一个
HashMap
来统计一段文本中每个单词出现的次数。
第18天:错误处理与Result
核心概念:
Result<T, E>
用于处理可能失败的操作。?
运算符可以简化错误传播。代码示例:
use std::fs::File; use std::io::Read; // 函数返回一个 Result fn read_username_from_file() -> Result<String, std::io::Error> { let mut f = File::open("hello.txt")?; // `?` 如果是 Err,则直接返回 Err let mut s = String::new(); f.read_to_string(&mut s)?; // `?` 如果是 Err,则直接返回 Err Ok(s) // 如果成功,则返回 Ok(s) } fn main() { match read_username_from_file() { Ok(username) => println!("Username: {}", username), Err(e) => println!("Error reading file: {}", e), } }
编程练习: 编写一个
safe_divide
函数,接收两个f64
参数。如果除数为零,则返回一个Err
包含描述错误的字符串;否则返回Ok
包含除法结果。在main
中调用它并使用match
处理Result
。
第19天:泛型 (Generics)
核心概念: 泛型是具体类型或其他属性的抽象替代,用于减少代码重复。
代码示例:
// 泛型结构体 struct Point<T> { x: T, y: T, } // 泛型函数 fn print_point<T: std::fmt::Display>(p: &Point<T>) { println!("Point is at ({}, {})", p.x, p.y); } fn main() { let integer_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.0, y: 4.0 }; print_point(&integer_point); print_point(&float_point); }
编程练习: 创建一个泛型函数
largest<T: PartialOrd>(list: &[T]) -> &T
,它可以找到任何实现了PartialOrd
trait(即可比较大小)的类型的切片中的最大元素。
第20天:Trait (特性)
核心概念: Trait类似于接口,定义了某种类型必须提供的方法签名。
代码示例:
// 定义一个 Trait pub trait Summary { fn summarize(&self) -> String; } pub struct Tweet { pub username: String, pub content: String, } // 为 Tweet 类型实现 Summary Trait impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } fn main() { let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from("of course, as you probably already know, people"), }; println!("1 new tweet: {}", tweet.summarize()); }
编程练习: 定义一个名为
CanSpeak
的trait,它有一个speak(&self) -> String
方法。为你之前创建的User
结构体和新创建的Cat
结构体实现这个trait。
第21天:生命周期 (Lifetimes)
核心概念: 生命周期是编译器用来确保所有借用都有效的范围。大多数情况编译器可自动推断,复杂时需手动标注。
代码示例:
// 'a 是生命周期参数 // 它告诉 Rust,返回的引用的生命周期与 x 和 y 中较短的那个生命周期相关联 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); // result 的生命周期被限制在 string2 的生命周期内 result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {}", result); } // println!("The longest string is {}", result); // <-- 这里会报错,因为 string2 和 result 都已失效 }
编程练习: 编写一个结构体
ImportantExcerpt<'a>
,它包含一个字段part
,类型为&'a str
。验证你不能创建一个比它所引用的字符串活得更长的ImportantExcerpt
实例。
第四周:高级特性与并发
第22天:闭包 (Closures)
核心概念: 闭包是可以捕获其环境的匿名函数。
代码示例:
fn main() { let x = 4; // 这个闭包捕获了环境中的 x let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); let v1 = vec![1, 2, 3]; // map 方法接收一个闭包作为参数 let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); println!("v2: {:?}", v2); // v2: [2, 3, 4] }
编程练习: 使用
find
方法和一个闭包,在一个Vec<i32>
中查找第一个大于10的数字。
第23天:迭代器 (Iterators)
核心概念: 迭代器是处理元素序列的一种懒惰(lazy)且高效的方式。
代码示例:
fn main() { let v1 = vec![1, 2, 3, 4, 5]; let iterator = v1.iter(); // 创建迭代器 // 迭代器是懒惰的,需要消耗它们才能做事 let total: i32 = iterator.sum(); // sum() 会消耗迭代器 println!("Sum: {}", total); // 链式调用 let v2: Vec<i32> = vec![1, 2, 3]; let v3: Vec<_> = v2.iter().map(|x| x * 2).filter(|&x| x > 4).collect(); println!("v3: {:?}", v3); // v3: [6] }
编程练习: 创建一个从1到100的数字vector。使用迭代器、
filter
和map
链式调用,找出所有能被3整除的数,将它们平方,然后使用sum
计算总和。
第24天:智能指针 (Box
, Rc
, RefCell
)
核心概念:
Box<T>
在堆上分配值;Rc<T>
允许多个所有者;RefCell<T>
在运行时检查借用规则。代码示例: (
Box
用于递归类型)// 使用 Box 来创建递归的 Cons List 类型 enum List { Cons(i32, Box<List>), Nil, } use List::{Cons, Nil}; fn main() { // (1, (2, (3, Nil))) let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
编程练习: 使用
Rc<T>
来创建一个允许多个列表共享同一个尾部(另一个列表)的数据结构。
第25天:并发:线程 (Threads)
核心概念: 使用
thread::spawn
创建新线程。move
关键字常用于闭包中,以转移所有权。代码示例:
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } // 等待子线程结束 handle.join().unwrap(); }
编程练习: 创建一个新线程,它持有一个
Vec
的所有权并打印其中的元素。在主线程中尝试访问这个Vec
,观察编译错误。
第26天:并发:消息传递 (Message Passing)
核心概念: 使用通道(Channel)在线程间安全地传递消息,避免共享内存。
代码示例:
use std::sync::mpsc; // multiple producer, single consumer use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); // println!("val is {}", val); // val 的所有权已经转移,这里会报错 }); let received = rx.recv().unwrap(); println!("Got: {}", received); }
编程练习: 创建一个通道。在一个子线程中,发送多条消息(比如数字1到5)。在主线程中,使用
for
循环来接收并打印所有消息。
第27天:并发:共享状态 (Mutex
与Arc
)
核心概念:
Mutex<T>
提供互斥访问。Arc<T>
是线程安全的引用计数指针,允许多个线程拥有同一个值的引用。代码示例:
use std::sync::{Arc, Mutex}; use std::thread; fn main() { // 使用 Arc 来允许多个所有者,Mutex 来保证一次只有一个线程能修改值 let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { // lock() 会阻塞当前线程,直到可以获取锁 let mut num = counter.lock().unwrap(); *num += 1; }); // 锁在这里被释放 handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); // 最终结果是 10 }
编程练习: 重做以上练习,但这次每个线程将计数器增加2而不是1。验证最终结果是20。
第28天:Cargo进阶与生态系统
核心概念: 通过在
Cargo.toml
中添加依赖来使用crates.io
上的第三方库。代码示例:
// 1. 在 Cargo.toml 文件中添加: // [dependencies] // serde = { version = "1.0", features = ["derive"] } // serde_json = "1.0" // 2. 在 main.rs 中使用: use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] struct Point { x: i32, y: i32, } fn main() { let point = Point { x: 1, y: 2 }; // 序列化为 JSON 字符串 let serialized = serde_json::to_string(&point).unwrap(); println!("serialized = {}", serialized); // 反序列化 let deserialized: Point = serde_json::from_str(&serialized).unwrap(); println!("deserialized = {:?}", deserialized); }
编程练习: 在你的项目中添加一个流行的第三方库,比如
rand
,用它来生成一个随机数并打印出来。
第29天:unsafe
Rust
核心概念:
unsafe
关键字让你绕过编译器的某些安全检查,用于与非Rust代码交互、或进行编译器无法理解的底层优化等。代码示例: (请谨慎使用)
fn main() { let mut num = 5; // 创建裸指针 let r1 = &num as *const i32; let r2 = &mut num as *mut i32; // 解引用裸指针必须在 unsafe 块中进行 unsafe { println!("r1 is: {}", *r1); *r2 = 10; // 修改数据 println!("r2 now points to: {}", *r2); } }
编程练习: (仅为理解)编写一个
unsafe
块,创建一个指向整数的裸指针,并解引用它来读取值。思考在哪些情况下这个操作可能导致未定义行为。
第30天:下一步与项目构思
核心概念: 回顾所学,总结Rust的核心优势。探索生态系统,如异步编程(
async/await
)、WebAssembly、嵌入式等。代码示例: (异步代码
async/await
概念)// 需要添加 tokio 依赖: `tokio = { version = "1", features = ["full"] }` #[tokio::main] async fn main() { println!("Hello"); // .await 表示等待异步操作完成,但不会阻塞整个线程 let result = fetch_data_from_db().await; println!("Data fetched: {}", result); } async fn fetch_data_from_db() -> String { // 模拟一个耗时的数据库查询 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; "Some data from database".to_string() }
编程练习: 为自己构思一个小的结业项目。例如:一个简单的命令行待办事项应用、一个TCP端口扫描器、一个简单的Web服务器。写下项目的功能需求和你计划使用的crates,然后开始动手吧!
祝您学习愉快,编程顺利!欢迎来到Rust的世界!