24小時快速入門大熱語言 Rust!學不會來找我
作者 | richardyao
最近幾年,Rust在業界的使用越來越多,本篇文章從Rust核心語法等基礎知識入手,進而詳細的介紹Rust語言不同于其它語言的思維方式,最后通過一個實戰的項目,帶大家快速入門Rust編程語言。
最近幾年,Rust在業界的使用越來越多。在Windows內核中(win32kbase_rs.sys)、Linux內核中、Chromium瀏覽器中都有Rust的身影,AWS S3也使用rust重構了他們的核心存儲服務ShardStore,Azure的CTO甚至說"Speaking of languages, it's time to halt starting any new projects in C/C++ and use Rust for those scenarios where a non-GC language is required."。從下面的Google Trends也可以看的出來,Rust的熱度正在上升,并且增長很快,可以說現在是學習Rust最好的時機。

本篇文章從Rust核心語法等基礎知識入手,進而詳細的介紹Rust語言不同于其它語言的思維方式,最后通過一個實戰的項目,帶大家快速入門Rust編程語言。
有C++、Golang、Python這些語言基礎的話,大部分知識都可以遷移過去,再加上大模型的輔助,24小時快速入門,是有可能達成的。
一、基礎篇
1. Rust的安裝與基本工具的使用
Rust的安裝直接參考官網的文檔,這里不做更具體的介紹了:https://www.rust-lang.org/tools/install。
在國內使用Rust的話,可以通過 https://rsproxy.cn/ 網站提供的鏡像,更方便快捷的安裝Rust以及下載相關的crate等。
Rust對應的編譯器是rustc,但是這個大家平時使用的并不多,更多的是通過包管理工具cargo等來管理、構建項目。cargo常用命令如下:
cargo build
cargo build --release
cargo clippy
cargo run
cargo run -- --help
cargo clean
cargo check
cargo doc
cargo expand # 需要使用cargo install cargo-expand先安裝Rust日常開發可以使用vscode + rust-analyzer插件。
2. 通過greplite小程序熟悉Rust的語言特點
在這里,我們使用一個簡單greplite程序來介紹Rust語言的特點。
先使用cargo new greplite命令創建一個binary的crate,然后在main.rs中輸入下面的代碼:
use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn main() -> io::Result<()> {
let args = env::args().collect::<Vec<_>>();
if args.len() < 3 {
eprintln!("Usage: greplite <search_string> <file_path>");
std::process::exit(1);
}
let search_string = &args[1];
let file_path = &args[2];
run(search_string, file_path)
}
fn run(search_string: &str, file_path: &str) -> io::Result<()> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.contains(search_string) {
println!("{line}");
}
}
Ok(())
}這個程序的第一個參數是要搜索的字符串,第二個參數是搜索的文件,比如說要搜索src/main.rs文件中包含main函數的行,可以如下執行:
$ cargo run -- main src/main.rs
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/greplite main src/main.rs`
fn main() -> io::Result<()> {這個小程序一共就30行代碼,但是知識點還挺多的,先整體介紹一下:
① use類似于C++中的include,用來引用std標準庫里面的module;
② fn用來定義一個函數,main函數是整個程序的入口;
③ ->表示函數的返回值,io::Result<()>表示返回的是個Result,通過文檔,或者vscode代碼跳轉的方式,可以看到std::io::Result<T>是基于std::result::Result<T, std::io::Error>定義的一個新類型;
https://doc.rust-lang.org/std/io/type.Result.html
pub type Result<T> = std::result::Result<T, std::io::Error>;同時std::result::Result是一個枚舉:
pub enum Result<T, E> {
Ok(T),
Err(E),
}Result表示的值,可以是表示成功的Ok,也可以是表示失敗的Err。
上面pub type Result<T> = std::result::Result<T, std::io::Error>;只是把std::io::Result的錯誤類型固定成std::io::Error,表示只會返回std::io::Error類型的錯誤。
④ io::Result<()>中的(),表示的是一個tuple,這個tuple沒有任何元素,也稱為unit;
⑤ 關于main函數的返回類型:https://doc.rust-lang.org/reference/crates-and-source-files.html?highlight=main#main-functions;
⑥ let args = env::args().collect::<Vec<_>>();中的env::args()返回的值,實現了Iterator這個trait,關于Rust中的迭代器,后面還會重點來講,這個collect是把迭代器的列表收集起來,構造成一個Vec。
這個語句還有下面幾種寫法,都是可以的:
let args = env::args().collect::<Vec<_>>();
let args = env::args().collect::<Vec<String>>();
let args: Vec<String> = env::args().collect();
let args: Vec<_> = env::args().collect();Rust的類型推斷是非常強大的,如果后面對args的使用能確定args的類型的話,也可以完全寫成:
let args = env::args().collect();比如說如果有函數fn print_args(args: &Vec<String>) {},同時后面調用了print_args(&args)的話,args的定義就完全不需要類型注解了。
另外,通過let定義的變量,默認是不可變的(immutable),如果要修改的話,需要顯示的使用let mut args = env::args().collect();
這里還有一個知識點,我們在前面并沒有通過use語句引入Vec,那為什么不報錯呢?
https://doc.rust-lang.org/std/prelude/index.html
Rust編譯器預先已經包含了std中部分常用的組件,這樣代碼會更簡潔一些。
⑦ args.len()中的len()是Vec<String>的成員函數 https://doc.rust-lang.org/std/vec/struct.Vec.html#method.len;
⑧ let search_string = &args[1];和let file_path = &args[2];這里定義了兩個引用,是對Vec<String>中對應元素的不可變的借用;
args[1]下標操作,在運行時會check下標是否越界,但是由于前面判斷了長度,因此這里的運行時的越界檢查,編譯器通常會優化掉。
⑨ 下面是調用run函數,另外這里是一個expression,expression的值作為整個main函數的返回值;
j)、第19行let file = File::open(file_path)?;打開一個文件,這個語句中的?是一個語法糖,這條語句等價于:
let file = match File::open(file_path) {
Ok(f) => f,
Err(err) => return Err(From::from(err)),
};⑩ 第20行let reader = BufReader::new(file);file實現了Read這個trait,然后BufReader是在Read這個trait的基礎上,做了一層封裝,實現了帶緩存的讀,并在此基礎上,提供了lines()等便捷的方法。
https://doc.rust-lang.org/std/fs/struct.File.html#impl-Read-for-File
BufReader實現了BufRead這個trait,因此在use std::io::BufRead;之后,可以調用這個trait對應的lines()等方法。
https://doc.rust-lang.org/std/io/struct.BufReader.html#impl-BufRead-for-BufReader
? 第22行for line in reader.lines() {lines()函數返回了一個迭代器,然后for line in的方式來遍歷這個迭代器,這個迭代器對應的Item為
type Item = Result<String, Error>https://doc.rust-lang.org/std/io/struct.Lines.html#associatedtype.Item
這個被稱為trait的Associated type。
? 這個迭代器的Item是一個Result,因此第23行使用?運算符把其轉換成了普通的String;
? 第24行if line.contains(search_string) {判斷line是否包含要搜索的子串;
? 第25行println!("{line}");打印輸出,和println!("{}", line);等價,前面這種方式被稱為named parameters:
https://doc.rust-lang.org/std/fmt/index.html#named-parameters
另外println!包括前面的eprintln!最后的這個!,表示這是一個宏。
Rust中,函數不支持可變參數,通過宏的方式來實現可變參數。
? 最后第29行,Ok(())這個expression作為整個函數的返回值表示成功。
? 同時我們注意到search_string和file_path都是&String類型的,run函數的參數&str是什么鬼?
類比于C++中的string和string_view,同時string到string_view可以通過string的operator string_view進行隱式轉換:
https://en.cppreference.com/w/cpp/string/basic_string/operator_basic_string_view.html
在rust中,String到&str也可以進行隱式轉換:
- https://doc.rust-lang.org/std/string/struct.String.html#impl-Deref-for-String
- https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion
思考題:
① 在上面的30行代碼中,一共涉及到哪些trait?
Read、BufRead、Iterator、FromIterator、From、Deref、Termination、Drop
② 在上面的30行代碼中,一共有哪些迭代器iterator?
std::env::Args、std::io::Lines
從上面的這個小例子中,我們也能一窺Rust程序的特點:
- 代碼風格,下劃線小寫命名的形式;
- 傾向于使用trait,使用組合的方式來實現程序的功能;
- 迭代器iterator功能挺強大的;
- Rust學習曲線確實很陡峭,30行代碼竟然涉及這么多語法。
3. Rust中組織數據的3種方式
在Rust中,我們可以使用struct來組織數據。
struct Person {
first_name: String,
last_name: String,
age: i32,
}也可以使用tuple(Rust中的tuple和python中的tuple概念是一致的):
let person_info = ("Harry", "Potter", 18);
let first_name = person_info.0;
let (first_name, _, _) = person_info;Rust中的enum表示的可以是一個集合類型中的任意一種:
enum WebEvent {
// An enum variant without any data.
PageLoad,
// An enum variant with a string slice.
KeyPress(char),
// An enum variant with a struct.
Click { x: i32, y: i32 },
// An enum variant with an owned String.
Paste(String),
}
impl WebEvent {
fn describe(&self) {
matchself {
WebEvent::PageLoad => println!("Page loaded"),
WebEvent::KeyPress(c) => println!("Key pressed: {}", c),
WebEvent::Click { x, y } => println!("Clicked at: ({}, {})", x, y),
WebEvent::Paste(s) => println!("Pasted: {}", s),
}
}
}enum通常配合match在一起使用。
4. Rust中的Ownership
Rust中的ownership規則:
1. Each value in Rust has an owner.
2. There can only be one owner at a time.
3. When the owner goes out of scope, the value will be dropped.通過ownership的機制實現RAII,當變量離開作用域的時候,會被釋放或者drop掉。
Rust在默認的情況下,是move語義的,比如說:
let a = vec![1, 2, 3];
let b = a;
println!("{:?}", a);
println!("{:?}", b); // error[E0382]: borrow of moved value: `a`但是如果對應的類型實現了Copy這個trait的話,默認就會走copy的語義:
let a = 1;
let b = a;
println!("{:?}", a);
println!("{:?}", b);https://doc.rust-lang.org/std/marker/trait.Copy.html
Rust中為很多簡單類型都自動實現了Copy這個trait。
Copy和Clone的區別:
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}在上面Point的定義中,由于i32同時實現了Copy和Clone這兩個trait,因此他們組合在一起,Point也能實現這兩個Trait。
#[derive(Clone)]
struct PointList {
points: Vec<Point>,
}而PointList中,由于Vec只實現了Clone這兩個trait,因此PointList也只能實現Clone這個trait,不能實現Copy。
Copy和Clone的區別,就類似于淺拷貝和深拷貝的區別,上面Point的定義,兩個變量都是分配在棧上的,淺拷貝和深拷貝沒有區別,因此Point可以同時實現Copy和Clone這兩個trait;而下面PointList的定義中,Vec在棧上只記錄了元信息(pointer, capacity, length),Vec的元素是存放在堆上的,只能深拷貝,因此只實現了Clone這個trait。
5. Rust中的引用和借用
1. At any given time, you can have either one mutable reference or any number of immutable references.
2. References must always be valid.由于Rust中默認是move語義的,在有的場景下,我并不想轉移ownership,這種情況下,可以通過引用來借用。
引用分為兩種,一種是immutable引用,一種是mutable的引用。
通過immutable的引用,借用者不能修改;通過mutable的引用,借用者可以對這個變量做任何的修改,比如說賦值、swap等,唯一的一個限制就是要保證這個變量的完整性。
Rust的安全機制要求引用在任何時候都必須有有效;同時,限制mutable引用和immutable引用不能同時存在:你可以有多個immutable的引用;也可以有一個mutable的引用;但是不允許有多個mutable的引用,也不允許mutable的引用和immutable的引用同時存在。
思考:為什么對同一個變量的mutable引用和immutable引用不能同時存在?思考下面的例子:
fn main() {
let mut v = vec![1, 2, 3, 4];
let first = &v[0];
v.push(5); // error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
println!("first element: {}", first);
}6. trait
Rust中的trait類似于golang中的interface,Rust中通過trait定義共同的行為。比如說咱們定義Shape形狀這樣的trait,所有的形狀都有面積:
trait Shape {
fn area(&self) -> f64;
}圓和長方形都能實現Shape形狀這個Trait:
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}有了這些定義之后,就可以實現編譯時的類型約束:
fn print_area1<S: Shape>(shape: &S) {
println!("The area is: {}", shape.area());
}
fn print_area2(shape: &impl Shape) {
println!("The area is: {}", shape.area());
}上面這兩種實現的方式是等價的,impl只是一個語法糖:https://doc.rust-lang.org/stable/reference/types/impl-trait.html
也可以實現運行時的多態:
fn print_area3(shapes: &[&dyn Shape]) {
for shape in shapes {
println!("The area is: {}", shape.area());
}
}調用上面幾個方法的例子:
fn main() {
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle {
width: 10.0,
height: 4.0,
};
print_area1(&circle);
print_area2(&rectangle);
let shapes: Vec<&dyn Shape> = vec![&circle, &rectangle];
print_area3(&shapes);
}7. 迭代器
Rust中的迭代器由Iterator這個trait來表示,表示會產生一個序列的值:https://doc.rust-lang.org/std/iter/trait.Iterator.html
這個trait只有一個Required的方法next,當next返回None的時候,表示序列結束:
// Required method
fn next(&mut self) -> Option<Self::Item>;可以直接調用next方法來一個一個的獲取值,但是更多的場景下是使用標準庫中提供的adapter和consume方法。
rust中的迭代器有以下特點:
① laziness
https://doc.rust-lang.org/std/iter/index.html#laziness
② infinity
https://doc.rust-lang.org/std/iter/index.html#infinity
③ 高效,性能很高,和手寫for循環性能是一致的。
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Fibonacci {
Fibonacci { current: 0, next: 1 }
}
}
implIteratorfor Fibonacci {
type Item = u64;
fn next(&mutself) -> Option<Self::Item> {
let next_number = self.current + self.next;
self.current = self.next;
self.next = next_number;
Some(self.current)
}
}
fn main() {
let fib_iterator = Fibonacci::new();
println!("The first 10 Fibonacci numbers are:");
for number in fib_iterator.take(10) {
println!("{}", number);
}
let fib_vec: Vec<u64> = Fibonacci::new().take(15).collect();
println!("\nThe first 15 numbers collected into a vector:");
println!("{:?}", fib_vec);
}8. 閉包
Rust中的閉包能夠capture環境中的值,根據capture的方式不同,閉包也分別實現了不同的trait,如果是普通的borrow來capture的話,實現了Fn,如果是mut borrow來capture的話,實現了FnMut,如果是move consume了變量的話,實現FnOnce,看下面的例子:
fn main() {
let s = String::from("hello");
let f1 = || &s;
println!("{}", f1());
println!("{}", f1());
}如上面的代碼f1是一個閉包,capture了變量s的引用,編譯器自動幫這個閉包實現了Fn的trait,這個閉包可以調用多次。我們也可以看到rust標準庫中Fn這個trait的定義是 fn call(&self, args: Args) -> Self::Output;,傳遞的是self的引用,因此才可以調用多次。
fn main() {
let mut s = String::from("hello");
let mut f2 = || s += "world";
f2();
//println!("{}", s);
f2();
println!("{}", s);
}如上面的代碼,f2也是一個閉包,mut borrow了s,因此編譯器自動幫這個閉包實現了FnMut這個trait,注意,上面的代碼中,如果注釋掉中間的println!的話,會報error[E0502]: cannot borrow s as immutable because it is also borrowed as mutable錯誤,f2是變量s的一個mut引用,要滿足s引用的限制規則。FnMut在標準庫中是這樣定義的:fn call_mut(&mut self, args: Args) -> Self::Output;,可以看到第一個參數是&mut self。
fn main() {
let s = String::from("hello");
let f3 = || s;
println!("{}", f3());
//println!("{}", f3()); // error[E0382]: use of moved value: `f3`
}上面的代碼中f3成為了s的owner,實現了FnOnce這個trait,f3只能調用一次,第二次調用的話會報use of moved value的錯誤信息。FnOnce在標準庫中的定義:fn call_once(self, args: Args) -> Self::Output;,self是move的這種調用方式,因此只能調用一次。
同時,編譯器為實現了Fn的閉包,也同時實現了FnMut和FnOnce;實現了FnMut的閉包也同時實現了FnOnce。
- https://doc.rust-lang.org/std/ops/trait.Fn.html
- https://doc.rust-lang.org/std/ops/trait.FnMut.html
- https://doc.rust-lang.org/std/ops/trait.FnOnce.html
9. Sync & Send
Rust中的并發安全,是通過Sync和Send這兩個trait來體現的。Send表示的含義是,變量可以跨越線程的邊界進行傳遞;Sync表示的含義是,變量可以多線程同時訪問。
這里通過一個簡單的例子,演示下Sync & Send如何保證并發安全的:
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
fn test1() {
letmut a = vec![1, 2, 3];
let handler = std::thread::spawn(move || {
a.push(4);
});
handler.join().unwrap();
}
fn test2() {
letmut a = vec![1, 2, 3];
thread::scope(|s| {
s.spawn(|| {
println!("hello from the first scoped thread");
a.push(4);
});
});
a.push(5);
}
fn test3() {
let a1 = Arc::new(vec![1, 2, 3]);
let a2 = a1.clone();
let handler = thread::spawn(move || {
println!("a1 {:?}", a1);
});
println!("a2 {:?}", a2);
handler.join().unwrap();
}
fn test4() {
let a1 = Arc::new(Mutex::new(vec![1, 2, 3]));
let a2 = a1.clone();
let a3 = a1.clone();
let handler1 = thread::spawn(move || {
letmut lock_guard = a1.lock().unwrap();
lock_guard.push(4);
});
let handler2 = thread::spawn(move || {
letmut lock_guard = a2.lock().unwrap();
lock_guard.push(4);
});
handler1.join().unwrap();
handler2.join().unwrap();
println!("a3 {:?}", a3.lock().unwrap());
}
fn main() {
test1();
test2();
test3();
test4();
}10. async & await
Rust的異步編程,被稱為無棧協程,先看一個簡單的例子:
use anyhow::Result;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Joke {
joke: String,
}
#[tokio::main]
asyncfn main() -> Result<()> {
let res = reqwest::get("https://geek-jokes.sameerkumar.website/api?format=json").await?;
let joke = res.json::<Joke>().await?;
println!("{}", joke.joke);
Ok(())
}使用cargo expand命令,上面的代碼,main大概展開成下面的樣子:
fn main() -> Result<()> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(async {
let res = reqwest::get("https://geek-jokes.sameerkumar.website/api?format=json").await?;
let joke = res.json::<Joke>().await?;
println!("{}", joke.joke);
Ok(())
})
}從上面的代碼可以看到,代碼中首先構建了一個tokio的runtime,然后block_on在某個async塊上進行執行。
async/await把Rust程序分割成了兩個世界,在async/await的上下文里,不能調用阻塞的函數,不然會卡住異步運行時tokio的執行和調度。
為了弄清楚async/await到底干了啥,咱們首先看下上面代碼中的其中一行代碼:
let res = reqwest::get("https://geek-jokes.sameerkumar.website/api?format=json").await?;這行代碼可以拆成兩行:
let res_future = reqwest::get("https://geek-jokes.sameerkumar.website/api?format=json");
let res = res_future.await?;在vscode里面,把鼠標懸停在res_future上,可以看到vscode給出的類型注解是:
let res_future: impl Future<Output = Result<Response, Error>>可以看到res_future實現了Future這個trait,但是res_future具體的類型不知道,只知道他實現了Future這個trait。
async只是一個語法糖:
async fn test() {
println!("This is a test function.");
}
fn test2() -> impl Future<Output = ()> {
async {
println!("This is a test function.");
}
}async的本質,實際上是編譯器把reqwest::get("https://geek-jokes.sameerkumar.website/api?format=json")編譯成了一個狀態機,然后這個狀態機實現了Future這個trait,所以這里get返回的時候,實際上并沒有發出任何http請求,只是返回了一個狀態機,這個狀態機實現了Future這個trait,僅此而已。理論上來說,也可以手寫一個struct,實現同樣的狀態機,只是這個過程會特別的復雜,編譯器直接幫忙咱們做了:https://doc.rust-lang.org/stable/reference/items/functions.html#r-items.fn.async
一些手動實現Future的例子:
- https://docs.rs/futures-util/latest/futures_util/future/struct.Select.html#impl-Future-for-Select
- https://docs.rs/tokio/latest/tokio/time/struct.Timeout.html#impl-Future-for-Timeout
await的本質,實際上是“不停的”調用上面狀態機的poll方法,驅動狀態機不停的往前走,直到Ready為止。
rust異步編程的核心,就是Future這個trait:
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T),
Pending,
}上面Future這個trait的定義中,poll函數的第一個參數是個Pin<&mut Self>的類型,什么是Pin,為什么需要Pin呢?
還是要從上面async生成的狀態機說起,reqwest::get("https://geek-jokes.sameerkumar.website/api?format=json")這個函數會返回一個對象,這個對象實現了Future trait,這個對象是一個狀態機,內部維護這個請求的執行狀態,然后await的時候,會“不停”的poll,驅動狀態機往前走,這個狀態機內部會維護很多的狀態,比如說tcp socket收發的buffer,以及buffer已經使用的大小等。換句話說,這個狀態機是一個自引用的對象,狀態機內部有一些buffer,然后狀態機內部有一些指針指向這些buffer的某些位置等。狀態機是自引用的,就要求這個狀態機不能在內存中隨意的移動,如果移動的話,自引用指針的指向就錯了,指向了別的位置。Rust對這個問題的解法就是加一層Pin,對這個狀態機的所有的訪問,都是通過Pin這個智能指針來訪問的,Pin限制了這個狀態機不能移動和復制。
異步任務的取消:不.await了,不poll了,異步的請求也就取消了,比如說前面提到的 https://docs.rs/futures-util/latest/futures_util/future/struct.Select.html 當其中的一個已經Ready之后,另外一個就自動的取消了。
二、思維篇
每個語言都有自己的特點,比如說Golang推崇通過消息的方式共享內存,Python語言中的list comprehensions是一種強大且簡潔的創建列表的方法等,這里介紹下Rust語言的思維方式。
1. expression
在Rust中,推崇簡潔,表達式可以作為值進行賦值,或者作為返回值:
fn foo(x: usize) -> usize {
x
}表達式可以賦值:
let x = if 3 > 2 {
1
} else {
2
};前面的greplite的main函數,也可以寫成:
fn main() -> io::Result<()> {
let mut args = env::args().skip(1);
let (search_string, file_path) = match (args.next(), args.next(), args.next()) {
(Some(s), Some(f), None) => (s, f),
_ => {
eprintln!("Usage: greplite <search_string> <file_path>");
std::process::exit(1)
}
};
run(&search_string, &file_path)
}2. split
由于Rust中ownership及引用規則的限制,有些對象會有split的操作,比如說Vec,split成兩個,每個只修改Vec的一部分元素,這樣整體還是安全的。
再比如說socket,可以split成一個只收數據的socket,一個只發數據的socket,這樣可以實現在一個線程中只收數據,在一個線程中只發數據。
https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html#method.into_split
3. 無處不在的Option和Result
就像在golang中,if err != nil {}和if someData != nil {}無處不在一樣,在Rust中,Option和Result也是隨處可見;Option和Result都是enum,如果都用match來進行判斷的話,代碼的遞進深度會比較深,另外代碼也看起來會很冗余。
為此Rust為Option和Result提供了很多便利的操作。
① question mark operator
https://doc.rust-lang.org/reference/expressions/operator-expr.html#r-expr.try
② is_some()、is_none()、is_ok()、is_err()便捷函數
③ let else賦值操作
let a = Some(1);
let Some(b) = a else {
return;
};④ 和迭代器的互操作等
- https://doc.rust-lang.org/std/option/#method-overview
- https://doc.rust-lang.org/std/result/#method-overview
熟練掌握上面的方法,會讓代碼更簡潔。
4. match的不僅僅是enum
在Rust中,match通常用來作用在enum上,然后每個分支判斷enum的每個變體。
但是match不僅僅可以用在enum上,在其它的場景中,match也能發揮大作用。
另外,match是exhaustive的,需要寫出所有的可能的分支。
- https://github.com/yaozongyou/rust-24-hour-crash-course/blob/bdf5bc30a67fe6b5649a8fff7cc25e2e0d19a0e6/mini-redis/src/connection.rs#L112
- https://doc.rust-lang.org/rust-by-example/flow_control/match.html
5. 宏強大的超乎想象
宏的3種場景:
① 類似于println!
本質是編譯器一些規則的替換
https://doc.rust-lang.org/stable/reference/macros-by-example.html?highlight=hygiene#hygiene
② 類似于前面例子中的#[tokio::main]
本質是在編譯期,把對應注解的函數的Token Stream給到這個宏,然后這個宏,在編譯期生成新的代碼。
一個簡單這種宏的例子,可以參考下面的這篇文章:
樂學Rust:100行代碼實現簡易集成測試框架
③ 類似于Debug宏
https://doc.rust-lang.org/std/fmt/derive.Debug.html
本質是在編譯期,把對應注解的對應的struct的Token Stream給到這個宏,然后這個宏,在編譯期生成新的代碼。
6. 通過傳遞消息的方式共享內存
在Golang中,推崇通過消息的方式共享內存。同樣,在Rust中,也支持這種編程的模式。
考慮對象從一個存儲桶搬遷到另外的存儲桶這種場景:

一種操作流程的組織形式可能類似于上圖,左邊的routine,調用list objects的接口,或者從文件列表中獲取所有的對象列表,然后再把這些對象,放入到一個channel中;然后右邊的copy的routine,具體執行copy每個對象的動作。
list操作和具體的copy操作在流程上做了分離,代碼簡潔清晰。
上面這個流程有一個小問題就是,當遇到大對象的時候,大對象可能會成為長尾的瓶頸,因為每個對象都是單routine拷貝的。流程上可以優化如下:

如上圖,再加一層,真正的copy操作只在worker中進行處理,在copy這一層,把每個對象的copy任務,拆分成多個task,如果對象比較小的話,對應一個task,對象比較大的話,采用分塊上傳的方式,拆分成多個task。上圖中的第二個channel里面的消息就是每個task任務,同時每個task任務中會包含另外一個channel,這個worker通過這個channel告知copy的routine對應的task的完成情況。
通知這種流程的組織形式,解決了大對象長尾的問題。流程依然清晰簡潔。
在Rust中同樣可以實現上面的這種流程模式。
不過對比Rust和Golang中channel和select的體驗,由于Rust不是像golang那樣,在語言本身支持channel和select,因此體感上,Rust稍微差了一丟丟。
7. 和C++一致的內存模型
Rust采用和C++一致的內存模型,都是通過atomic原子操作來體現的,C++上的經驗可以直接遷移到Rust上。
比如說,考慮配置熱加載的場景,一種可能的實現是這樣的,一個atomic的pointer指向當前的配置,然后有一個線程從本地或者通過sdk周期性從外部取最新的配置,然后再atomic的更新pointer指向最新的配置。
這里有個問題是,原來的配置何時釋放的問題(safe memory reclamation),在C++中,通常使用hazard pointer來解決,在Rust中也類似,也有hazard pointer。
不過最新的這種問題的解決方式,建議使用《Concurrent Deferred Reference Counting with Constant-Time Overhead》這篇paper介紹的方法,使用更方便:https://github.com/cmuparlay/concurrent_deferred_rc
對應Rust的crate:https://github.com/aarc-rs/aarc
關于Rust內存模型的書籍推薦。
8. Interior Mutability Pattern
由于Rust的ownership以及引用規則的限制,在寫代碼的時候,要想好各種數據結構,是否會跨多線程訪問,如果跨多線程訪問的話,可能要使用interior mutability pattern,所有的struct的函數都是&self,而不是&mut self。
參考例子:https://github.com/yaozongyou/rust-24-hour-crash-course/blob/bdf5bc30a67fe6b5649a8fff7cc25e2e0d19a0e6/mini-redis/src/store.rs#L45
9. build.rs在編譯期執行各種操作
crate有個build.rs腳本,可以獲取代碼倉庫的git信息,編譯c/c++程序等:https://doc.rust-lang.org/cargo/reference/build-scripts.html
10. 迭代器真的很好用
在Rust中,適當的使用迭代器會讓代碼更簡潔。各種collection(Vec、HashMap、BTreeMap等)都能通過迭代器來遍歷,Result和Option等也都能和迭代器相互轉換等,迭代器也有特別多的adapter。
從C++轉到Rust的話,可以嘗試多使用下迭代器。
https://doc.rust-lang.org/std/iter/
之前greplite程序,可以改下成:
fn run(search_string: &str, file_path: &str) -> io::Result<()> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
reader.lines().try_for_each(|line| {
let line = line?;
if line.contains(search_string) {
println!("{}", line);
}
Ok::<(), _>(())
})
}或者:
fn run(search_string: &str, file_path: &str) -> io::Result<()> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
reader
.lines()
.collect::<io::Result<Vec<_>>>()?
.into_iter()
.filter(|line| line.contains(search_string))
.for_each(|line| println!("{}", line));
Ok(())
}11. 想定義個全局變量真不容易
關于全局變量,下面這篇文章總結的非常好:
https://www.sitepoint.com/rust-global-variables/

另外,上面這篇文章寫的比較久了,上圖中的lazy_static or once_cell,在當前最新的Rust的版本中,可以使用OnceLock 或者 LazyLock來替代,這樣就不需要依賴第三方的crate了。
三、實戰篇
使用Rust實現一個mini-redis:https://github.com/yaozongyou/rust-24-hour-crash-course/tree/bdf5bc30a67fe6b5649a8fff7cc25e2e0d19a0e6/mini-redis
Rust學習建議:
- The Book通讀一遍
- https://rustlings.rust-lang.org/
rustlings上面的練習全部走一遍。
- 不要嘗試寫鏈表、不要嘗試寫鏈表、不要嘗試寫鏈表。
參考資料:
- Using lightweight formal methods to validate a key-value storage node in Amazon S3
- https://rsproxy.cn/
- The Rust Programming Language
- This Week in Rust
- https://doc.rust-lang.org/nomicon/intro.html
- https://doc.rust-lang.org/stable/reference/introduction.html
- Rust Atomics and Locks
- https://rustlings.rust-lang.org/
- https://doc.rust-lang.org/rust-by-example/index.html
- https://github.com/yaozongyou/rust-24-hour-crash-course
























