基本概念
- 类型
- 函数
- 流程控制
rust所有变量默认是不可变的
声明增加 mut 才可以改变
rust 数据类型
rust是静态类型语言
Scalar Types
-
integer
Length Signed Unsigned 8 i8 u8 16 i16 u16 32 i32 u32 64 i64 u64 128 i128 u128 arch isize usize iszie和usize取决于电脑架构 int 默认是i32声明示例
literals 示例 Decimal 1_000 Hex 0xff Octal 0o77 Binary 0b1111_0000 Byte(u8 only) b'A' 标准库中处理溢出的方法
wrapping_*循环checked_*溢出返回Noneoverflowing_*返回数字值和是否溢出saturating_*返回最接近的值
-
floating-point
- f32
- f64 默认
let x = 2.0 //f64
-
boolean 1 byte长度 使用
bool -
character 4 byte长度 使用
char#![allow(unused)] fn main() { let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
Compound Types
-
tuple 固定长度,声明后长度不能变动 两种使用方式 1 destructuring
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}"); }2 indexing
fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; }空元组
()叫unit,表示一个空值或者一个空的返回类型,表达式在返回空值时隐式返回unit -
array 固定长度,分配在栈上
#![allow(unused)] fn main() { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }声明时指定类型和长度
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }指定相同内容和长度
#![allow(unused)] fn main() { let a = [3; 5]; //a = [3, 3, 3, 3, 3] }
Function
使用 fn 声明函数,snake case 命名
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }
声明带参数的函数
#![allow(unused)] fn main() { fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); } }
Statements and Expressions 语句和表达式
rust 是基于表达式的语言
- 语句
Statements是执行某些操作而不返回值的指令 - 表达式
Expressions是计算并返回值的代码
fn main() { let y = 6; }
let y = 6; 是一个语句,整个函数也是语句。表达式可以是语句的一部分,let y = 6;其中的6是一个表达式
调用函数和宏 macro是表达式
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); }
一个由 {}构成的语句块是表达式。表达式不需要句末的 ;,如语句块的第二行 x+1,加上 ;会变成语句
带返回值的函数
无返回值时隐式返回空元组()即unit
声明带返回值的函数
#![allow(unused)] fn main() { fn 函数名(参数列表) -> 返回值类型 { 函数体 } }
rust 的函数返回值和函数体的最后表达式的值相同,大多数函数隐式的返回最后表达式的值,当然也可以显式的使用 return返回
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}"); }
如上述five函数的5这行末尾没加;,隐式的返回5为函数返回值。若加上;,则5变成了语句,five函数隐式返回空tuple ()的unit,和函数指定的i32不匹配,就会报编译错误error[E0308]: mismatched types
通过元组返回多个值
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
流程控制
if
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
注意 if后{}内的类型必须一致
循环
循环分为三种:loop、while、for
loop表示永远循环,支持break返回参数
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
可以使用 'loopname 给 loop 命名,break 指定 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}"); }
while条件循环
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
for循环
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
搭配range使用
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
理解所有权 Ownership
所有权是rust独一无二的特性,使rust在没有GC的情况下保证内存安全
什么是所有权
所有权是一组控制 Rust 程序如何管理内存的规则
所有程序在运行时都必须管理如何使用内存,有些语言使用GC在程序运行时定期处理未使用的内存,有些语言必须显式的分配和释放内存。rust使用第三种方式:通过在编译期检查一系列规则的所有权系统管理内存。若违反规则就不会通过编译
所有权只在编译期检查,不会影响运行期的效率
栈Stack和堆Heap
存在stack上的数据必须是已知,固定的大小。反之会存在heap上
当存数据到heap上时,需要先申请指定的空间,然后 memory allocator在heap中找到一个足够大的空间,标记其被使用,然后返回指向这个位置的指针。因为指针是一个固定大小的值,所以该指针可以存在stack上,当需要实际的数据时,还是需要去指针指向的位置获取数据。
往stack上存数据是快过于往heap上存数据的,因为stack只需要放到栈顶,而heap需要找到足够大的空间,然后标记被使用,然后返回指针。同样,从stack上取数据也是快过heap的
调用函数时,传递进函数的值(包括指向heap数据的指针)和函数的局部变量push到stack,函数结束时这些值将会从stack上pop
跟踪代码的哪些部分正在使用heap上的哪些数据,最小化heap上的重复数据,以及清理heap上未使用的数据,这样就不会耗尽内存,这些都是所有权解决的问题
所有权规则
- rust中每个值都有一个拥有者(owner)
- 每次只能有一名拥有者
- 当所有者超出作用域(scope)时,该值将被删除(drop)。
变量作用域(scope)
作用域是程序中某项有效的范围
#![allow(unused)] fn main() { { // s is not valid here, it’s not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid }
- 当
s进入作用域时开始有效 - 保证有效直到离开作用域
String 类型
String存储在heap上
从字面量声明一个String
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
图左面的ptr,len,cap存储在stack上,右面的数据存储在heap上
上述代码将s1赋值给s2其实是将stack上的ptr,len,cap复制了一份,并不会复制右边heap的实际数据
在将s1赋值给s2后使用s1会报
error[E0382]: borrow of moved value:
s1
s1赋值给s2在其他语言中叫shallow copy 浅拷贝,但rust同时让s1无效,在rust中叫move。上述例子就是s1 moved to s2
rust永远不会自动创建深拷贝
深拷贝需要主动调用clone()方法
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
Stack-Only Data: Copy
rust有一个特殊的标识trait Copy,如果一个类型实现了Copy trait,那么它将会被保存在stack上,所以在赋值时是浅拷贝的
实现了droptrait的类型,不能添加Copy trait
以下这些类型都实现了copy trait
- 所有整数类型,如
i32 - 布尔类型,如
bool - 所有浮点数类型,如
f64 - 字符类型,如
char - 元组,如
(i32, i32),只要元组中所有元素都实现了Copytrait
所有权和函数
函数传递参数类似赋值,传递参数时可能会copy或者move
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
返回值和作用域
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
变量的所有权始终遵循相同的模式:赋值给新变量时,原变量move
The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it.
当包含heap数据的变量出作用域时,变量将会被drop清理除非其所有权已经moved
引用References 和借用Borrowing
函数每次调用都伴随参数所有权的转移和返回,为了解决这种问题,rust提供了引用(reference)
- 引用类似指针,可通过它保存的地址访问存储在该地址的数据。和指针不同,引用的整个生命周期保证指向有效的值
- 引用不会拿走值的所有权
fn main() { let s1 = String::from("hello"); //`&s1`语法创建了一个指向`s1`的值引用,但不拥有`s1` let len = calculate_length(&s1); //因为没有s1的所有权,函数`calculate_length`结束时`s`也不会`drop` println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because it does not have ownership of what // it refers to, it is not dropped.
使用
&引用,*取引用指向的值
rust称之为引用借用(borrowing),类比现实中的某人有某物,你借用该物,在用完后归还,你从来没有拥有过该物
修改一个引用会直接编译错误
fn main() { let s = String::from("hello"); change(&s); } fn change(some_string: &String) { some_string.push_str(", world"); }
error[E0596]: cannot borrow
*some_stringas mutable, as it is behind a&reference
可变(Mutable)引用
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
&mut T语法创建了一个可变引用mutable reference,允许修改引用所指向的值
但可变引用有一个巨大限制,在已有一个可变引用存在的情况下,不能再创建其他可变引用
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); //报错 error[E0499]: cannot borrow `s` as mutable more than once at a time }
这个限制可以在编译期防止数据竞争data race
数据竞争在以下情况时发生
- 2个或更多个指针同时访问相同数据
- 至少有一个指针被用来写入数据
- 没有访问数据的同步机制
可以使用创建新的作用域规避这个限制
#![allow(unused)] fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s; }
同样,在已有引用存在的情况下,不能在创建可变引用
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{r1} and {r2}"); // variables r1 and r2 will not be used after this point let r3 = &mut s; // no problem println!("{r3}"); }
但支持多个不可变引用
引用的作用域从第一次引用开始到最后一次引用结束
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{r1} and {r2}"); // variables r1 and r2 will not be used after this point let r3 = &mut s; // no problem println!("{r3}"); }
悬空(Dangling)引用
在其它有指针的语言中,很容易错误的创建悬空指针(指向内存可能已被分配给其它指针的指针,如释放内存但保留指针)
rust编译器保证引用永不会悬空,确保引用的作用域内引用的数据始终有效
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { // dangle returns a reference to a String let s = String::from("hello"); // s is a new String &s // we return a reference to the String, s } // Here, s goes out of scope, and is dropped. Its memory goes away. // Danger! //编译报错 error[E0106]: missing lifetime specifier
引用规则总结
- 引用不会拿走所有权
- 在任何时间,可以有一个可变引用或者多个不可变引用
- 引用必须总是有效的
切片 Slice
切片允许引用集合中的连续元素序列,而不是使用整个集合。切片也是一种引用,同样不会有所有权
字符串切片
字符串切片就是字符串的部分引用
#![allow(unused)] fn main() { let s = String::from("hello world"); let hello = &s[0..5];// s[..5] == "hello" let world = &s[6..11];// s[6..] == "world" }
创建slice语法为&s[start_index..end_index]
slice长度为end_index - start_index
range..语法
- 若想从
0开始,可以省略start_index,如&s[..end_index] - 若想包含
String的最后一个字符,可省略end_index,如&s[start_index..](等于&s[start_index..s.len()]) - 若取整个
String,可以全部省略&s[..]
string slice写作&str
#![allow(unused)] fn main() { fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } }
理解字符串字面量(Literals)
#![allow(unused)] fn main() { let s = "Hello, world!"; }
s类型实际是&str,这也解释了为什么字符串字面量不可变,因为&str是不可变引用 immutable reference
使用字符串切片替换字符串作为参数
fn first_word(s: &String) -> &str { //变更为 fn first_word(s: &str) -> &str { //字符串切片&str替换字符串引用&String让参数变的更灵活 fn main() { let my_string = String::from("hello world"); //可接受字符串切片为参数 let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); //同样接收字符串引用为参数(和整个字符串切片相等) let word = first_word(&my_string); let my_string_literal = "hello world"; // 也可接收字符串字面量切片 let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); //字符串字面量本身就是字符串切片自然可以接收 let word = first_word(my_string_literal); }
上述代码利用rust的类型自动转换deref coercions特性
其它类型的Slice
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
上面切片类型为&[i32],原理和字符串切片一样,保存一个指向第一个元素的引用和长度。slice可以用在所有可排序的集合上
总结
所有权(ownership),借用(borrowing),和切片(slice)在编译期确保了rust的内存安全
rust语言可以像其他系统编程语言一样控制内存使用,还可以当数据的所有者超出作用域时自动清理数据
结构体 struct
定义和实例化
#![allow(unused)] fn main() { struct User { active: bool, username: String, email: String, sign_in_count: u64, } }
上面定义了一个user结构体
fn main() { let mut user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
上面创建了一个user的可变实例user1,注意整个实例都是可变的,rust不支持控制指定字段可变性
初始化时并不需要按照struct字段的顺序
创建实例时可省略字段名
#![allow(unused)] fn main() { fn build_user(email: String, username: String) -> User { User { active: true, username, email, sign_in_count: 1, } } }
使用更新语法 struct update syntax
fn main() { // --snip-- let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; //下面的struct update syntax比上面的简洁许多 let user2 = User { email: String::from("another@example.com"), ..user1 }; }
注意 stuct update syntax类似于使用 =赋值。上述的 user1的name所有权已经 moved到user2了。不能再次使用user1的name。但active和sign_in_count是实现copy存储在stack上的,所以可以再次使用。
元组结构体 Tuple Structs
struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
注意 black和origin是不同类型,虽然他们的字段类型相同
元组结构体的字段没有名字,类似于元组可通过索引访问
类似于空元组()即unit的结构体
空结构体在你需要实现某些特性trait,但不想存储任何数据的时候很实用
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
使用派生特性添加实用功能 Adding Useful Functionality with Derived Traits
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {}", rect1); }
使用println! 宏以{}打印结构体时,需要结构体实现Display trait
使用{:?}打印结构体时,需要结构体实现Debug trait
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
以上将输出
rect1 is Rectangle { width: 30, height: 50 }
使用{:#?}将输出
#![allow(unused)] fn main() { rect1 is Rectangle { width: 30, height: 50, } }
另外一种方式是使用dbg!宏。dbg!宏将输出到stderr,而println!宏则输出到stdout。dbg!宏将会改变参数的所有权,println!宏是borowing参数的引用
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
输出
#![allow(unused)] fn main() { [src/main.rs:10:16] 30 * scale = 60 [src/main.rs:14:5] &rect1 = Rectangle { width: 60, height: 50, } }
方法 Method
方法method大体类似函数function,但method的第一个参数始终是self
定义方法
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
其中&serf是self:&Self的语法糖,在impl块中Self就是这个块的类型别名。方法可以拿走所有权(使用self),也可以借用(&self)和可变借用(&mut self)
注意方法名可以和字段名相同
impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
使用同名方法返回字段在其它语言叫getter
调用方法直接使用struct的实例即可,rust会自动处理和方法签名的Receiver匹配
方法可以有参数
#![allow(unused)] fn main() { impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
can_hold方法除了一个不可变借用的Receiver外还有一个同类型的不可变借用参数
相关函数 Associated Functions
所有定义在impl块中的函数都被称为associated functions
在某些不需要类型实例的清下可以定义第一个参数不是self的的函数(因为没有self,所以不是方法)
不是方法的associated function经常被用来作为返回该类型新实例的构造器;如常用的String::from就是String类型的构造器
#![allow(unused)] fn main() { impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } }
如上面的square函数通过Self(该类型别名)方便的构造一个正方形Rectangle实例
调用associated function语法为Rectangle::square(3);
::语法同时用在associated function和modules创建的命名空间上
多个impl块
#![allow(unused)] fn main() { impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
总结
struct让你可以自定义类型用于存储多个相关联的数据- 在
impl可以定义和类型相关的函数(associated function),方法(method)也是一种让你可以指定struct实例行为的相关函数
枚举 Enum
enums give you a way of saying a value is one of a possible set of values.
定义枚举
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } }
上面定义了一个ip地址类型的枚举,其中包含两个变体(variant)V4和V6。
枚举值
#![allow(unused)] fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; }
分别为枚举的两个变体创建了两个实例
注意使用枚举的变体同样使用::
#![allow(unused)] fn main() { fn route(ip_kind: IpAddrKind) {} route(IpAddrKind::V4); route(IpAddrKind::V6); }
上面的route接收枚举IpAddrKind作为参数
#![allow(unused)] fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
枚举的变体可以直接存放数据,而且每个变体可以是不同类型
#![allow(unused)] fn main() { struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
标准库std的ip地址实现
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } //上面枚举的4个变体类似于下面的4个结构体 //但使用结构体却不能简单的用一种函数参数传递上面所有的消息类型 struct QuitMessage; // unit struct struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct impl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call(); }
更复杂的枚举
Quit是一个空变体Move类似带命名字段的结构体Write和ChangeColor类似元组结构体
类似结构体,枚举也可以在impl块中定义方法
Option Enum 及其相对Null的优点
rust中没有Null
#![allow(unused)] fn main() { enum Option<T> { None, Some(T), } }
rust通过标准库中的Option枚举来表达存在或者不存在(Null)
因为Option枚举非常实用,所以已经预置不需要在明确引入作用域,不再需要Option::前缀直接使用Some和None即可
#![allow(unused)] fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; }
Some可以不必显式声明Option类型,可以通过Some传递的类型推断。
但None必须显式声明Option类型
为什么 Option 优于 Null
在你使用T之前,你必须处理Option<T>到T的转换。这个过程通常能解决使用Null最常见的问题:当它实际为空时假设它不为空
当你需要一个可能为空的值时,必须明确使用Option<T>来表示这个值。在使用这个值时,必须明确的处理当这个值是Null的情况。这样
只要不是Option<T>的类型都可以安全的认为该值不会为Null
This was a deliberate design decision for Rust to limit null’s pervasiveness and increase the safety of Rust code.
match 流程控制结构
match是rust中非常强大的流程控制结构。可以允许你比较一个值和一系列的模式(pattern),然后执行匹配模式的代码
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
和if不同的是if条件只接受bool值,而match模式可以接受任何类型
类似if,match的每一行也叫臂arm,arm由=>分隔模式和代码,每行的arm由,分隔
arm后代码是表达式;当arm后代码非常简单时不需要{},使用{}时,分隔可省略
绑定值的模式
#![allow(unused)] fn main() { #[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {state:?}!"); 25 } } } }
当调用value_in_cents(Coin::Quarter(UsState::Alaska)),模式中的state会绑定到UsState::Alaska
与Option<T>匹配
#![allow(unused)] fn main() { 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是全面的
match中arm的模式必须覆盖所有可能的值
#![allow(unused)] fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { Some(i) => Some(i + 1), } } }
以上代码会报错
error[E0004]: non-exhaustive patterns:
Nonenot covered
捕获所有模式和_占位符
#![allow(unused)] fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {} }
最后一个arm是other可以覆盖所有可能的值,因此other不能出现在你指定的模式之前,否则你的模式不再会匹配,当然编译器也会用warn提醒你
#![allow(unused)] fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), //注意这是一个空元组()也就是unit } fn add_fancy_hat() {} fn remove_fancy_hat() {} }
当你不需要模式绑定值时,可以直接使用_占位符
模式匹配总结
rust模式匹配必须全面覆盖所有可能的值rust有2种捕获所有模式(catch-all pattern)- 需要模式绑定值时使用
other - 反之使用
_
- 需要模式绑定值时使用
使用if let精简流程控制
#![allow(unused)] fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {max}"), _ => (), } //可使用if let简化为 let config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {max}"); } }
if let块只有在匹配到模式之后才会执行
选择if let或match取决于你的具体场景和在简洁及全面模式覆盖之间的权衡。换言之,在只需要匹配一种模式忽略其他模式时将if let当作match的语法糖
也可添加else块
#![allow(unused)] fn main() { let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {state:?}!"), _ => count += 1, } //if let简化 let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {state:?}!"); } else { count += 1; } }
总结
rust标准库中的Option<T>有助于规避Null引起的问题- 使用
match或if let体验rust强大的模式匹配实现直观的流程控制
使用Package,crate和Moudles管理日益增长的Rust程序
module system
- Package: A Cargo feature that lets you build, test, and share crates.
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
Package 和 Crates
crate
crate分为两种
- binary crate :二进制
- library crate :库
二进制的crate可以编译成可执行文件,必须包含main函数;而库的crate没有main函数也不被编译成可执行文件。它定义打算和多个项目共享的功能,大多数情况下rust开发者说的crate就是库的crate
包 Package
A package is a bundle of one or more crates that provides a set of functionality.
package包含一个描述如何构建这些crates的Cargo.toml文件,
Cargo实际就是一个package,包含了构建代码的命令行工具的二进制的crate和其依赖的库 crate
package可以包含任意数量的二进制crate,但最多有一个库 crate。package必须包含至少一个crate,不论是二进制crate还是库 crate
#![allow(unused)] fn main() { $ cargo new my-project crated binary (application) `my-project` package $ ls my-project Cargo.toml src $ ls my-project/src main.rs }
执行cargo new之后,Cargo遵循惯例将src/main.rs作为二进制crate的根(root),若是src/lib.rs则是library crate的根,Cargo传递crate根到rustc以构建lib和可执行文件
定义模块(Modules)以控制作用域和隐私
模块快速参考
- 从crate根开始: 当编译crate时,编译器会首先寻找crate根(通常是src/lib.rs或者src/main.rs)开始编译代码
- 声明模块: crate根文件中可声明新模块,比如使用
mod garden;声明"garden"模块。编译器会在以下位置寻找该模块代码- 在当前根文件中寻找
mod garden{} - src/garden.rs中
- src/garden/mod.rs中
- 在当前根文件中寻找
- 声明子模块: 在crate根文件之外,可以声明子模块。如在src/garden.rs中声明一个
mod vegetables;。编译器会在以下位置中寻找- 在当前文件src/garden.rs中寻找
mod vegetables{} - src/garden/vegetables.rs中
- src/garden/vegetables/mod.rs中
- 在当前文件src/garden.rs中寻找
- 模块中的代码路径: 比如使用vegetable模块的
Asparagus类型为crate::garden::vegetables::Asparagus - private和public: 模块中的代码默认是私有的。显式使用
pub mod声明模块为公开。同样使用pub关键字声明公开模块的成员为公开 - use: 作用域中可使用
use关键字,可以创建捷径减少长路径的重复。如crate::garden::vegetables::Asparagus可使用use crate::garden::vegetables::Asparagus;,然后就可以直接使用Asparagus而不再需要前面那一长串路径
#![allow(unused)] fn main() { backyard ├── Cargo.lock ├── Cargo.toml └── src ├── garden │ └── vegetables.rs ├── garden.rs └── main.rs }
根文件是src/main.rs,其内容为
use crate::garden::vegetables::Asparagus; pub mod garden; fn main() { let plant = Asparagus {}; println!("I'm growing {plant:?}!"); }
pub mod garden;告诉编译器在src/garden.rs中寻找garden模块的代码,其内容为
#![allow(unused)] fn main() { pub mod vegetables; }
pub mod vegetables;表明src/garden/vegetables.rs也被包含。其内容如下
#![allow(unused)] fn main() { #[derive(Debug)] pub struct Asparagus {} }
引用模块树中项的路径
路径有两种
- 绝对路径: 从crate根开始的全路径。外部crate从crate名开始,当前crate从
crate开始 - 相对路径: 从当前模块开始。使用
self,super或当前模块的标识符
绝对和相对路径都使用::分隔
src/lib.rs
#![allow(unused)] fn main() { mod front_of_house {//这儿不需要pub是因为eat_at_restaurant是同一模块 pub mod hosting { //使用pub声明模块,下面才能访问 pub fn add_to_waitlist() {}//同样添加pub才能使用该函数 } } pub fn eat_at_restaurant() { // Absolute path crate::front_of_house::hosting::add_to_waitlist(); // Relative path front_of_house::hosting::add_to_waitlist(); } }
rust中,所有项目包括函数,方法,结构体,枚举,模块和常量对于它们上级模块默认都是私有的
父模块不能使用子模块中的私有项,但子模块可以使用祖先模块中的项。使用pub公开子模块中的项即可访问
含二进制(binary)和库(libray)的package最佳实践
对于同时包含二进制和库crate的包,通常只在二进制crate中包含调用库crate足够启动可执行程序的代码。模块树应该定义在src/lib.rs中,这样二进制crate可以以包名开始的路径访问所有公开的项。二进制crate就变成了库crate的用户,就像其它完全外部的crate使用库crate一样:只能使用库crate公开的API。这有助于你设计更好的API
用super开始相对路径
#![allow(unused)] fn main() { fn deliver_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::deliver_order(); } fn cook_order() {} } }
使用super可访问父模块中的项
公开结构体和枚举
默认情况下,结构体和成员都是私有的。可以使用
pub关键字来公开
#![allow(unused)] fn main() { mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, //私有的 } impl Breakfast { //构造夏季特供水果的函数 pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } } pub fn eat_at_restaurant() { // Order a breakfast in the summer with Rye toast let mut meal = back_of_house::Breakfast::summer("Rye"); // Change our mind about what bread we'd like meal.toast = String::from("Wheat"); println!("I'd like {} toast please", meal.toast); // The next line won't compile if we uncomment it; we're not allowed 因为seasonal_fruit是私有的 // meal.seasonal_fruit = String::from("blueberries"); } }
相反,只要枚举是公开的,其所有变体也会公开
#![allow(unused)] fn main() { mod back_of_house { pub enum Appetizer { Soup, Salad, } } pub fn eat_at_restaurant() { let order1 = back_of_house::Appetizer::Soup; let order2 = back_of_house::Appetizer::Salad; } }
因为枚举变体若不公开的话枚举也没有意义,所以枚举变体默认是公开的。但结构体经常会使用私有的字段,所以除非显示添加pub,字段默认是私有的
use引入路径到作用域
无论使用相对还是绝对路径引入模块中的项都是冗长的,所有引入了use来简化流程,使用use创建一次捷径之后就可以使用更简洁的名字在作用域的任意位置
#![allow(unused)] fn main() { mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; //之后就可以直接使用hosting pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } }
注意use创建的捷径只对其使用时的作用域有效
#![allow(unused)] fn main() { mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; //mod customer和上面use是不同的作用域,编译会报错 //error[E0433]: failed to resolve: use of undeclared crate or module `hosting` mod customer { //use crate::front_of_house::hosting; 可在模块内引入 pub fn eat_at_restaurant() { hosting::add_to_waitlist();//或者使用super:: } } }
创建惯用的use路径
#![allow(unused)] fn main() { mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant() { add_to_waitlist(); } }
虽然这段代码和之前的实现相同的任务,但直接用use引入到函数不是惯用的做法。因为显式的指定其父模块可表明其不是当前模块的函数也可以避免同名的冲突
另一方面使用use引入结构体,枚举和其它项时,指定全路径时惯用做法
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
这个习惯用法背后并没有什么强有力的理由: 它只是一个已经出现的约定,人们已经习惯了用这种方式读写 Rust 代码。
作用域出现同名的项时需要显式指定模块避免冲突
#![allow(unused)] fn main() { use std::fmt; use std::io; fn function1() -> fmt::Result { //重名时显式指定模块 // --snip-- } fn function2() -> io::Result<()> { // --snip-- } }
使用as重命名
上面的例子可用as重命名解决
#![allow(unused)] fn main() { use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- } fn function2() -> IoResult<()> { // --snip-- } }
使用pub use重新导出
当引入一个私有的模块时,为了使其可用,rust组合了pub use。这个技巧称为re-exporting,因为将该项引入到作用域,同时也让其他可以把该项引入他们的作用域
#![allow(unused)] fn main() { mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } }
使用外部包
比如引入rand包,需要先在Cargo.toml中添加依赖
#![allow(unused)] fn main() { rand = "0.8.5" }
这将告知Cargo去 crates.io下载rand及其依赖到我们的项目中
use rand::Rng; //引入rand包 fn main() { let secret_number = rand::thread_rng().gen_range(1..=100); }
注意std标准库虽然也是外部的crate,但rust中已经将其内置。所以不需要在Cargo.toml中添加,直接use即可
#![allow(unused)] fn main() { use std::collections::HashMap; }
使用嵌套路径整理use列表
#![allow(unused)] fn main() { // --snip-- use std::cmp::Ordering; use std::io; // --snip-- //可使用嵌套路径简化为 // --snip-- use std::{cmp::Ordering, io}; // --snip-- }
#![allow(unused)] fn main() { use std::io; use std::io::Write; //可以简化为 use std::io::{self, Write}; }
使用 glob *通配符
如果想引入所有公开项到作用域,可以使用*通配符
#![allow(unused)] fn main() { use std::collections::*; }
分隔模块到不同文件
将之前的src/lib.rs中的front_of_house模块提取为一个单独的文件src/front_of_house.rs
src/lib.rs
#![allow(unused)] fn main() { mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } }
src/front_of_house.rs
#![allow(unused)] fn main() { pub mod hosting { pub fn add_to_waitlist() {} } }
注意在模块树中只需要使用
mod声明加载模块一次 接着继续提取hosting模块到src/front_of_house/hosting.rs注意front_of_house.rs将变更为 src/front_of_house.rs
#![allow(unused)] fn main() { pub mod hosting; }
src/front_of_house/hosting.rs
#![allow(unused)] fn main() { pub fn add_to_waitlist() {} }
对于上面的例子中模块front_of_house编译器会在以下文件中寻找模块代码
- src/front_of_house.rs (上面是这种)
- src/front_of_house/mod.rs (older style, still supported path)
hosting模块一样
- src/front_of_house/hosting.rs (上面是这种)
- src/front_of_house/hosting/mod.rs (older style, still supported path)
同一模块中可以使用上面两种路径的任意一种,但混用就会引起编译错误。不同模块倒可以混用,但不推荐,会降低项目代码可读性。想想如果你使用第二种方式,很多文件都叫mod.rs,在编辑器中同时打开时够你受的
mod声明模块,编译器会去指定的位置寻找模块同名的文件
总结
rust划分包package到不同的crate,crate又由不同的模块module构成- 可使用相对或绝对路径引入其它模块,也可使用
use简化路径 - 模块中代码默认都是私有,使用
pub公开
常用集合
rust的std中有多种实用的数据结构称为集合Collection
集合存储在堆heap上,意味在编译期不必预知集合的大小,集合大小在运行期动态变化。每种集合有不同的能力和代价
我们主要论述以下三种使用频繁的集合
- vector 存储相邻的数据
- string 存储字符的集合
- hashmap 存储键值对,关联值v到特定的k上
vector
vector将多个相同类型数据存储在连续的内存中
创建vector
#![allow(unused)] fn main() { //创建空vector,需要显式声明i32类型 let v: Vec<i32> = Vec::new(); //通过vec!宏使用初始值创建,rust会自动推断类型为i32 //因为i32是int的默认类型 let v = vec![1, 2, 3]; }
添加元素
#![allow(unused)] fn main() { //使用mut创建一个可变vector let mut v = Vec::new(); //使用push添加元素,rust会自动推断类型为i32 v.push(5); v.push(6); v.push(7); v.push(8); }
移除元素
remove移除并返回指定索引的元素,并移动索引之后的元素到左边swap_remove若不需保留元素顺序可替代removepop移除并返回最后一个元素的Option
#![allow(unused)] fn main() { let mut v = vec![1, 2, 3]; assert_eq!(v.remove(1), 2); assert_eq!(v, [1, 3]); }
读取元素
有两种方式可读取vertor中的元素
- 使用索引
- 使用get方法
#![allow(unused)] fn main() { let v = vec![1, 2, 3, 4, 5]; //索引 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."), } }
使用超出范围的索引会直接panic
#![allow(unused)] fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); //thread 'main' panicked at src/main.rs:4:28: //index out of bounds: the len is 5 but the index is 100 }
注意所有权和借用规则
#![allow(unused)] fn main() { let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; //不可变借用 v.push(6); //可变借用 println!("The first element is: {first}");//不可变借用 //error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable }
上述代码会报错是由于vector的存储原理:vector始终连续的存储值到内存中,当push一个新元素时,如果当前的空间不足以存储,vector会重新分配新内存然后copy之前的元素到新空间。
所以上述first的不可变借用可能会被push的可变借用所影响,rust借用规则自然会阻止其通过编译
遍历vector
#![allow(unused)] fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } //也可使用可变借用以改变其元素的值 let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50;//使用*取引用的值 } }
* :dereference operator 解引用(叫取值更通顺)操作符
注意,无论使用可变或不可变引用遍历时不能添加或删除元素,会造成编译错误。还是因为借用规则,可变引用和不可变引用不能同时存在
使用枚举
vector只能存储相同类型的值,但可以借助枚举的特性间接存储不同类型的值
#![allow(unused)] fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; }
rust在编译期需要知道vector中存储的元素类型,然后才能精确计算需要多少heap内存去存储每个元素。所以使用枚举+match可以保证在编译期所有可能的情况都被处理
删除vector会删除其所有元素
和其它类型一样,vector在离开作用域时会被释放
#![allow(unused)] fn main() { { let v = vec![1, 2, 3, 4]; // do stuff with v } // <- v goes out of scope and is freed here }
当vector被释放时,vector中所有元素也会被清理
vector始终是(ptr,len,cap)三元组
包含'a','b'的容量为4的vector如下
#![allow(unused)] fn main() { ptr len capacity +--------+--------+--------+ | 0x0123 | 2 | 4 | +--------+--------+--------+ | v Heap +--------+--------+--------+--------+ | 'a' | 'b' | uninit | uninit | +--------+--------+--------+--------+ }
参考vector文档
String
String是UTF-8编码的字符串 若不需要utf8编码的字符串,可使用OsString
str
str是string slice,是最原始的字符串类型。常见使用&str,也是字符串字面量
#![allow(unused)] fn main() { //声明一个字符串字面量 let hello_world = "Hello, World!"; }
string字面量拥有静态statics生命周期,意味着在整个程序运行期间都是有效的
#![allow(unused)] fn main() { //显式的声明生命周期 let hello_world: &'static str = "Hello, world!"; }
&str由指向字节数组的指针和长度length构成
#![allow(unused)] fn main() { use std::slice; use std::str; let story = "Once upon a time..."; let ptr = story.as_ptr(); let len = story.len(); // story has nineteen bytes assert_eq!(19, len); // We can re-build a str out of ptr and len. This is all unsafe because // we are responsible for making sure the two components are valid: let s = unsafe { // First, we build a &[u8]... let slice = slice::from_raw_parts(ptr, len); // ... and then convert that slice into a string slice str::from_utf8(slice) }; assert_eq!(s, Ok(story)); }
创建string
一些在vector中相同的方法同样在String中可用,因为String实际是在Vec<u8>的基础上增加其它保证,限制,能力的封装
#![allow(unused)] fn main() { //比如String同样可用new创建 let mut s = String::new(); }
实现了Displaytrait的可使用to_string创建String,比如下面使用string字面量
#![allow(unused)] fn main() { let data = "initial contents"; let s = data.to_string(); // the method also works on a literal directly: let s = "initial contents".to_string(); }
也可使用String::from创建
#![allow(unused)] fn main() { let s = String::from("initial contents"); }
添加内容
使用push_str或push方法
#![allow(unused)] fn main() { let mut s = String::from("foo"); s.push_str("bar"); s.push('l');//也可添加单个字符 }
可使用+操作符或format!宏连接字符串
#![allow(unused)] fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used }
+操作符使用add方法
#![allow(unused)] fn main() { fn add(self, s: &str) -> String { }
#![allow(unused)] 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}"); }
索引
rust中String不支持索引
#![allow(unused)] fn main() { let s1 = String::from("hello"); let h = s1[0]; //error[E0277]: the type `String` cannot be indexed by `{integer}` }
内部展示
String实际是Vec<u8>的封装
#![allow(unused)] fn main() { //len = 4 byte,每个字符使用1个字节当使用utf8编码 let hello = String::from("Hola"); //貌似是西里尔字母啥的,首字母也不是3,是西里尔字母的Ze //实际len=24 byte,每个字符在utf8编码中用2个字节表示 let hello = String::from("Здравствуйте"); //所以使用0这个索引根本得不到预期的结果 let answer = &hello[0]; }
所以了为避免非预期的结果造成bug,rust编译器会阻止使用索引
rust不允许使用index访问string的最终原因是String不能保证常数时间复杂度(O(1))去索引一个字符,因为rust必须遍历从开始到索引的内容,以确定有多少有效字符。
Slice String
可使用[range]创建字符串slice
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4];//Зд }
但如果只获取字符的一部分时
#![allow(unused)] fn main() { //会报错 let s = &hello[0..1]; // thread 'main' panicked at src/main.rs:4:19: // byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте` }
所以slice stirng时要当心
遍历string的方法
使用chars方法明确指定获取unicode字符
#![allow(unused)] fn main() { for c in "Зд".chars() { println!("{c}"); } //会输出 З д }
或使用bytes方法获取字节
#![allow(unused)] fn main() { for b in "Зд".bytes() { println!("{b}"); } //会输出 208 151 208 180 }
更多可参考string
hash map
HashMap<K, V>使用hash函数存储类型K到V的映射,hash函数决定了K,V如何被存储到内存
创建hash map
#![allow(unused)] fn main() { //首先需要use引入 use std::collections::HashMap; //使用new创建 let mut scores = HashMap::new(); //insert添加元素 scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); }
不同于vector和String是自动预置的,hashmap需要显式引入
访问hash map的值
使用get方法获取值
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name).copied().unwrap_or(0); }
get方法返回一个Option<&V>,所以使用copied方法将Option<&V>转换为Option<V>,再使用unwrap_or方法设置默认值为0若K不存在
使用for遍历
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{key}: {value}"); } //输出顺序是随机的 //Yellow: 50 //Blue: 10 }
注意hashmap迭代的顺序不确定
所有权 ownership
对于实现了Copy trait的类型,例如i32,值会直接copy到hashmap中。但对于String,值会move到hashmap中,hashmap会持有所有权
#![allow(unused)] fn main() { use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); //field_name and field_value 所有权转移到map中 map.insert(field_name, field_value); // field_name and field_value are invalid at this point, try using them and // see what compiler error you get! }
更新hash map
hashmap的k是唯一的,所以插入相同的k会覆盖之前的值
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{scores:?}"); //输出 {"Blue": 25} }
当K不存在时才添加
使用entry方法
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(50); println!("{scores:?}"); //{"Yellow": 50, "Blue": 10} }
entry方法返回一个Entry枚举表明值是否存在,Entry的or_insert方法在K存在时返回一个K对应值的可变引用,不存在则插入并返回可变引用
使用entry比我们自己写逻辑更清晰,和借用检测器(borrow checker)也配合的更好
#![allow(unused)] fn main() { pub enum Entry<'a, K: 'a, V: 'a> { Occupied(OccupiedEntry<'a, K, V>), Vacant(VacantEntry<'a, K, V>), } }
基于旧值更新
#![allow(unused)] fn main() { use std::collections::HashMap; 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;//前面提过or_insert返回的是值得可变引用 } println!("{map:?}"); //{"world": 2, "hello": 1, "wonderful": 1} }
hash函数
hashmap默认使用SipHash算法,可以抵御Denial of Service (DoS)攻击。它不是目前最快的hash算法,但为了更好的安全性而牺牲性能是值得的。
当然你也可以通过指定不同的hasher选择其它算法,hasher是实现BuildHasher trait的类型
更多可参考hash map
总结
vector,string,hash map都存放在heap上
都可以用来存储一个列表的数据,需要根据场景选择合适的集合
错误处理
rust中错误主要有2类
- recoverable error
- unrecoverable error
对于recoverable error,如文件未找到,一般我们提醒用户,然后重试即可 unrecoverable error始终是bug的征兆,比如使用超出范围的索引访问数组,因此程序会直接终止
大多数编程语言并不区分这2种错误,而统一使用异常(Exception))机制来处理。rust没有异常而使用 Result<T, E>来处理recoverable error和当遇到unrecoverable error时使用 panic!宏来停止程序执行
Unrecoverable Errors 和 panic!宏
有两种方式可触发panic
- 导致代码崩溃的操作(如数组访问越界)
- 使用
panic!宏
Unwinding 或 Aborting
默认情况下,当发生panic时,程序会开始 unwinding,即回溯并清理堆栈,然后退出。但是回溯和清理需要大量的时间,因此rust提供立即终止的(aborting)选择,即直接终止程序但不清理
如果你需要项目打包的二进制文件尽可能小,可以选择aborting替代unwinding,在Cargo.toml的 [profile]中添加如下配置
#![allow(unused)] fn main() { [profile.release] panic = 'abort' }
panic!宏
fn main() { panic!("crash and burn"); } //输出 thread 'main' panicked at src/main.rs:2:5: crash and burn note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这个例子输出2行,第一行为线程panic信息和触发panic的位置包括文件名和行号,第二行为panic的消息。在其它实际情况下,文件名和行号可能是调用 panic!宏的位置,而不是实际导致调用 panic!的行,可使用回溯(backtrace)找出导致panic的代码
使用panic! Backtrace
fn main() { let v = vec![1, 2, 3]; //访问超过范围的索引会直接panic v[99]; } //输出 thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
在c语言中,尝试访问超过数据结构范围的索引是未定义行为(undefined behavior),或许会得到内存中与数据结构中的元素相对应的位置,即使这个内存不属于它。这种现象称为buffer overread,而且如果攻击者能够操纵索引来读取数据,就会导致安全漏洞。
使用RUST_BACKTRACE=1环境变量,可以显示调用栈,调用栈包含panic的位置和调用panic的函数的位置
#![allow(unused)] fn main() { $ RUST_BACKTRACE=1 cargo run thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99 stack backtrace: 0: rust_begin_unwind at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5 1: core::panicking::panic_fmt at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14 2: core::panicking::panic_bounds_check at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5 3: <usize as core::slice::index::SliceIndex<[T]>>::index at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:255:10 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:18:9 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/alloc/src/vec/mod.rs:2770:9 6: panic::main at ./src/main.rs:4:6 7: core::ops::function::FnOnce::call_once at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. }
使用调用栈需要enable debug symbol,使用cargo build/run不加--release即可
Recoverable Errors 和 Result
Result<T, E>
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {error:?}"), }; }
File::open返回一个Result<T, E>,若文件存在则返回Ok(std::fs::File)即一个文件句柄,否则返回Err(std::io::Error)
注意类似Option枚举,Result枚举同样自动预置,不必显式使用Result::
匹配不同的错误类型
use std::fs::File; use std::io::ErrorKind; fn main() { let 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:?}"); } }, }; }
使用闭包替代匹配
use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {error:?}"); }) } else { panic!("Problem opening the file: {error:?}"); } }); }
unwrap 和 expect
unwrap方法在Result值为OK时返回T的值,若为Err则调用panic!宏
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
类似unwrap方法,expect方法可以自定义panic!宏的错误消息
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
错误传播(Propagating)
当一个函数可能会出错时,有两种方式处理
- 函数自己处理错误
- 返回错误给调用者,让调用者选择如何处理,这被称为错误传播(propagating errors)
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), //直接返回错误 }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e),//同样返回错误 }//最后一个表达式,所有没有;也不必显式return } }
上述代码并不处理错误的逻辑,而是向上级传播错误,让调用者在合适的机会处理
使用?运算符
因为错误传播非常普遍所以提供了?运算符来简化流程
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
在Result值之后的?运算符
- 如果
Result的值为Ok,则表达式返回Ok中的值,程序继续执行 - 如果
Result的值为Err,Err就像使用return一样直接从整个函数返回。这样错误也就传播给调用者了
?运算符不同于match的点是,若Err中类型实现了From trait,则?运算符会自动调用From trait的from方法来转换类型。当函数定义一种类型去处理所有可能类型的错误时很实用
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; 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) } }
因为从文件中读取字符串是相当常见的操作,所以std::fs模块提供了read_to_string函数
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
何处可用?运算符
只有函数的返回类型和?运算符使用的值一致时才可用
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt")?; } //显然main方法的返回值不是Result<T, E> //error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
从错误输出中可发现
ResultOption- 以及实现了
FromResidual的类型
可以用?运算符处理
类似Result上使用?运算符,Option的?运算符逻辑为
- 如果
Option的值为None,则函数直接返回None - 如果
Option的值为Some,则表达式返回Some中的值,函数继续执行
#![allow(unused)] fn main() { fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } }
虽然main通常返回空元组unit,但也可以返回Result<(), E>
use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; //返回类型一致,?运算符就可用了 Ok(())//返回空元组 }
当main方法返回Result<(), E>时
- 如果
main方法返回Ok(()),则程序结束值为0 - 如果
main方法返回Err,则程序结束值为非0值
c程序在正常结束为0,错误为非0值。rust同样兼容这个约定
To panic! or Not to panic!
例子,原型代码,测试
编写示例代码解释某些概念时,使用unwrap或其它会触发panic!的方法可表示这是一个占位符希望实际实际程序应当处理的错误
编写原型代码时,在你决定如何处理错误之前,使用unwrap和expect很方便。它们留下了清晰了标记供你完善错误处理
测试时使用panic!标记失败
错误处理指南
如果别人调用你的代码传递不合理的参数,你应当返回错误给调用者让其决定如何处理。但如果继续执行,可能会不安全或者有害,这时你应该panic!警示调用者处理其逻辑。同样如果你调用外部代码返回一个你没法处理的状态时,使用panic!是合适的
但如果错误是可预期的,应该返回Result,并在api文档中注明可能发生的错误
总结
- 选择适当的错误处理方式。
panic!或Result - 使用
?运算符让错误处理更简洁直观