rust笔记
Rust程序结构
cargo init 初始化一个文件夹为新的rust项目
cargo run编译运行
一个简单rust程序:
use std::io::stdin;fn main() {let mut msg = String::new();println!("Please enter message");std::io::stdin().read_line(&mut msg).unwrap();print!("Message is {}", msg);
}
猜数游戏
cargo add rand@0.8.5在项目中添加rand库,版本为0.8.5
cargo build编译代码,处理依赖
use std::{cmp::Ordering, io};
use rand::{self, Rng};fn main() {println!("Guess the number");//1..101 大于等于1小于101//1..=100 大于等于1小于等于100let secret_number = rand::thread_rng().gen_range(1..101);loop {println!("Please input your guess.");//mut 表示变量可变let mut guess = String::new();io::stdin().read_line(&mut guess)//Result这个枚举,有两个变体:// --- OK// --- Err//except用于处理Result(Result必须处理).expect("Failed to read line");println!("Your guessed: {}", guess);//rust中,一个作用域内可以使用重复的变量名 “遮蔽”let guess: u32 = match guess.trim().parse() {Ok(num) => num,Err(_) => continue,};match guess.cmp(&secret_number) {Ordering::Less => println!("small"),Ordering::Greater => println!("big"),Ordering::Equal => {println!("You Win");break;},}}
}
一些概念
Rust所有数据的类型在编译时已知,且对数据类型的赋值进行规则约束
在编译时进行类型检查, 更早检测出错误,性能更快,但代码灵活度不如在运行期间进行类型检查的语言
栈内存
非常快,lifo后进先出
堆内存
较为无序,可存储未知大小的数据
所有权是一组规则,用于管理Rust程序如何处理内存。
它在没有垃圾回收器的情况下,帮助确保程序的内存安全。
数据类型
Tuple元组
let tup = ("A", 1, 1.2);
长度固定,可含不同类型的元素
loop循环
可使用loop标签灵活选择跳出哪一层循环
fn main() {let mut count = 0;'counting_up: loop {println!("count = {count}");let mut remaining = 10;loop {println!("remaining = {remaining}");if remaining == 9 {break;}if count == 2 {break 'counting_up;}remaining -= 1;}count += 1;}println!("End count = {count}");
}
所有权
Rust的一个基础目标:确保你的程序永远不会有未定义行为
let a = [0; 1_000_000];//创建大小为1000_000_000的数组,元素全为0
let b = a;
会发生复制,占用过大空间
不想发生复制:使用指针
常见使用指针的方法就是将内存分配到堆上
let a = Box::new([0; 1_000_000]);//栈中的a没有直接存储数据,而是存储了堆中这个数组的指针
let b = a;
此时a,b两个指针都指向堆中的数组
Rust不允许手动管理内存
Stack Frame由Rust自动管理:
调用一个函数时,Rust为被调用的函数分配一个Stack Frame。调用结束时,Rust释放该Stack Frame。
let b = Box::new([0; 100]);
free(b);
assert!(b[0] == 0);
b对应的堆上的数组已经被清理,b[0]尝试访问无效的内存。
Rust会自动释放Box的堆内存。
如果一个变量拥有到一个Box,当Rust释放变量的frame时,Rust也会释放Box的堆内存。
let a = Box::new([0; 1_000_000]); / / a拥有这个box
let b = a; // box的所有权被交给了b
fn main() {let a_num = 4;make_and_drop();
}fn make_and_drop() {let a_box = Box::new(5);
}
a_box存储在栈中 make_and_drop 函数的Stack Frame内。当 make_and_drop 函数执行结束时,Rust 的所有权机制会自动释放堆上存储 5 的内存,避免内存泄漏。
移动堆数据原则:
如果变量x将堆数据的所有权移动给另一个变量y,那么在移动后,x不能再使用。
避免数据移动:
使用.clone()方法进行克隆
fn main() {let first = String::from("Ferris");let first_clone = first.clone();let full = add_suffix(first_clone);println!("{full}, originally {first}");
}fn add_suffix(mut name: String) -> String {name.push_str(" Jr.");name
}
first和first_clone通过clone,各自指向堆中存储的字符串"Ferris"。- 调用
add_suffix时,first_clone的所有权转移到函数内,被修改为"Ferris Jr."后返回给full,此时first_clone因所有权转移失效。 first仍保留对原堆中"Ferris"的引用,最终打印full("Ferris Jr.")和first("Ferris")。
引用和借用
use std::{cmp::Ordering, io};
use rand::{self, Rng};fn main() {let m1 = String::from("Hello");let m2 = String::from("World");greet(m1, m2);// 调用greet后,m1 m2已经被移动,就不能再使用了let s = format!("{} {}", m1, m2); // x
}
fn greet(g1: String, g2: String) {print!("{} {}", g1, g2);
}
想再使用: 给greet加返回值,但这样写比较麻烦
fn main() {let m1 = String::from("Hello");let m2 = String::from("World");let(m1_again, m2_again) = greet(m1, m2);let s = format!("{} {}", m1_again, m2_again);
}
fn greet(g1: String, g2: String) -> (String, String) {print!("{} {}", g1, g2);(g1, g2)
}
rust提供了一种解法:“引用” — 没有所有权的指针
fn main() {let m1 = String::from("Hello");let m2 = String::from("World");greet(&m1, &m2);let s = format!("{} {}", m1, m2);
}
fn greet(g1: &String, g2: &String) {print!("{} {}", g1, g2);
}
此时 g1 -> m1 -> “Hello”
greet调用后,g1和g2的Stack Frame消失
fn main() {let v: Vec<i32> = vec![1, 2, 3];let num: &i32 = &v[2];v.push(4);println!("Third element is {}", *num);
}
添加数据后,num失效了(未定义行为:指针指向的数据已被释放)
vec满了,想再添加新元素,会创建一个新的内存分配,有更大的容量,把原来的元素都copy进去,再释放堆中原来的vec。所以新vec的位置可能和原来vec的位置不同。
Box(有所有权的指针),不能起别名(只有box的所有者可以访问数据)。
将一个Box变量赋给另一个变量,移动了所有权。
引用(无所有权的指针),旨在临时创建别名。
Rust通过借用检查器确保引用的安全性
变量对其数据有三种权限:
-
读R
-
写W
-
拥有O:谁拥有,谁可以free,避免double free
默认情况,变量对其数据有R、O权限
如果变量是mut的,那么还具有W
引用可以临时移除这些权限


引用和借用(二)
不可变引用
// v 初始拥有 R(读)、W(写)、O(所有权) 权限
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];// 创建不可变引用:v 暂时失去 W 和 O 权限
// num 获得对 v[2] 的 R 权限,以及 num 变量自身的 O 权限
let num: &i32 = &v[2];// ❌ 错误:此时 v 没有 W 权限,因为 num 的不可变引用仍在使用中
v.push(4);// num 在这里被使用,所以不可变引用的生命周期持续到此处
// 如果删除这行,num 的上次使用是第2行(声明时),第3行就不会报错了
println!("{}", *num);
可变引用
&mut
因此 *num 同时具有R和W
在 Rust 中,解引用操作 (*) 的权限取决于引用本身的类型:
- 如果是不可变引用,解引用后只能读
- 如果是可变引用 ,解引用后可读写

可变引用可以临时降级为只读引用

此处num、num2都是对v[2]的引用,即为别名,所以num的写权限被去掉了

数据必须在其所有的引用存在的期间存活
流动权限F:在表达式使用输入引用或返回输出引用时需要
F权限在函数体内不会发生变化
如果一个引用被允许在特定表达式中使用(即流动),那么它就具有F权限
fn first_or(strings: &Vec<String>, default: &String) -> &String {if strings.len() > 0 {&strings[0]} else {default}
}
此时rust不知道返回的是strings还是default,就会报错
例子:
fn main() {let strings = vec![];let default = String::from("default");let s = first_or(&strings, &default);// 返回的s有可能是default,此处又释放default,报错drop(default);println!("{}", s);
}
另一种情况:
fn return_a_string() -> &String {let s = String::from("Hello world");let s_ref = &s;// 函数结束,s已经被释放,返回的是无效引用s_ref
}
小题目:
fn main() {let mut s = String::from("hello");let s2 = &s;let s3 = &mut s;s3.push_str("world");println("{s2}");
}
错误:在同一作用域内,不能同时存在可变引用和不可变引用
修复所有权常见错误
fn main() {let value = return_a_string();println!("{}", value);
}
fn return_a_string() -> &String {let s = String::from("hello world");&s
}
函数结束,s被释放,value就无法使用了,此时对s 的引用比s活得长,是不可以的
解法:
1 不返回引用,直接移交所有权
fn main() {let value = return_a_string();println!("{}", value);
}
fn return_a_string() -> String {let s = String::from("hello world");s
}
2 'statichello world字面值一直存在,直到程序停止
fn main() {let value = return_a_string();println!("{}", value);
}
fn return_a_string() -> &'static str {"Hello, world!"
}
3 使用Rc
fn main() {let value = return_a_string();println!("{}", value);
}
fn return_a_string() -> Rc<String> {let s = Rc::new(String::from("hello world"));Rc::clone(&s)
}
4 直接修改原数据(用在这里这里不太合适,因为函数名没有修改的意思)
fn main() {let mut s = String::from("hello");return_a_string(&mut s);println!("{}", s);
}
fn return_a_string(output: &mut String) {output.replace_range(.., "Hello world!");
}
缺少权限
解法
fn main() {let name = vec![String::from("ferris")];let first = &name[0];let full = stringify_name_with_title(&name);println!("{}", first);println!("{:?}", name);println!("{}", full);
}
fn stringify_name_with_title(name: &Vec<String>) -> String {// 克隆数据,造成内存浪费let mut name_clone = name.clone();name_clone.push(String::from("Esq."));let full = name_clone.join(" ");full
}
fn main() {let name = vec![String::from("ferris")];let first = &name[0];let full = stringify_name_with_title(&name);println!("{}", first);println!("{:?}", name);println!("{}", full);
}
fn stringify_name_with_title(name: &Vec<String>) -> String {// 省内存let mut full = name.join(" ");full.push_str("Esq.");full
}
关于从集合中移出元素的所有权
fn main() {let v: Vec<i32> = vec![0, 1, 2];let n_ref: &i32 = &v[0];// 此处发生的是复制let n: i32 = *n_ref;let v: Vec<String> = vec![String::from("hello world")];let s_ref: &String = &v[0];// 此处发生的是所有权移动let s: String = *s_ref;
}
// 如果一个值不拥有堆数据,那么它可以在不移动的情况下被复制:
// 一个 i32 不拥有堆数据,因此可以在不移动的情况下被复制。
// 一个 String 拥有堆数据,因此不能在不移动的情况下被复制。
// 一个 &String 不拥有堆数据,因此可以在不移动的情况下被复制。
解决:通过克隆得到值
let s: String = v[0].clone()
fn main() {let mut name = (String::from("Ferris"),String::from("Rustacean"));// 这一行执行完,name这个Tuple不具备写权限,但这个Tuple中的其他元素仍然具备写权限let first = &name.0;name.1.push_str(", Esq.");println!("{first} {}", name.1);
}
fn main() {let mut name = (String::from("Ferris"),String::from("Rustacean"));let first = get_first(&name);// 此处不可修改了name.1.push_str(", Esq.");println!("{first} {}", name.1);
}
fn get_first(name: &(String, String)) -> &String {&name.0
}
从get_first函数可以看出,返回结果是借用了参数中的某个String,但是是借用传入的name的哪个元素,是不可知的,所以rust会判定name.0和name.1都被不可变借用了
fn main() {let mut a = [0, 1, 2];let x = &mut a[1];// a失去所有权限*x += 1;// 如果放开这两行代码,就会报错// let y = &a[2];// *x += *y;println!("{a:?}");
}
元组的不同字段可以有独立的借用状态,而数组的借用会保守地影响整个数组
Silce
fn main() {let mut s = String::from("hello world");let word = first_word(&s);// 这里s被清除,再想使用first_word返回的索引找第一个单词,就不行了s.clear();
}
// 隐患:传入的参数String以后可能改,也可能删,此时你再用返回的索引,就有可能指向修改过或已释放的String
fn first_word(s :&String) -> usize {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item==b' ' {return i;}}s.len()
}
SIice是特殊的引用类型,属于fat指针,带有元数据(长度)
fn main() {let s = String::from("hello");let slice = &s[0..2];let len = s.len();// 效果相同let slice = &s[3..len];let slice = &s[3..];// 效果相同let slice = &s[..];let slice = &s[0..len];
}
用String切片重构刚才的例子:
fn main() {let mut s = String::from("hello world");let word = first_word(&s);// 此时,clear会报错s.clear();println!("{}", word);
}
fn first_word(s :&String) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item==b' ' {return &s[..i];}}&s[..]
}
函数参数需要引用字符串的话,通常写成&str
.... let mut s = String::from("hello");let mut lit = "Hello Rust"; // let word = first_word(&s);// let word = first_word(&lit);
}
fn first_word(s :&str) -> &str {
....
这样既可以传String的引用,也可以传字面值的引用
&str是不可变引用
对数组也可以进行切片
fn main() {let a = [11, 22, 33, 44, 55];let slice = &a[1..3];assert_eq!(slice, &[22, 33]);
}
结构体
struct User {active: bool,username: String,email: String,sign_in_count:u64,
}fn build_user(email: String, username: String) -> User {let mut user1 = User {// 简便写法 原写法:email: email,email,username,active: true,sign_in_count: 1,};let user2 = User {email: String::from("another@example.com"),// 简便写法:其他字段与user1相同..user1};user2
}
Tuple Struct
字段没有名字
fn main() {struct Color(i32, i32, i32);struct Point(i32, i32, i32);let black = Color(0, 0, 0);let origin = Point(0, 0, 0);
}
black和origin字段一样,但确是不同的类型
Unit-Like Struct
无字段的结构体
struct AlwaysEqual;fn main() {let subject = AlwaysEqual;
}
借用结构体的字段

// 实现debug这个Trait,这样才可以通过:?的方式打印结构体
#[derive(Debug)]
struct Rectangele {width: u32,height: u32,
}impl Rectangele {fn area(&self) -> u32 {self.width * self.height}
}fn main() {let rect1 = Rectangele {width: 30,height: 50,};println!("{:?}", rect1);
}
#[derive(Debug)]
struct Rectangele {width: u32,height: u32,
}impl Rectangele {fn area(&self) -> u32 {self.width * self.height}fn can_hold(&self, other: &Rectangele) -> bool {self.width > other.width && self.height > other.height}// 可变的Rectangle实例才可以用这个方法fn set_width(&mut self, width: u32) {self.width = width;}// 这里参数从&self改成了self,方法调用返回的Rectangle会获得所有权fn max(self, other: Self) -> Self {if self.area() > other.area() {self} else {other}}// 关联函数(第一个参数不是self) 静态方法fn square(size: u32) -> Rectangele {Self {width: size,height: size,}}
}fn main() {let rect1 = Rectangele {width: 30,height: 50,};let rect2 = Rectangele::square(3);println!("{:?}", rect1);
}
枚举与模式匹配
定义了一组可能的值
enum IpAddrKind {V4,V6
}
fn main() {// four和six的类型相同let four = IpAddrKind::V4;let six = IpAddrKind::V6;
}
enum IpAddrKind {// 可以持有数据V4(u8, u8, u8, u8),V6(String),
}
fn main() {let four = IpAddrKind::V4(127, 0, 0, 1);let six = IpAddrKind::V6(String::from("::1"));
}
// 如果用struct来写,需要写四个
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),
}
// 为枚举实现方法
impl Message {fn call(&self) {// ..}
}
常用枚举:Option
表示某个值可能存在或不存在
enum Option<T> {None,Some(T),
}
// Option<i32>let some_number = Some(5);// Option<String>let some_string = Some("a string");// 没有值,rust无法推断,所以必须指定类型// Option<i32>let absent_number: Option<i32> = None;
fn main() {let x : i8 = 5;let y : Option<i8> = Some(6);// Option可能为空,这里无法相加// 必须处理可能为空的情况,避免出现x + nulllet sum = x + y;
}
match
#[derive(Debug)]
enum UsState {Alabama,Alaska,// --snip--
}
enum Coin {Penny,Nickel,Dime,Quarter(UsState),
}fn value_in_cents(coin: Coin) -> u8 {match coin {Coin::Penny => {println!("Lucky penny!");1},Coin::Nickel => 5,Coin::Dime => 10,Coin::Quarter(state) => {println!("State quarter from {:?}!", state);25},}
}
fn plus_one(x: Option<i32>) -> Option<i32> {match x {None => None,Some(i) => Some(i + 1),}}let five = Some(5);let six = plus_one(five);let none = plus_one(None);
match 表达式默认会获取被匹配值的所有权
opt 的所有权被移动到 match 中,导致在 match 之后无法再使用 opt
不想移动数据:match &opt

项目代码组织
crate是组织和共享代码的基本构件块
- binary crate:可执行的,需要有main函数
- library crate:没有main函数,无法执行。定义一些功能,可共享使用。
create root是编译crate的入口点
- binary crate:src/main.rs
- library crate:src/lib.rs
可见性
private vs public
- 所有的东西 (functions, methods, structs, enums, modules, and constants) 默认对父模块是 private(私有的)
- 父模块中的项不能使用子模块中的私有项
- 但子模块中的项可以使用其祖先模块中的项
- 使用 pub 关键字让其变为 public
- 相对路径可使用 super、self 关键字
Struct:需为 struct 本身和各字段单独设置 pub
Enum:只要 enum 本身是 pub 的,那么所有变体都是 pub 的
Vector
// 此处需标明类型let v: Vec<i32> = Vec::new();let v = vec![1, 2, 3];// 后面push进了i32,可以推断出vec中的类型,就不需要标明了let mut v = Vec::new();v.push(5);v.push(6);v.push(7);
let v = vec![1, 2, 3, 4, 5];// 引用vec中的元素let third: &i32 = &v[2];println!("The third element is {}", third);// 使用get方法let third: Option<&i32> = v.get(2);match third {Some(third) => println!("The third element is {}", third),None => println!("There is no third element."),}
fn main() {let v = vec![1, 2, 3, 4, 5];for n_ref in &v {let n_plus_one: i32 = *n_ref + 1;println!("{}", n_plus_one)}// 修改vec中的值let mut v = vec![100, 32, 57];for n_ref in &mut v {*n_ref += 50;println!("{}", *n_ref)}
}
enum SpreadsheetCell {Int(i32),Float(f64),Text(String),
}
fn main() {// 让vector存储不同类型的元素let row = vec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from("blue")),SpreadsheetCell::Float(10.12),];
}
String
字节的集合,外加一些方法
Rust核心语言:str(&str)
str是一种字符串的切片,&str是我们经常使用的借用形式
可变可增长、拥有所有权、UTF-8编码的字符串类型
创建:
fn main() {let mut s = String::new();// 这里data是&strlet data = "initial contents";// 实现了ToString trait的类型有toString方法let s = data.to_string();let s = "initial contents".to_string();let s = String::from("initial contents");
}
附加
fn main() {let mut s = String::from("foo");let s2 = "bar";// push_str接收的参数是&str,所以参数用完之后还可以继续使用s.push_str(s2);println!("s2 is {}", s2);let mut s = String::from("lo");s.push('l);println!("s is {}", s);
}
连接
fn main() {let s1 = String::from("hello");let s2 = String::from("world");// +: 使用了add方法 fn add(self, s: &str) -> Stringlet s3 = s1 + &s2;
}
fn main() {let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");// 这样连接有些笨重// let s = s1 + "-" + &s2 + "-" + &s3;// 改为使用宏let s = format!("{s1}-{s2}-{s3}");
}
使用a + b和a.push_str(b)来连接两个字符串有什么区别?
+操作符会消耗a的所有权,而push_str不会
索引访问
rust不允许通过索引访问String的元素
原因:
- Rust 的
String使用 UTF-8 编码,字符可能占用 1-4 个字节 - 如果允许
s[i],用户会期望这是 O(1) 操作。但在 UTF-8 中:要找到第 i 个字符,必须从头开始遍历
遍历元素
fn main() {let s = String::from("tic");// 获得字符for c in s.chars() {println!("{}", c);}// 获得原始字节for b in s.bytes() {println!("{}", b);}
}
HashMap
fn main() {let mut scores = HashMap::new();scores.insert(String::from("Blue"), 10);scores.insert(String::from("Yellow"), 50);let vec = vec![("key1", "value1"), ("key2", "value2")];let map: HashMap<&str, &str> = vec.into_iter().collect();let team_name = String::from("Blue");// get方法返回Option<&V>// copied返回Option<V>// unwrap:Optin是Some,返回值,否则paniclet score = scores.get(&team_name).copied().unwrap();// 遍历for (key, value) in &scores {println!("{}: {}", key, value);}
}
HashMap与所有权
对于实现Copy trait的值,例如i32,直接复制到map里
对于“具有所有权”的类型的值,例如String,“移动”到map里,map会是这些值的所有者
fn main() {let field_name = String::from("Favorite color");let field_value = String::from("Blue");let mut map = HashMap::new();// 发生所有权移动, field_name 和 field_value 不可再被使用map.insert(field_name, field_value);
}
更新
fn main() {// key存在:替换/保留/新旧合并let mut scores = HashMap::new();// 替换scores.insert(String::from("Blue"), 10);scores.insert(String::from("Blue"), 25);// 存在则保留原来的值,不存在则新添加键值对scores.entry(String::from("Yellow")).or_insert(50);scores.entry(String::from("Blue")).or_insert(50);// 新旧合并let text = "hello world wonderful world";let mut map = HashMap::new();for word in text.split_whitespace() {let count = map.entry(word).or_insert(0);*count += 1;}println!("{map:#?}");
}
结果:
{"world": 2,"wonderful": 1,"hello": 1,
}
Hashing函数
默认使用SipHash(能抵御涉及hashtable的dos攻击),相对安全,并不是最快的
可通过指定hasher(实现BuildHasher这个Trait)来切换Hashing函数
错误处理
Rust没有“异常”
只分可恢复和不可恢复(Bug)的错误
不可恢复的错误 panic
导致panic的方式:
- 代码中的某些行为
- 显示调用panic!()宏
默认情况:panic后,会打印失败信息,对stack进行unwind,清理stack
panic后的响应:
-
展开stack并清理数据
-
立即终止(abort):需在Cargo.toml中添加:
[profile.release] panic = "abort"
Backtrace: 到达某个点之前所调用的所有函数的列表
设置环境变量,能更详细显示backtrace:
SET RUST_BACKTRACE = full
必须是Debug模式
使用Result处理可恢复的错误
enum Result<T, E> {Ok(T),Err(E),
}
fn main() {// 成功:返回Ok<T>, T:std::fs::File// 失败:返回Err<E>, E:std::io::Errorlet greeting_file_result = File::open("hello.txt");let greeting_file = match greeting_file_result {Ok(file) => file,Err(error) => match error.kind() {ErrorKind::NotFound => match File::create("hello.txt") {Ok(fc) => fc,Err(e) => panic!("Problem creating the file: {:?}", e),},other_error => panic!("Problem opening the file: {:?}", other_error),},};
}
出现错误时,使程序产生恐慌(panic)的常用快捷方式
unwrap(),用于提取Option后Result类型内部的值,如果是None或Err,程序将panic并终止
expect(),与unwrap相似,允许您提供一个自定义的panic消息
fn main() {// 此时返回结果不再是Result,而是Filelet greeting_file_result = File::open("hello.txt").unwrap();// expect可以自定义错误信息let greeting_file_result = File::open("hello.txt").expect("hello.txt should be included in this project");
}
实际生产中,便于调试的expect使用更多
传播错误
将错误返回,由调用该函数的代码来决定如何处理错误
fn read_username_from_file() -> Result<String, io::Error> {let username_file_result = File::open("username.txt");let mut username_file = match username_file_result {Ok(file) => file,Err(error) => return Err(error),};let mut username = String::new();// 这里的match没有分号,是一个表达式,它的结果就是函数返回值match username_file.read_to_string(&mut username) {Ok(_) => Ok(username),Err(error) => Err(error),}
}
?运算符
使用?运算符时
- 操作成功:解包Ok并继续执行下一行代码
- 操作失败:立即返回Err,并将错误传播给调用者
使用?运算符可以避免大量的match或if let语句,使代码更简洁
fn read_username_from_file() -> Result<String, io::Error> {let mut username_file = File::open("username.txt")?;let mut username = String::new();username_file.read_to_string(&mut username)?;Ok(username)
}
优化:
fn read_username_from_file() -> Result<String, io::Error> {let mut username = String::new();File::open("hello.txt")?.read_to_string(&mut username)?;Ok(username)
}
再优化:
fn read_username_from_file() -> Result<String, io::Error> {fs::read_to_string("hello.txt")
}
通过?处理错误,会通过调用from函数,进行类型转换
#[derive(Debug)]
pub enum MyError {IO(io::Error),ParseInt(ParseIntError),Other(String),
}
// 实现from trait,以便io类型的错误可以通过问号转化为MyError
impl From<io::Error> for MyError {fn from(value: Error) -> Self {MyError::IO(value)}
}
// 实现from trait,以便parseInt类型的错误可以通过问号转化为MyError
impl From<ParseIntError> for MyError {fn from(value: ParseIntError) -> Self {MyError::ParseInt(value)}
}
// 返回的Result中的泛型是MyError,发生错误会返回MyError
// 由于为标准库中的io::Error和ParseIntError实现了From trait,所以可以通过问号转化为MyError
fn read_username_from_file() -> Result<String, MyError> {let mut name = String::new();let file = File::open("some.txt")?.read_to_string(&mut name)?;let num: i32 = "55".parse()?;Ok(name)
}
什么时候可以使用?
函数的返回类型与?所作用的值的类型兼容。
-
函数返回 Result<T, E> ---->?可作用在 Result<T, E> 上
-
函数返回 Option ---->?可作用在 Option 上
?可用于返回类型为 Result、Option 或者实现了 FomResidual 的类型的函数内
不兼容的例子:
fn main() {// 错误let greeting_file = File::open("hello.txt")?;
}
但是,main函数也可以返回Result<T, E>
main函数可返回任何实现了std::process::Termination这个Trait的类型
fn main() -> Result<(), Box<dyn Error>>{let greeting_file = File::open("hello.txt")?;Ok(())
}
错误处理的基本原则 Panic or not?
何时使用 panic!? 不可恢复的错误场景
-
程序进入不可预期的 bad state
-
安全问题 或 代码无法继续执行
-
违反函数契约或关键假设
何时使用 Result? 可能恢复的错误场景
-
提供恢复选项
-
预期可能发生的错误
-
希望调用者决定如何处理错误
推荐使用 panic! 的情况
-
原型代码和示例
-
测试代码
-
安全性关键的输入验证
-
调用外部不可控代码时的异常状态
推荐使用 Result 的情况
-
可预期的错误
-
HTTP请求失败
-
解析错误
-
用户输入验证
具体请阅读 Rust Book:Error Handling - To panic! or not to panic!
泛型
Rust中用于消除重复的工具之一
fn largest<T>(list: &[T]) -> &t {}
结构体中使用
struct Point<T> {x: T,y: T,
}
可使用多个泛型参数
struct Point<T, U> {x: T,y: U,
}
泛型参数过多,可能意味着需要重构
Rust中使用泛型类型不会比使用具体类型让程序运行得更慢。
Rust通过单态化在编译时实现这种效率
Trait
一个Trait定义了特定类型所具有的功能
可以使用Trait以一种抽象的方式来定义共享的行为
可使用Trait Bounds来指定哪些类型才是我们想要的泛型类型(实现了某些特定行为的类型)
pub struct NewsArticle {pub headline: String,pub location: String,pub author: String,pub content: String,
}pub struct Tweet {pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}pub trait Summary {// 不需要写方法体// 写方法体的话,就是默认实现fn summarize(&self) -> String;
}impl Summary for NewsArticle {fn summarize(&self) -> String {format!("@{}", self.author)}
}impl Summary for Tweet {fn summarize(&self) -> String {format!("@{}", self.username)}
}
Trait的实现规则
只要 trait 或 类型其中之一属于当前 crate,就可以实现该 trait
合法示例:
- 在本地类型 Tweet 上实现标准库的 Display (trait) ✅
- 在标准库的 Vec 上实现本地的 Summary (trait) ✅
非法示例:
- 不能在 Vec 上实现 Display (trait) ❌ (因为两者都来自标准库,都不属于本地)
一致性和孤儿规则:
孤儿规则要求 trait 或类型至少有一个是本地(定义在当前crate中),防止冲突实现。
避免多个crate为同一类型实现相同trait导致的歧义,保证代码稳定性。
Trait作为参数
要求参数item必须实现了Summary这个trait
pub fn notify(item: &impl Summary) {println!("Breaking news! {}", item.summarize());
}
其实这是一种简便写法
Trait Bound
impl Trait语法适用于简单的情况,但它实际上是更长形式的语法糖,称为Trait Bound
pub fn notify<T: Summary>(item: &T) {// ...
}
trait bound适用于复杂情况
对比:
pub fn notify(item1: &impl Summary, item2: &impl Summary)pub fn notify(<T: Summary>(item1: &T, item2: &T))
使用+来指定多个bound
pub fn notify(item: &(impl Summary + Display))pub fn notify<T: Summary + Display(item: &T)>
使用where让Trait Bound更清晰
使用过多Trait Bound会使函数签名难以阅读:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
因此Rust提供了where子句来简化Trait Bound的指定
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,U: Clone + Debug {// ...
}
返回实现了某个Trait的类型
fn returns_summarizable() -> impl Summary {Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,}
}
impl Trait只能在返回单一类型的时候使用(函数不可以“可能返回两种不同的类型”),必须且只能返回一种具体的类型
使用Trait Bound来有条件地实现方法
struct Pair<T> {x: T,y: T,
}impl<T> Pair<T> {fn new(x: T, y: T) -> Self<T> {Self { x, y }}
}
// 实现了这两个trait的T,才有这个方法
impl <T: Display + PartialOrd> Pair<T> {fn cmp_display(&self) {if self.x >= self.y {println!("The largest member is x = {}", self.x);} else {println!("The largest member is y = {}", self.y);}}
}
blanket实现:可以为实现了蘑菇额trait的类型有条件地实现另一个trait
// 为任何实现了Display trait的类型实现ToString trait
impl<T: Display> ToString for T {// --snip--
}let s = 3. to_string();
生命周期
确保引用在所需的时间内有效
每个引用都有生命周期
大多数情况,生命周期都是隐式的,且可被推断出来
当引用的生命周期可能以几种不同的方式相关联时,就必须标注生命周期了(使用泛型生命周期参数)
主要目的:防止悬垂引用(程序引用到已经被释放的数据) :
fn main() {let r;{let x = 5;r = &x;// 到这里,x的内存已经被释放}println!("r: {}", r);
}
// 错误信息:borrowed value does not live long enough
借用检查器Borrow Checker
确保数据存活时间长于其引用
通过比较作用域,以确定所有的借用是否有效
函数中的泛型生命周期
错误:
fn longest(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y}
}
不知道返回类型与x关联还是与y关联
修复:添加泛型生命周期参数
生命周期注解不会改变引用存活时间,而是描述多个引用之间的生命周期关系
只要x、y这两个引用都有效,那么返回的引用就有效:
// 返回值的生命周期与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 string2 = String::from("xyz");let result = longest(string1.as_str(), string2.as_str());// &str 类型自动解引用为 str。当你有一个 &T 而函数需要 T 时,Rust 会自动解引用。println!("The longest string is {}", result);}
}
错误代码:
// 返回类型为 &'a str,意味着返回的引用必须具有生命周期 'a
// 当返回 y 时(生命周期 'b),编译器无法保证 'b 至少与 'a 一样长
fn shortest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {if x.len() < y.len() {x} else {y}
}
从函数中返回一个引用时,返回类型的生命周期参数需要与其中一个参数的生命周期参数匹配
如果函数返回的引用不指向某个参数,它必须指向在该函数中创建的一个值,但这样就会导致悬垂引用
Struct定义中的生命周期注解
// 结构体实例的生命周期不能超出字段part持有的那个引用的生命周期
struct ImportantExcerpt<'a> {part: &'a str,
}
生命周期省略规则
无需程序员遵守
是一组编译器需要考虑的特殊情况
满足这些情况,就不需要显示的写出生命周期了
如果rust应用这套规则后,仍存在歧义,编译器就会报错
三条规则(在没有显示声明生命周期注解时):
- 编译器为每个输入类型中的每个生命周期分配不同的生命周期参数
fn foo(x: &i32) ------ fn foo<'a>(x: &'a i32)
fn foo(x: &i32, y: &i32) ------ foo<'a, 'b>(x: &'a i32, y: &'b i32)
fn foo(x: &ImportantExcerpt) ------ fn foo<'a, 'b>(x: &'a ImportantExcerpt<'b>)
- 如果只有一个输入生命周期参数,该生命周期将被分配给所有的输出生命周期参数
fn foo<'a>(x: &'a i32) -> &'a i32、
- 如果有多个输入生命周期参数,但其中一个是&self或&mut self,那么self的生命周期会被分配给所有的输出生命周期参数
静态生命周期
'static
表示受影响的引用可以在整个程序的持续时间内存活
所有的字符串字面量都具有'static生命周期
泛型类型参数、Trait Bound和生命周期一起使用的例子
fn longest_with_an_announcement<'a, T> (x: &'a str,y: &'a str,ann: T
) -> &'a str
whereT: Display,
{println!("Announcement! {}", ann);if x.len() > y.len() {x} else {y}
}
习题
下面的代码会推断出什么生命周期?
struct Foo<'a> {bar: &'a i32,
}
fn baz(foo: &Foo) -> &i32 {/* ... */}
结果
fn baz<'a>(f: Foo<'a>) -> &'a i32
编写自动化测试
Tests(测试)就是带有#[test]的函数
- 设置所需的数据或状态
- 运行需要被测试的代码
- 断言其结果是你想要的
pub fn add(left: u64, right: u64) -> u64 {left + right
}#[cfg(test)]
mod tests {use super::*;#[test]fn it_works() {let result = add(2, 2);assert_eq!(result, 4);}
}
assert_eq!断言两个值相等
assert_ne!断言两个值不相等
#[should_panic]发生panic测试才会通过
#[should_panic(expected = "aaa")]发生的panic必须包含aaa
执行与组织测试
测试命令的参数
先列出carge test的参数
然后跟着–
再列出Test binary(测试二进制文件)的参数
测试默认使用线程并行运行
必须确保:
- 各个测试不互相依赖
- 不依赖于共享的状态(包括环境:目录、环境变量等)
cargo test -- --show-output可以看到输出
cargo test functionName指定跑某个测试
#[ignore]默认不跑的测试
单元测试(Unit tests):小而集中的测试,每次只测试一个模块,并且可以测试私有接口
用于隔离测试代码单元,快速定位问题 测试代码通常与被测代码放在同一文件中,并放在#[cfg(test)]注解的 tests 模块中
#[cfg(test)]确保测试代码仅在运行cargo test时编译和运行,而不会影响普通的构建(cargo build)流程。 集成测试在不同目录中,不需要#[cfg(test)] 单元测试与代码在同一文件中,需要使用此注解避免它们被编译进最终结果。
集成测试(Integration tests):完全外部的测试,它们以其他外部代码的方式使用你的代码,仅使用公共接口,并且可能在一个测试中覆盖多个模块
集成测试独立于库 只能调用公共 API 用于验证库各部分的协作性 测试覆盖很重要 需在项目中创建 tests 目录来编写集成测试
闭包
可以存储在变量中或作为参数传递给其他函数的匿名函数
闭包通常不需要像fn函数那样标注参数或返回值的类型
不会在暴露给用户的接口中使用
通常很短,只在有限的上下文使用,以便编译器可推断其参数和返回值的类型
可以添加类型注释
对于闭包定义,编译器将会为每个参数及其返回值推断出一个具体类型
fn main() {let example_closure = |x| x;let s = example_closure(String::from("hello"));// 这行代码会报错 已经传过string了,不可以再传i32let n = example_closure(5);
}
let f = |_| ();:厕所闭包
let f = |_| ();
let s = String::from("Hello");
f(s);
// f会导致s立即被丢弃
捕获引用或移动所有权
三种方式:
- 不可变引用
fn main() {let list = vec![1, 2, 3, 4, 5];// println! 宏会自动对传入的变量进行不可变引用println!("Before defining closure: {list:?}");let only_borrows = || println!("From closure: {list:?}");println!("Before calling closure: {list:?}");only_borrows();println!("After calling closure: {list:?}");
}
- 可变引用
fn main() {let mut list = vec![1, 2, 3, 4, 5];println!("Before defining closure: {list:?}");let mut borrows_mutably = || list.push(6);// 这里不可以打印list了 因为闭包已经对list进行了可变引用 可变引用只能有一个borrows_mutably();println!("After calling closure: {list:?}");
}
- 取得所有权
fn main() {let mut list = vec![1, 2, 3, 4, 5];// println! 宏会自动对传入的变量进行不可变引用println!("Before defining closure: {list:?}");// move关键字thread::spawn(move || println!("From thread: {list:?}")).join().unwrap();
}
Fn Traits
闭包体可以对捕获的值进行的操作:
- 将捕获的值移出闭包
- 修改捕获的值
- 既不移动也不修改
- 完全不从环境中捕获值
三种Fn相关的Trait
FnOnce -> FnMut -> Fn
FnOnce
适用于只能被调用一次的闭包,所有闭包都至少实现了FnOnce,因为所有闭包都可以被调用 如果一个闭包将捕获的值移出其主体(例如,通过将值返回或传递给其他函数),那么它只能实现FnOnce,因为一旦值被移出,闭包就不能再次调用。
FnMut
适用于不会移出值,但可能会修改捕获的值的闭包,这类闭包可以对多次调用
Fn
Fn 适用于既不移出值,也不修改捕获的值的闭包
也适用于完全不捕获环境中值的闭包 这类闭包可以被多次调用且不会修改其环境
特别适用于需要并发调用闭包的场景
Option的unwrap_or_else()
如果Option是Some,闭包f就不会执行,如果是null才会执行
impl<T> Option<T> {pub fn unwrap_or_else<F>(self, f: F) -> TwhereF: FnOnce() -> T{match self {Some(x) => x,None => f(),}}
}
Slices上的sort_by_key()
#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}fn main() {let mut list = [Rectangle { width: 10, height: 1 },Rectangle { width: 3, height: 5 },Rectangle { width: 7, height: 12 },];list.sort_by_key(|r| r.width);println!("{list:#?}");
}
闭包必须命名捕获的生命周期
当设计需要接收或返回闭包的函数的时候,必须想一下被闭包捕获的数据的生命周期。
fn main() { let s_own = String::from("hello");let cloner = make_a_cloner(&s_own);drop(s_own);cloner();
}
fn make_a_cloner(s_ref: &str) -> impl Fn() -> String {move || s_ref.to_string()
}
当调用 drop后,s_own 被释放,但闭包中仍然持有指向它的引用 s_ref,产生悬垂引用
解法,标注一下生命周期:
fn make_a_cloner<'a>(s_ref: &'a str) -> impl Fn() -> String + 'a {move || s_ref.to_string()
}
告诉rust,从make_a_cloner返回的闭包的生命周期不能比s_ref更长
也可以这样简写:
fn make_a_cloner(s_ref: &str) -> impl Fn() -> String + '_ {move || s_ref.to_string()
}
告诉rust,函数返回的闭包依赖于某个生命周期,而这个函数又只有一个引用,那肯定就是他
习题
1
fn main() {let mut s = String::from("hello");let mut add_suffix = || s.push_str(" world!");println!("{}", s);add_suffix();
}
定义闭包时,它隐式地捕获了 s 的可变引用(因为闭包内部修改了 s)。
在 println!("{s}") 处,需要不可变借用 s,但此时可变借用仍然有效,因此导致编译错误。
2
fn main() {let mut s = String::from("hello");let mut add_suffix = |s: &mut String| s.push_str(" world!");println!("{}", s);add_suffix(&mut s);
}
1中,闭包隐式捕获了环境变量s的可变引用
2中,闭包定义时没有借用任何东西,只有在调用 add_suffix(&mut s) 时才发生借用,所以2可以通过编译
迭代器
- 允许你依次对一系列项目执行某些操作
- 惰性的:除非调用某些方法消耗迭代器,否则他们不会立即生效
fn main() {let v1 = vec![1, 2, 3];let v1_iter = v1.iter();// 使用迭代器for val in v1_iter {println!("{}", val);}
}
Iterator Trait 和 next方法
所有的迭代器都实现了std的Iterator Trait
pub trait Iterator {type Item;// 实现Iterator trait的时候,必须同时定义一个Item类型,用于next方法的返回类型fn next(&mut self) -> Option<Self::Item>;
}
next()方法:每次返回iterator里的一个item,包裹在Some中,迭代结束时,返回None
#[test]
fn iterator_demonstration() {let v1 = vec![1, 2, 3];// 每次调用next,都会“吃掉”迭代器中的一个item,所以迭代器得是mut的// 上面for循环使用迭代器不需要mut,是因为for循环实际上会获取迭代器的所有权,在内部将其设为可变let mut v1_iter = v1.iter();assert_eq!(v1_iter.next(), Some(&1));assert_eq!(v1_iter.next(), Some(&2));assert_eq!(v1_iter.next(), Some(&3));assert_eq!(v1_iter.next(), None);
}
-
iter() — -> 不可变引用的迭代器
-
into_iter() — -> 能获得 v1 的所有权,并返回具有所有权的值
-
iter_mut() — -> 遍历可变的引用
消耗迭代器的方法
Iterator trait 在标准库中有许多具有默认实现的方法,其中一些方法在其定义中调用 next 方法,调用 next 方法的这些方法被称为"消耗性适配器"(consuming adaptors), 一个典型的例子是 sum 方法
fn main() -> Result<(), std::io::Error> {let v1 = vec![1, 2, 3];let v1_iter = v1.iter();// 调用sum后,v1_iter这个迭代器被消耗,无法再使用let total: i32 = v1_iter.sum();println!("{}", total);Ok(())
}
生成其他迭代器的方法
迭代器适配器(Iterator adaptors)是定义在 Iterator trait 上的方法,不会消耗原始迭代器,通过修改原始迭代器的某些方面来产生新的迭代器,例如map方法
fn main() -> Result<(), std::io::Error> {let v1 = vec![1, 2, 3];let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();Ok(())
}
使用闭包捕获环境
许多迭代器适配器(iterator adaptors)都接收闭包作为参数,而通常这些作为参数的闭包,它们会捕获其所在的环境 - 例如filter 方法
性能比较 循环 vs 迭代器
用mini grep搜索英文书籍中的"the"
测试出的结果:迭代器版本的性能略优于显示for循环版本!
零成本抽象
- 迭代器虽然是高级抽象,但在编译后,它们被转换成了与你手写低级代码几乎相同的代码。
- 迭代器是 Rust 的零成本抽象之一,这意味着使用这种抽象不会引入任何额外的运行时开销。
在 Rust 中可放心使用迭代器和闭包等高级特性,它们提供了更高层次的代码抽象,同时保持着极高的运行时性能,不会带来性能损失。
Box
智能指针Smart Pointers
- 指针:内存中存储数据的地址的变量,这个地址指向其他数据,最常见的指针是引用(&xxx),除了引用数据外,无其他特殊功能,无额外开销
- 智能指针:类似指针的数据结构,具有额外的元数据和功能
- 关键区别:引用仅借用数据,智能指针通常拥有数据
- 本课程之介绍常见的智能指针: Box、Rc、Ref、RefMut、RefCell
使用Box<T>指向存储于Heap的数据
Box<T>允许你将数据存储在 Heap 上(而不是 Stack)- 留在 Stack 上的只是指向 Heap 数据的指针 -
- 特点:
- 除了在 Heap 上而不是 Stack 上存储数据外,没有性能开销
- 没有太多额外功能
使用 Box<T> 的场景
- 在需要知道确切大小的上下文中,却使用一个在编译时无法确定大小的类型
- 有大量数据,想要转移所有权,但需确保在转移时数据不会被复制
- 当你想要拥有一个值,且你只关心它是否实现了某 Trait,而不是具体的类型
使用Box实现递归类型
fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
enum List {// 这里如果不用Box,rust无法估计需要多大的内存空间Cons(i32, Box<List>),Nil,
}
Deref Trait
通过Deref Trait处理智能指针,让它像普通引用一样。
- Deref trait 允许你自定义解引用运算符 * 的行为
- 通过适当实现 Deref trait,可以让智能指针像普通引用来使用
- 你编写的用于引用的代码,也能用于智能指针
实现
只需实现一个deref方法
fn main() {let x = 5;let y = NyBox::new(x);assert_eq!(5, x);assert_eq!(5, *y); // *(y.deref())
}struct NyBox<T>(T);impl<T> NyBox<T> {fn new(x: T) -> NyBox<T> {NyBox(x)}
}impl<T> Deref for NyBox<T> {type Target = T;fn deref(&self) -> &T {&self.0}
}
Deref coercion 隐式解引用转换
- 隐式解引用转换能将实现了 Deref trait 的类型的引用转换为另一个类型的引用
- 例如:由于 String 实现了返回 &str 的 Deref trait,所以可以将 &String 转换为 &str
- 编写函数和方法调用时,不需要添加太多显式的 & 和 *
- 允许编写能同时适用于引用或智能指针的代码
fn main() {let m = NyBox::new(String::from("rust"));hello(&m); // &MyBox<String> -> &String -> &str// deref()
}
fn hello(name: &str) {println!("hello, {}", name);
}
Deref Coercion与可变性
-
Deref:对不可变引用的 * 的重载
-
DerefMut:对可变引用的 * 的重载
三种解引用转换场景:
- &T -> &U (T: Deref<Target=U>)
- &mut T -> &mut U (T: DerefMut<Target=U>)
- &mut T -> &U (T: Deref<Target=U>)
Drop Trait
用于定义一个值即将超出作用域时的清理行为
- 实现智能指针时几乎总是会用到 Drop trait 的功能
- Rust 编译器会自动插入 Drop 实现中的代码,避免资源泄漏
- Drop trait 只要求实现 drop 方法,参数是对 self 的可变引用
- Drop trait 在 prelude 里,无需手动引入
fn main() {// 先释放let c = CustomSmartPointer {data: String::from("my stuff"),};// 后释放let d = CustomSmartPointer {data: String::from("other stuff"),};
}struct CustomSmartPointer {data: String,
}impl Drop for CustomSmartPointer {fn drop(&mut self) {println!("Dropping Cus with data {} !", self.data);}
}
使用std::mem::drop可以前drop值
- Rust 不允许手动调用 Drop trait 的 drop 方法
- 如果你想在值的作用域结束前强制丢弃它,你必须调用标准库提供的 std::mem::drop 函数
- std::mem::drop 的调用不会干扰 Rust 的自动清理机制
- 因为它通过接管值的所有权(ownership)并在调用后销毁它,避免了双重释放(double free)问题
fn main() {let c = CustomSmartPointer {data: String::from("my stuff"),};c.drop(); // double free!!
}
fn main() {let c = CustomSmartPointer {data: String::from("my stuff"),};drop(c);
}
Rc<T>
Reference Counting
- 有些情况下,一个值可能有多个所有者
- Rc 可以开启 “多重所有权”
- 跟踪一个值的引用数量,可判断该值是否还在使用
- 如果没有引用了,就可以清理掉了
- 跟踪一个值的引用数量,可判断该值是否还在使用
fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));println!("count after creating a = {}", Rc::strong_count(&a));let b = Cons(3, Rc::clone(&a));println!("count after creating b = {}", Rc::strong_count(&a));{let c = Cons(4, Rc::clone(&a));println!("count after creating c = {}", Rc::strong_count(&a));}println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}enum List {Cons(i32, Rc<List>),Nil,
}
使用场景
- 想在 Heap 上分配一些数据,供程序的多个部分读取,但在编译时无法确定哪个部分会最后完成对数据的使用
- 只可用于单线程场景
RefCell<T>和内部可变性
内部可变性模式 Interior mutability
- 对数据不可变引用时也可以修改数据
- 数据结构内部使用 unsafe 代码来绕过 Rust 通常的规则
- unsafe 代码:手动检查规则,而不是依赖编译器
- 只有当能保证在运行时借用规则被遵守时,才可使用内部可变性模式的类型
在运行时通过 RefCell<T> 强制执行借用规则
- RefCell 类型表示对其所持有数据的单一所有权
- 回顾借用规则:
- 在任何时刻,要么拥有一个可变引用,要么任意数量的不可变引用
- 引用必须始终有效
比较 RefCell<T> 和 Box<T>
| Box/ 引用 | RefCell | |
|---|---|---|
| 借用规则 | 编译时强制执行 | 运行时强制执行 |
| 若违反规则 | 编译报错 | 程序 panic |
RefCell何时使用
当你确信代码遵守了借用规则,但编译器无法理解或保证时
- Rust 编译器是保守的
- RefCell 只适用于单线程场景
- 用于多线程场景时,会给出编译时的错误
何时,用哪个?
| Box | Rc | RefCell | |
|---|---|---|---|
| 所有权 | 单一所有者 | 多个所有者 | 单一所有者 |
| 借用检查时机 | 编译时 | 编译时 | 运行时 |
| 允许的借用类型 | 可变、不可变 | 不可变 | 可变、不可变 |
| 内部可变性 | 不支持 | 不支持 | 支持 |
| 线程安全 | 看情况 | 仅单线程 | 仅单线程 |
在运行时使用RefCell<T>跟踪借用
borrow() -> Ref<T>
borrow_mut() -> RefMut<T>
引用循环导致内存泄漏
内存泄漏
永远不会被清理掉的内存
- Rust 的安全保障使得意外的内存泄漏很难发生,但不是不可能
- 完全防止内存泄漏并不是 Rust 的保证之一 -> 内存泄漏是内存安全的
- 例如:通过
Rc<T>和RefCell<T>就可创建出循环引用,导致内存泄漏- 各个项的引用数永不为 0
引用循环的现实影响与预防建议
- 在简单程序中引用循环的影响有限,但在复杂程序中可能导致严重的内存耗尽
- 创建引用循环虽然不易,但确实可能发生
- 当使用 RefCell 和 Rc 等类似嵌套组合时需特别注意避免循环引用
- Rust 无法自动检测引用循环,这属于逻辑错误
- 应使用自动化测试、代码审查等开发实践来避免引用循环
另一种策略
- 通过区分所有权引用和非所有权引用来重组数据结构可避免引用循环
- 只有所有权关系会影响值是否可被丢弃
- 链表结构中 Cons 变体必须拥有 List,所以这种方法不适用
- 图结构(父子节点关系)是展示非所有权关系防止循环的更好例子
防止引用循环:将 Rc<T> 转换为 Weak<T>
- 通过 Rc::clone 创建强引用,增加 strong_count。只有当 strong_count 为 0 时,
Rc<T>指向的值才会被清理 - 通过 Rc::downgrade 创建弱引用,增加 weak_count。弱引用不表示所有权,不影响清理时机,因此不会导致循环引用
Weak<T>不能直接使用它指向的值,需要通过 upgrade 方法检查该值是否仍然存在:- 如果
Rc<T>仍存在,upgrade 返回 Some (Rc<T>),否则返回 None
- 如果
- 应用场景:用树形结构(包含父子关系)替代单向链表
Thread
Rust 无畏并发的特点
- 目标:安全高效的并发编程
- 独特方法:利用所有权和类型系统在编译时防止并发错误
- 优势:在开发阶段而非生产环境中发现错误
- 灵活性:为不同并发模型提供多种工具
Rust 标准库的线程模型
- 编程语言以几种不同的方式实现线程,许多操作系统提供了一个 API,供语言调用以创建新线程。
- Rust 标准库使用 1:1 的线程实现模型,即程序中的一个语言线程对应一个操作系统线程。
- 还有一些 crate 实现了其他线程模型,这些模型对 1:1 模型做出了不同的权衡。(例如:Rust 的 async)
使用 thread::spawn 创建新线程
- 可以使用 thread::spawn 函数来创建一个新的线程
- 其参数为一个闭包
- 里面包含着在新线程要执行的代码
fn main() {thread::spawn(|| {for i in 1..10 {println!("hi number {} from the spawned thread!", i);thread::sleep(std::time::Duration::from_millis(1));}});for i in 1..5 {println!("he number {i} from the main thread!");thread::sleep(std::time::Duration::from_millis(1));}
}
使用 join Handles 等待所有线程完成
- 我们可以通过将 thread::spawn 的返回值保存到一个变量中,来解决生成的线程无法运行或过早结束的问题
- thread::spawn 的返回类型是 JoinHandle
- JoinHandle 是一个拥有的值,当我们调用它的 join 方法时,它会等待其线程完成
- 在 Handle 上调用 join 会阻塞当前正在运行的线程,直到该 Handle 代表的线程终止
- 阻塞一个线程意味着该线程被阻止执行工作或退出
fn main() {let handle = thread::spawn(|| {for i in 1..10 {println!("hi number {} from the spawned thread!", i);thread::sleep(std::time::Duration::from_millis(1));}});for i in 1..5 {println!("he number {i} from the main thread!");thread::sleep(std::time::Duration::from_millis(1));}handle.join().unwrap();
}
在线程中使用 move 闭包
- 我们经常会在传递给 thread::spawn 的闭包中使用 move 关键字,因为这样闭包会接管它从环境中使用的值的所有权,从而将这些值的所有权从一个线程转移到另一个线程
fn main() {let v = vec![1, 2, 3];let handle = thread::spawn(move || {print!("here is a vector : {v:?}");});handle.join().unwrap();
}
消息传递
- 线程或 actors 通过发送包含数据的消息来相互通信
- Go 语言口号:“不要通过共享内存来通信;而是通过通信来共享内存。”
- Rust 的标准库提供了通道(channel)的实现
channel
- 通道是一种程序设计概念,用于在不同线程之间发送数据
- 两个核心部分:发送端(transmitter)和接收端(receiver)
- 当通道的任一端(发送端或接收端)被丢弃时,我们说通道被关闭了
创建channel
- 使用 std::sync::mpsc 模块来创建通道
- mpsc 是 "multiple producer, single consumer” 的缩写,意味着可以有多个发送者,但只能有一个接收者
- 创建通道使用 mpsc::channel ()
- 这个函数返回一个 tuple
- 第一个元素是发送端,第二个元素是接收端
- 这个函数返回一个 tuple
发送数据
- 我们使用 send 方法发送一个值
- 这个方法接收一个参数,即我们想要发送的值
- 返回一个 Result<T, E> 类型
- 如果接收端已经被丢弃,send 操作会返回一个错误
接收数据
在接收端,我们有两种主要方法来接收消息:recv 和 try_recv
- recv 方法会阻塞当前线程,直到收到一个值
- 一旦有值被发送过来,recv 会返回一个包含该值的 Result
- 当所有发送端都关闭时,recv 会返回一个错误,表示不会再有更多值到来
- 一旦有值被发送过来,recv 会返回一个包含该值的 Result
- try_recv 方法不会阻塞,而是立即返回一个 Result
- 如果当前有消息可用,它返回 Ok 包含该消息;
- 如果当前没有消息,它返回 Err
fn main() {let (tx, rx) = mpsc::channel();thread::spawn(move || {let val = String::from("hi");tx.send(val).unwrap();});let received = rx.recv().unwrap();println!("Got: {}", received);
}
共享状态的并发
共享数据
- 另一种方法是让多个线程访问相同的共享数据。
- 在某种程度上,
- 任何编程语言中的通道都类似于单一所有权
- 共享内存并发就像多重所有权:多个线程可以同时访问相同的内存位置
Mutex 互斥锁
- Mutex: Mutual Exclusion
- 互斥锁在任何给定时间只允许一个线程访问某些数据
- 要访问互斥锁中的数据,线程必须请求获取互斥锁的锁
- 锁是互斥锁的一种数据结构,用于跟踪谁当前拥有对数据的独占访问权。
- 互斥锁被描述为通过锁定系统来保护它所持有的数据。
fn main() {let m = Mutex::new(5);{let mut num = m.lock().unwrap();*num = 6;}println!("m = {:?}", m);
}
使用 Arc<T>进行原子引用计数
// Rc<T>没法安全地在线程间被共享
fn main() {let counter = Rc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
Arc<T>是一种类似于Rc<T>的类型,可以安全地在并发环境中使用- A 代表 atomic(原子性),意味着它是一种原子引用计数类型
- 请参阅标准库文档中的 std::sync::atomic
- 为什么所有基本类型都不是原子的,为什么标准库类型默认不实现为使用
Arc<T>- 原因是线程安全会带来性能损失
fn main() {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 || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
Sync & Send trait
- Rust 语言本身的并发特性非常少
- 大多数并发功能都是标准库的一部分,而不是语言本身
- 可以编写自己的并发功能或使用第三方库
- 两个并发概念:std::marker traits ---- Sync 和 Send
Send trait
- Send (marker trait):所有权可以在线程之间转移
- 几乎所有 Rust 类型都是 Send
- 但有一些例外,例如 Rc 不是 Send
- Rc 仅用于单线程情况
- Rust 的类型系统和 trait 约束确保不会意外地将非 Send 类型跨线程发送
- 完全由 Send 类型组成的任何类型也自动标记为 Send
- 几乎所有原始类型都是 Send,原始指针除外
Sync trait
- Sync (marker trait):可以安全地从多个线程引用实现该 trait 的类型
- 如果 &T 是 Send,则类型 T 是 Sync
- 即该引用可以安全地发送到另一个线程
- 原始类型是 Sync
- 完全由 Sync 类型组成的类型也是 Sync
线程安全性与 Sync
-
Sync 是 Rust 中最接近 “线程安全” 的概念
- “线程安全” 指特定数据可以被多个并发线程安全使用
-
分开 Send 和 Sync 特性的原因:一个类型可能是其中之一,两者都是,或两者都不是:
Rc<T>:既不是 Send 也不是 SyncRefCell<T>:是 Send(如果 T 是 Send),但不是 SyncMutex<T>:是 Send 也是 Sync,可用于多线程共享访问MutexGuard<'a, T>:是 Sync(如果 T 是 Sync)但不是 Send
手动实现 Send 和 Sync
- 由 Send 和 Sync trait 组成的类型自动也是 Send 和 Sync
- 通常不需要手动实现这些 trait
- 作为 marker trait,它们甚至没有任何方法需要实现
- 手动实现这些 trait 涉及实现 unsafe Rust 代码
- 构建不是由 Send 和 Sync 部分组成的新并发类型需要仔细思考
- “The Rustonomicon” 提供了更多关于这些保证的信息
模式匹配
模式
- Rust 中的特殊语法,用于匹配简单或复杂类型的结构
- 例如:与 match 表达式和其他构造结合,增强程序控制流程
模式的组成
- 字面值 (Literals): 例如数字或字符串
- 解构数据结构:数组、枚举、结构体、元组等
- 变量 (Variables): 命名的变量
- 通配符 (Wildcards): _ 表示任意值
- 占位符 (Placeholders): 尚未具体定义的部分
使用方法
- 比较模式与值,若匹配成功则提取并使用数据
- 常用场景:例如 match 表达式,允许根据数据形状选择不同代码路径
关键点
- 可反驳模式 vs 不可反驳模式
- 学会运用模式以清晰地表达编程概念
可以使用模式的地方
match分支
match x {None => None,Some(i) => Some(i + 1),
}
if let
fn main() {let favorite_color: Option<&str> = None;let is_tuesday = false;let age: Result<u8, _> = "34".parse();if let Some(color) = favorite_color {println!("Using your favorite color, {color}, as the background");} else if is_tuesday {println!("Tuesday is green day!");} else if let Ok(age) = age {if age > 30 {println!("Using purple as the background color");} else {println!("Using orange as the background color");}} else {println!("Using blue as the background color");}
}
while let
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {for val in [1, 2, 3] {tx.send(val).unwrap();}
});while let Ok(value) = rx.recv() {println!("{value}");
}
for
let v = vec!['a', 'b', 'c'];for (index, value) in v.iter().enumerate() {println!("{value} is at index {index}");
}
Rust 模式中的可反驳性
- 不可反驳模式:适用于所有可能的值,例如 let x = 5 中的 x。
- 可反驳模式:可能不匹配某些值,例如 if let Some (x) = a_value 中的 Some (x)。
哪些地方只能使用不可反驳模式
- 像函数参数、let 语句、for 循环,它们都只能使用不可反驳的模式
- 因为如果模式不能保证匹配成功,程序就无法继续执行下去
- 例如:let Some (x) = some_option_value;,如果值是 None… 程序崩溃
哪些地方可接受可反驳模式
- if let、while let、let else语句可以接受可反驳模式
if let x = 5 {println!("{x}");
};
- 编译器会警告你:你这段代码写了个没必要的if let,还不如直接用let呢!
- 注意:在 match 表达式中
- 大多数情况下你需要使用可反驳的模式来分别处理不同的情况
- 只有最后一个 match 分支,用来兜底,可以使用不可反驳的模式
- 比如
_,确保所有可能的情况都被涵盖。
模式的语法
匹配字面值
let x = 1;match x {1 => println!("one"),2 => println!("two"),_ => println!("anything"),
}
