数据结构与算法-Rust 版笔记
一、语言入门
1、关键字、注释、命名风格
目前(可能还会增加)39个,注意,Self和self是两个关键字。
Self enum match super
as extern mod trait
async false move true
await fn mut type
break for pub union
const if ref unsafe
continue impl return use
crate in self where
dyn let static while
else loop struct
Rust中各种值推荐的命名风格:
项 约定
包(Crate) snake_case
类型 UpperCamelCase
特性(Trait) UpperCamelCase
枚举 UpperCamelCase
函数 snake_case
方法 snake_case
构造函数 new 或 with_more_details
转换函数 from_other_type
宏 snake_case!
局部变量 snake_case
静态变量 SCREAMING_SNAKE_CASE
常量 SCREAMING_SNAKE_CASE
类型参数 UpperCamelCase 的首字母,如 T、U、K、V
生命周期 lowercase,如 'a、'src、'dest
风格示例:
// 枚举
enum Result<T, E> {
Ok(T),
Err(E),
}
// 特性,trait
pub trait From<T> {
fn from<T> -> Self;
}
// 结构体
struct Rectangle {
height: i32,
width: i32,
}
impl Rectangle {
// 构造函数
fn new(height: i32, width: i32) -> Self {
Self { height, width }
}
// 函数
fn calc_area(&self) -> i32 {
self.height * self.width
}
}
// 静态变量和常量
static NAME: &str = "kew";
const AGE: i32 = 25;
// 宏定义
macro_rules! add {
($a:expr, $b:expr) => {
{
$a + $b
}
}
}
// 变量及宏使用
let sum_of_nums = add!(1, 2);
2、常量
// 定义常量,类似于C/C++中的#define
const AGE: i32 = 1984;
// AGE = 1995; 报错,不允许改变
const NUM: f64 = 233.0;
// const NUM: f64 = 211.0; 报错,已经定义过,不能被覆盖
3、变量
let x: f64 = 3.14; //用let 定义变量x,x可被覆盖,但不允许改变
// x = 6.28; 报错,x 不可变
let x: f64 = 2.71 // 变量x被覆盖
let mut y = 985; // 用let mut定义变量y,y可被覆盖,并且允许改变
y = 996; // 改变y
let y = 2019; // 变量y被覆盖
静态变量:
static NAME: &str = "shieber" // 静态变量可当作常量使用
// NAME = "kew"; 报错,NAME 不允许改变
static mut NUM: i32 = 100; // 静态变量, NUM允许改变
unsafe {
NUM += 1; // 改变NUM
println!("Num:{}",NUM);
}
mut 是约束条件,在变量前加mut后,变量才可以改变,否则不允许改变,这和其他编程语言有很大的不同。
静态变量和常量有相似的地方,但其实它们大不同。
常量在使用时采取内联替换,用多少次就替换多少次;
而静态变量在使用时取一个引用,全局只有一份。
用static mut定义的静态变量需要用unsafe进行包裹,说明这是不安全的,建议你只使用常量和变量,忘记静态变量,以免编码时出现错误。
4、整数
Rust 原生的复合类型:元组(tuple) 和 数组。
长度 有符号 无符号
8 i8 u8
16 i16 u16
32 i32 u32
64 i64 u64
128 i128 u128
arch isize usize
第一,64位机器是如何处理128位的数字的?答案是采用分段存储,通过多个寄存器就能处理。
第二,isize和usize是和机器架构相匹配的整数类型,所以在64位机器上,isize和usize分别表示i64和u64,在32位机器上则分别表示i32和u32。
5、浮点数
长度 有符号
32 f32
64 f64 (默认)
6、布尔类型
布尔类型用bool表示,只有两个值——true和false
7、字符和字符串
字符类型char和C语言中的char是一样的,都是最原始的类型。字符用单引号声明,字符串用双引号声明。下面的c和c_str是完全不同的类型,字符是一个4字节的Unicode标量值,而字符串对应的是数组。
// Unicode 标量值
let c = 's';
// 动态数组
let c_str = "s";
8、元组
元组是一种能够将多个其他类型的值组合成复合值的类型,一旦声明,长度就不能增大或缩小。元组使用圆括号包裹值,并用逗号分隔值。为了从元组中获取值,你可以使用模式匹配和点符号,下标从0开始。
let tup1: (i8, f32, i64) = (-1, 2.33, 8000_0000);
// 使用模式匹配获取所有值
let (x, y, z) = tup1;
let tup2 = (0, 100, 2.4);
let zero = tup2.0; // 使用点符号获取值
let one_hundred = tup2.1;
9、数组
数组中每个元素的类型必须相同,并且长度也不能变。但如果需要可变数组,则可以使用Vec,Vec是一种允许增减长度的集合类型。大部分时候,你需要的数据类型很可能就是Vec。
// 定义数组
let genders = ["Female", "Male", "Bigender"];
let gender_f = genders[0]; // 访问数组元素
let digits[i32; 5] = [0, 1, 2, 3, 4]; //用[type; num]定义数组
let zeros = [0; 10]; // 定义包含10个0的数组
10、转换
Rust不支持原生类型之间的隐式转换,只能使用as关键字进行显式转换。
// type_transfer.rs
#![allow(overflowing_literals)] // 忽略类型转换的溢出警告
fn main() {
let decimal = 61.3214_f32;
// let integer: u8 = decimal; // 报错,不能将f32转成u8
let integer = decimal as u8; // 正确,用as进行显式转换
let character = integer as char;
println!("1000 as a u16: {}", 1000 as u16);
println!("1000 as a u8: {}", 1000 as u8);
}
对于一些复杂的类型,Rust提供了From和Into两个trait来进行转换。
pub trait From<T> {
fn from<T> -> Self;
}
pub trait Into<T> {
fn into<T> -> T;
}
通过这两个trait,你可以按照自己的需求为各种类型提供转换功能。
// integer_to_complex.rs
#[derive(Debug)]
struct Complex {
real: i32, // 实部
imag: i32 // 虚部
}
// 为i32实现到复数的转换功能,将i32转换为实部,虚部置 0
impl From<i32> for Complex {
fn from(real: i32) -> Self {
Self { real, imag: 0 }
}
}
fn main() {
let c1: Complex = Complex::from(2_i32);
let c2: Complex = 2_i32.into(); // 默认实现了Into
println!("c1: {:?}, c2: {:?}", c1, c2);
}
11、所有权、作用域规则、生命周期
Rust比较难学的部分原因就在于其作用域规则。Rust引入这些概念主要是为了应对复杂类型系统中的资源管理、悬荡引用等问题。
所有权系统是Rust中用来管理内存的手段,可以理解成其他语言中的垃圾回收机制或手动释放内存,但所有权系统与垃圾回收机制或手动释放内存有很大的不同。垃圾回收机制在程序运行时不断寻找不再使用的内存来释放,在有些编程语言中,程序员甚至需要亲自分配和释放内存。Rust则通过所有权系统来管理内存,编译器在编译时会根据一系列规则进行检查,倘若违反了规则,程序连编译都通不过。所有权规则很简单,我们可以将其归纳为如下三条:
●每个值都有一个所有者(变量)。
●值在任意时刻都只有一个所有者。
●当所有者离开作用域时,其值将被丢弃(相当于执行垃圾回收)。
Rust的所有权规则还意味着更节省内存,因为Rust程序在运行过程中会不断释放不用的内存,这样后面的变量就可以复用这些释放出来的内存。自然地,Rust程序运行时的内存占用将会维持在一个合理的水平。相反,采用垃圾回收机制或手动释放内存的编程语言会因为变量增加而占用内存,忘记手动释放还会造成内存泄漏。下面结合代码来说明Rust的所有权机制:
fn main() {
let long = 10; <--- long出现在main作用域内
{ // 临时作用域
let short = 5; <--- short出现在临时作用域内
println!("inner short: {}", short);
let long = 3.14; <--- long出现在临时作用域内
println!("inner long: {}", long);
} <--- long和short离开临时作用域,清除
let long = 'a'; <--- long被覆盖
println!("outer long: {}", long);
} <--- long离开main作用域,清除
注意内部的long和外部的long是两个不同的变量,内部的long并不覆盖外部的long。
1、移动
谈及所有权,我们不妨用一个比喻来阐释其含义。你购买了一本书,那么这本书的所有权就属于你。如果你的朋友从其他渠道得知这本书不错,说不定会向你借走看看,此时这本书的所有权还是你的,你的朋友只是暂时持有这本书。当然,如果你已经读完这本书,决定将其送给朋友,那么这本书的所有权就移动到了你的朋友手里。将这样的概念在Rust中推广,就得到了“借用”和“移动”。下面我们结合例子加以具体分析:
fn main() {
let x = "Shieber".to_string(); // 在堆上创建字符串 "Shieber"
let y = x; // x 把字符串移动给了 y
// println!("{x}"); 报错,x 已经不持有字符串了
} <--- y 调用 drop 方法以释放内存
let y = x;”被称为移动,它将所有权移交给了y。
不是离开作用域才释放吗?当使用println输出时,x和y都还在main作用域内,怎么会报错呢?其实,所有权规则的第二条说得很清楚,一个值在任何时候都只能有一个所有者。所以变量x在移动后,立即就被释放了,后面不能再用。x的作用域只到“let y = x;”这一行,而没有到“}”这一行。此外,这种机制还保证了在“}”处只用释放y,不用释放x,从而避免了二次释放这种内存安全问题。为了同时使用x和y,你可以采用下面这种方式。
2、借用
fn main() {
let x = "Shieber".to_string(); // 在堆上创建字符串 "Shieber"
let y = &x; // x把字符串借给了y
println!("{x}"); // x持有字符串,y借用字符串
}
“let y = &x;”被称为借用,“&x”是对x的引用。引用就像指针(地址),可以通过引用来访问存储于该地址的属于其他变量的数据,创建引用就是为了方便别人借用。
借来的书,按理来说是不能做任何改动的。也就是说,借来的书是不可变的。不过,朋友在上面勾画一下也没什么不可。同样,Rust中借用的变量也分为可变变量和不可变变量两种,上面的“let y =&x;”表明y只是从x那里借用了一个不可变变量。如果想要变量可变,就需要为其添加mut修饰符,如下所示:
fn main() {
let x = "Shieber".to_string(); // 在堆上创建字符串 "Shieber"
let y = &mut x; // x把字符串可变地借给了y
y.push_str(", handsome!");
// let z = &mut x; 报错,可变借用只能有一个
println!("{x}"); // x持有字符串,y可变地借用字符串
}
假如你购买了一本电子书,你可能想着复制一份给你的朋友,这样你们两人就各自有了一本电子书,你们拥有各自电子书的所有权。以此类比,这就是Rust中的“拷贝”和“克隆”。
3、克隆
fn main() {
let x = "Shieber".to_string(); // 在堆上创建字符串 "Shieber"
let y = x.clone(); // 克隆字符串给 y
println!("{x}、{y}"); // x 和 y 持有各自的字符串
}
clone函数通过深拷贝复制了一份数据给y。
借用只是获取一个有效指针,速度快;
而克隆需要复制数据,效率更低,而且内存消耗还会增加一倍。
如果你尝试下面的写法,没有用clone函数,就会发现编译通过了,运行也没报错,那么是不是就不满足所有权规则了呢?
fn main() {
let x = 10; // 在栈上创建 x
let y = x;
println!("{x}、{y}"); // 按理来说,x 应该不可用了
}
其实仍满足所有权规则,此时的“let y= x;”并没有把10交给y,而是自动复制了一个新的10给y,这样x和y便各自持有一个10,这和所有权规则并不冲突。这里并没有调用clone函数,但Rust自动执行了clone函数。因为这些简单的变量都在栈上,Rust为这类数据统一实现了一个名为Copy的trait,通过这个trait可以实现快速拷贝,而旧变量x并没有被释放。**在Rust中,数值、布尔值、字符等都实现了Copy这个trait,所以此类变量的移动等于复制。**在这里,调用clone函数的结果是一样的,所以没必要。
4、生命周期:有效指针、无效指针
引用是有效指针,其实还可能有无效指针,其他编程语言中经常出现的悬荡指针就是无效指针,如下所示。
fn dangle() -> &String {
let s = "Shieber".to_string();
&s
}
fn main() {
let ref_to_nothing = dangle();
}
上面的代码在编译时会出现如下类似的报错信息(此处有删减):
error[E0106]: missing lifetime specifier
--> dangle.rs:1:16
1 | fn dangle() -> &String {
| ^ expected named lifetime parameter
| help: function's return type contains a borrowed value,
| but there is no value for it to be borrowed from
| help: consider using the ''static' lifetime
1 | fn dangle() -> &'static String {
| ~~~~~~~~
其实,通过分析代码或报错信息就能发现问题:dangle函数返回了一个无效的引用。
fn dangle() -> &String { <--- 返回无效的引用
let s = "Shieber".to_string();
&s <---- 返回s的引用
} <---- s释放
按照所有权分析,s释放是满足要求的;“&s”是一个指针,返回了似乎也没问题,最多是指针位置无效。s和“&s”是两个不同的对象,所有权系统只能按照三条规则检查数据,但不可能知道“&s”指向的地址实际是无效的。那么编译为何会出错呢?错误信息显示是缺少lifetime specifier,也就是生命周期标记。可见悬荡引用和生命周期是冲突的,所以才报错。也就是说,即使所有权系统通过了,但生命周期不通过,也会报错。
其实,Rust中的每个引用都有生命周期,也就是引用保持有效的作用域。所有权系统并不能保证数据绝对有效,因此需要通过生命周期来确保数据有效。大部分时候,生命周期是隐含的并且可以推断,正如大部分时候类型也可以自动推断一样。当作用域内有引用时,就必须注明生命周期以表明相互间的关系,这样就能确保运行时实际使用的引用绝对是有效的。
fn main() {
let a; // ----------+'a, a 生命周期 'a
// |
{ // |
let b = 10; // --+'b | b 生命周期 'b
// | |
a = &b; // -+ | b' 结束
} // |
// |
println!("a: {}", a); // ----------+ a' 结束
}
a引用了b,a的生命周期比b的生命周期长,所以编译器报错。
要让a正常地引用b,则b的生命周期至少要长到a的生命周期结束才行。
通过比较生命周期,Rust能发现不合理的引用,从而避免悬荡引用问题。
为了合法地使用变量,Rust要求所有数据都带上生命周期标记。生命周期用单引号’加字母表示,置于&后,如&'a、&mut 't。函数中的引用也需要带上生命周期标记。
fn longest<'a>(x: &'a String, y: &'a String) -> &'a String {
if x.len() < y.len() {
y
} else {
x
}
}
因为Rust会自动推断生命周期,所以很多时候可以省略生命周期标记。
12、泛型、trait
// 泛型实现add函数
fn add<T>(x: T, y: T) -> T {
x + y
}
// 泛型实现的枚举
enum Result<T, E> {
Ok(T),
Err(E),
}
// 泛型实现的结构体
struct Point<T> {
x: T,
y: T,
}
let point_i = Point { x: 3, y: 4 };
let point_f = Point { x: 2.1, y: 3.2 };
// let point_m = Point { x: 2_i32, y: 3.2_f32 }; 错误
即便用泛型为add函数实现了一套通用的代码,也还是存在问题。上面的参数T并不能保证就是数字类型,如果一种类型不支持相加,却对其调用add函数,那么必然出错。
要是能限制add函数的泛型参数的类型,只让数字调用就好了。
trait就是一种定义和限制泛型行为的方法,trait里面封装了各类型共享的功能。
利用trait就能很好地控制各类型的行为。
一个trait只能由方法、类型、常量三部分组成,它描述了一种抽象接口,这种抽象接口既可以被类型实现,也可以直接继承其默认实现。比如,定义老师和学生两种类型,它们都有打招呼的行为,于是可以实现如下trait。
trait Greete {
// 默认实现
fn say_hello(&self) {
println!("Hello!");
}
}
// 各自封装自身独有的属性
struct Student {
education: i32, // 受教育年限
}
struct Teacher {
education: i32, // 受教育年限
teaching: i32, // 教书年限
}
impl Greete for Student {}
impl Greete for Teacher {
// 重载实现
fn say_hello(&self) {
println!("Hello, I am teacher Zhang!");
}
}
// 泛型约束
fn outer_say_hello<T: Greete>(t: &T) {
t.say_hello();
}
fn main() {
let s = Student{ education: 3 };
s.say_hello();
let t = Teacher{ education: 20, teaching: 2 };
outer_say_hello(&t);
}
其中,outer_say_hello函数加上了泛型约束T:Greete,表明只有实现了Greete特性的类型T才能调用say_hello函数,这种泛型约束又称为trait bound。前面的add函数如果要用trait bound的话,则应该类似于下面这样。
fn add<T: Addable>(x: T, y: T) -> T {
x + y
}
trait约束还有另一种写法,那就是通过impl关键字来写,如下所示。这样写的意思是,t必须是实现了Greete特性的引用。
fn outer_say_hello(t: &impl Greete) {
t.say_hello();
}
trait可能有多个,参数也可能有多种类型,只需要通过逗号和加号就可以将多个trait bound写到一起。
fn some_func<T: trait1 + trait2, U: trait1> (x: T, y: U) {
do_some_work();
}
为了避免尖括号里写不下多个trait bound,Rust又引入了where语法,以便将trait bound从尖括号里拿出来。
// where 语法
fn some_func<T, U> (x: T, y: U)
where T: trait1 + trait2,
U: trait1,
{
do_some_work();
}
13、枚举
enum Gender {
Male,
Female,
TransGender,
}
要使用枚举,通过“枚举类型::枚举名”就可以了。
let male = Gender::Male;
Rust中常见的枚举类型是Option,其中的Some表示有,None表示无。
enum Option<T> {
Some(T),
None
}
match允许将一个值与一系列模式做比较,并根据匹配的模式执行相应的代码。
enum Cash {
One,
Two,
Five,
Ten,
Twenty,
Fifty,
Hundred,
}
fn cash_value(cash: Cash) -> u8 {
match cash {
Cash::One => 1,
Cash::Two => 2,
Cash::Five => 5,
Cash::Ten => 10,
Cash::Twenty => 20,
Cash::Fifty => 50,
Cash::Hundred => 100,
}
}
match还支持采用通配符和_占位符来进行匹配。
match cash {
Cash::One => 1,
Cash::Two => 2,
Cash::Five => 5,
Cash::Ten => 10,
other => 0, // _ => 0,
}
14、函数式编程、迭代器
Rust不像面向对象编程语言那样喜欢通过类来解决问题,而是推崇函数式编程。
函数式编程是指将函数作为参数值或其他函数的返回值,在将函数赋值给变量之后执行。
函数式编程有两个极为重要的构件,分别是闭包和迭代器。
闭包是一种可以保存变量或作为参数传递给其他函数使用的匿名函数。
闭包可以在一处创建,然后在不同的上下文中执行。
不同于函数,闭包允许捕获调用者作用域中的值,闭包特别适合用来定义那些只使用一次的函数。
// 定义普通函数
fn function_name(parameters) -> return_types {
code_body;
return_value
}
// 定义闭包
|parameters| {
code_body;
return_value
}
闭包也可能没有参数,同时返回值也可写可不写。实际上,Rust会自动推断闭包的参数类型和返回值类型,所以参数和返回值的类型都可以不写。为了使用闭包,你只需要将其赋值给变量,然后像调用函数一样调用它即可。比如定义如下判断奇偶的闭包。
let is_even = |x| { 0 == x % 2 };
let num = 10;
println!("{num} is even: {}", is_even(num));
闭包可以使用外部变量。
let val = 2;
let add_val = |x| { x + val };
let num = 2;
let res = add_val(num);
println!("{num} + {val} = {res}")
此处,闭包add_val捕获了外部变量val。
闭包捕获外部变量可能是为了获取所有权,也可能是为了获取普通引用或可变引用。
针对这三种情况,Rust专门定义了三个trait:FnOnce、FnMut和Fn。
●FnOnce会消费从周围作用域捕获的变量,也就是说,闭包会获取外部变量的所有权并在定义闭包时将其移进闭包内。Once代表这种闭包只能被调用一次。
●FnMut会获取可变的借用值,因此可以改变其外部变量。
●Fn则获取不可变的借用值。这里的FnOnce、FnMut和Fn相当于实现了所有权系统的移动、可变引用和普通引用。
由于所有闭包都可以被调用至少一次,因此所有闭包都实现了FnOnce。那些没有移动变量所有权到闭包内而只使用可变引用的闭包,则实现了FnMut。不需要对变量进行可变访问的闭包实现了Fn。
如果希望强制将外部变量所有权移动到闭包内,那么可以使用move关键字。
let val = 2;
let add_val = move |x| { x + val };
// println!("{val}"); 报错,val 已被移动到闭包 add_val 内。
下面展示了三种不同类型迭代器的用法。
如果只读取值,那就实现iter();如果还需要改变原始数据,那就实现iter_mut();如果要将原始数据直接转换为迭代器,那就实现into_iter(),以获取原始数据所有权并返回一个迭代器。
let nums = vec![1,2,3,4,5,6];
// 不改变nums中的值
for num in nums.iter() { println!("num: {num}");
println!("{:?}", nums); // 还可再次使用nums
// 改变nums中的值
for num in nums.iter_mut() { *num += 1; }
println!("{:?}", nums); // 还可再次使用nums
// 将nums转换为迭代器
for num in nums.into_iter() { println!("num: {num}"); }
// println!("{:?}", nums); 报错,nums已被迭代器消费
消费是迭代器上的一种特殊操作,其主要作用就是将迭代器转换成其他类型的值而非另一个迭代器。
sum、collect、nth、find、next和fold都是消费者,它们会对迭代器执行操作,得到最终值。
既然有消费者,就必然有生产者。Rust中的生产者就是适配器,适配器的作用是对迭代器进行遍历并生成另一个迭代器。
take、skip、rev、filter、map、zip和enumerate都是适配器。按照此定义,迭代器本身就是适配器。
// adapter_consumer.rs
fn main() {
let nums = vec![1,2,3,4,5,6];
let nums_iter = nums.iter();
let total = nums_iter.sum::<i32>(); // 消费者
let new_nums: Vec<i32> = (0..100).filter(|&n| 0 == n % 2)
.collect(); // 适配器
println!("{:?}", new_nums);
// 求小于1000的能被3或5整除的所有整数之和
let sum = (1..1000).filter(|n| n % 3 == 0 || n % 5 == 0)
.sum::<u32>(); // 结合适配器和消费者
println!("{sum}");
}
因此,建议你多利用闭包结合迭代器、适配器、消费者进行函数式编程。
15、智能指针
指针是包含内存地址的变量,用于引用或指向其他的数据。
智能指针则是一种数据结构,其行为类似于指针,含有元数据,在大部分情况下拥有指向的数据,提供内存管理或绑定检查等附加功能,如管理文件句柄和网络连接。Rust中的Vec、String都可以看作智能指针。
Rust语言为智能指针封装了两大trait——Deref和Drop,当变量实现了Deref和Drop后,就不再是普通变量了。
实现Deref后,变量重载了解引用运算符“*”,可以当作普通引用来使用,必要时可以自动或手动实现解引用。
实现Drop后,变量在超出作用域时会自动从堆中释放,当然还可自定义实现其他功能,如释放文件或网络连接。
智能指针的特征:
●智能指针在大部分情况下具有其所指向数据的所有权。
●智能指针是一种数据结构,一般使用结构体来实现。
●智能指针实现了Deref和Drop两大trait。
●Box是一种独占所有权的智能指针,指向存储在堆上且类型为T的数据。
●Rc是一种共享所有权的计数智能指针,用于记录存储在堆上的值的引用数。
●Arc是一种线程安全的共享所有权的计数智能指针,可用于多线程。
●Cell是一种提供内部可变性的容器,不是智能指针,允许借用可变数据,编译时检查,参数T要求实现Copy trait。●RefCell也是一种提供内部可变性的容器,不是智能指针,允许借用可变数据,运行时检查,参数T不要求实现Copy trait。●Weak是一种与Rc对应的弱引用类型,用于解决RefCell中出现的循环引用。
●Cow是一种写时复制的枚举体智能指针,我们使用Cow主要是为了减少内存分配和复制,Cow适用于读多写少的场景。
// 自定义元组结构体
struct SBox<T>(T);
impl<T> SBox<T> {
fn new(x: T) -> Self {
Self(x)
}
}
fn main() {
let x = 10;
let y = SBox::new(x);
println!("x = {x}");
// println!("y = {}", *y); 报错,*y不能解引用
} <--- x和y自动调用drop方法以释放内存,只是无输出,看不出来
为SBox实现Deref和Drop:
use std::ops::Deref;
// 为SBox实现Deref,自动解引用
impl<T> Deref for SBox<T> {
type Target = T; // 定义关联类型,也就是解引用后的返回值类型
fn deref(&sefl) -> &Self::Target {
&self.0 // .0表示访问元组结构体SBox<T>(T)中的T
}
}
// 为SBox实现Drop,添加额外信息
impl<T> Drop for SBox<T> {
fn drop(&mut self) {
println("SBox drop itself!"); // 只输出信息
}
}
使用:
fn main() {
let x = 10;
let y = SBox::new(x);
println!("x = {x}");
println!("y = {}", *y); // *y相当于*(y.deref())
// y.drop(); 主动调用会造成二次释放,所以报错
} <--- x和y自动调用了 drop方法,y在自动调用drop方法时会输出 SBox drop itself!
数据存储在堆上,实现Deref后,可以自动解引用。
fn main() {
let num = 10; // num存储在堆上
let n_box = Box::new(num); // n_box存储在堆上
println!("n_box = {}", n_box); // 自动解引用堆上的数据
println!("{}", 10 == *n_box); // 解引用堆上的数据
}
所有权系统的规则规定了一个值在任意时刻只能有一个所有者,但在有些场景下,我们又需要让值具有多个所有者。为了应对这种情况,Rust提供了Rc智能指针。Rc是一种可共享的引用计数智能指针,能产生多所有权值。引用计数意味着通过记录值的引用数来判断值是否仍在使用。如果引用数是0,就表示值可以被清理。
如下图,3被变量a(1)和b(2)共享。共享就像教室里的灯,最后离开教室的人负责关灯。同理,在Rc的各个使用者中,只有最后一个使用者会清理数据。克隆Rc会增加引用计数,就像教室里新来了一个人一样。
use std::rc::Rc;
fn main() {
let one = Rc::new(1);
let one_1 = one.clone(); // 增加引用计数
println!("sc:{}", Rc::strong_count(one_1)); // 查看计数
}
**Rc可以共享所有权,但只能用于单线程。如果要在多线程中使用,Rc就不行了。为解决这一问题,Rust提供了Rc的线程安全版本Arc(原子引用计数)。**Arc在堆上分配了一个共享所有权的T类型值。在Arc上调用clone函数会产生一个新的Arc,它指向与原Arc相同的堆,同时增加引用计数。Arc默认是不可变的,要想在多个线程间修改Arc,就需要配合锁机制,如Mutex。
Rc和Arc默认不能改变内部值,但有时修改内部值又是必需的,所以Rust提供了Cell和RefCell两个具有内部可变性的容器。内部可变性是Rust中的一种设计模式,允许在拥有不可变引用时修改数据,但这通常是借用规则所不允许的。为此,该模式使用unsafe代码来绕过Rust可变性和借用规则。
Cell相当于在不可变结构体Fields上开了一个后门,从而能够改变内部的某些字段。
// use_cell.rs
use std::cell::Cell;
struct Fields {
regular_field: u8,
special_field: Cell<u8>,
}
fn main() {
let fields = Fields {
regular_field: 0,
special_field: Cell::new(1),
};
let value = 10;
// fields.regular_field = value; 错误:Fields 是不可变的
fields.special_field.set(value);
// 尽管Fields不可变,但special_field是一个 Cell
// 而 Cell 的内部值可被修改
println!("special: {}", fields.special_field.get());
}
RefCell相比Cell多了前缀Ref,所以RefCell本身具有Cell的特性,RefCell与Cell的区别和Ref有关。RefCell不使用get和set方法,而是直接通过获取可变引用来修改内部数据。
// use_refcell.rs
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;
fn main() {
let shared_map: Rc<RefCell<_>> =
Rc::new(RefCell::new(HashMap::new()));
{
let mut map: RefMut<_> = shared_map.borrow_mut();
map.insert("kew", 1);
map.insert("shieber", 2);
map.insert("mon", 3);
map.insert("hon", 4);
}
let total: i32 = shared_map.borrow().values().sum();
println!("{}", total);
}
在这里,shared_map通过borrow_mut( )直接得到了类型为RefMut<>的map,然后直接通过调用insert方法往map里添加元素,这就修改了shared_map。RefMut<>是对HashMap的可变借用,通过RefCell可以直接修改其值。
Rust本身提供了内存安全保证,这意味着很难发生内存泄漏,然而RefCell有可能造成循环引用,进而导致内存泄漏。为防止循环引用,Rust提供了Weak智能指针。
Rc每次克隆时都会增加实例的强引用计数strong_count的值,只有strong_count为0时实例才会被清理。循环引用中的strong_count永远不会为0。
而Weak智能指针不增加strong_count的值,而是增加weak_count的值。weak_count无须为0就能清理数据,这样就解决了循环引用问题。
正因为weak_count无须为0就能清理数据,所以Weak引用的值可能会失效。为确保Weak引用的值仍然有效,你可以调用它的upgrade方法,这会返回Option<Rc>。
如果值未被丢弃,结果将是Some;如果值已被丢弃,结果将是None。下面展示了一个使用Weak解决循环引用的例子,Car和Wheel存在相互引用,如果都用Rc,就会出现循环引用。
// use_weak.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Car {
name: String,
wheels: RefCell<Vec<Weak<Wheel>>>, // 引用 Wheel
}
struct Wheel {
id: i32,
car: Rc<Car>, // 引用Car
}
fn main() {
let car: Rc<Car> = Rc::new(
Car {
name: "Tesla".to_string(),
wheels: RefCell::new(vec![]),
}
);
let wl1 = Rc::new(Wheel { id:1, car: Rc::clone(&car) });
let wl2 = Rc::new(Wheel { id:2, car: Rc::clone(&car) });
let mut wheels = car.wheels.borrow_mut();
// downgrade,得到Weak
wheels.push(Rc::downgrade(&wl1));
wheels.push(Rc::downgrade(&wl2));
for wheel_weak in car.wheels.borrow().iter() {
let wl = wheel_weak.upgrade().unwrap(); // Option
println!("wheel {} owned by {}", wl.id, wl.car.name);
}
}
Cow的存在就是为了减少复制,提高效率。
假如要过滤字符串中的所有空格:
// use_cow.rs
use std::borrow::Cow;
fn delete_spaces2<'a>(src: &'a str) -> Cow<'a, str> {
if src.contains(' ') {
let mut dest = String::with_capacity(src.len());
for c in src.chars() {
if ' ' != c { dest.push(c); }
}
return Cow::Owned(dest); // 获取所有权,dest被移出
}
return Cow::Borrowed(src); // 直接获取src的引用
}
// use_cow.rs
fn main() {
let s = "i love you";
let res1 = delete_spaces(s);
let res2 = delete_spaces2(s);
println!("{res1}, {res2}");
}
16、异常处理
Rust并没有像其他编程语言那样提供try catch这样的异常处理方法,而是提供了一套独特的异常处理机制。
Rust中的异常有4种,分别是Option、Result、Panic和Abort。
Option用于应对可能的失败情况,Rust用有(Some)和无(None)来表示是否失败。比如获取某个值,但如果没有获取到,得到的结果就是None,这时不应该报错,而是应该依据情况进行处理。失败和错误不同,前者是符合设计逻辑的,也就是说,失败本来就是可能的,所以失败不会导致程序出问题。下面是Option的定义。
enum Option<T> {
Some(T),
None,
}
Result用于应对可恢复错误,Rust用成功和失败来表示是否有错。出错不一定导致程序崩溃,但需要进行专门处理,以使程序继续执行。下面是Result的定义。
enum Result<T,E> {
Ok(T),
Err(E),
}
17、宏定义
Rust中并不存在内置库函数,一切都需要自己定义。但是Rust实现了一套高效的宏,包括声明宏、过程宏,利用宏能完成非常多的任务。
比如,使用derive宏可以为结构体添加新的功能,常用的println!、vec!、panic!等也是宏。
Rust中的宏有两大类:一类是使用macro_rules!声明的声明宏;另一类是过程宏,过程宏又分为三小类——derive宏、类属性宏和类函数宏。
声明宏的格式:macro_name!()、macro_name![]、macro_name!{}。首先是宏名,然后是感叹号,最后是()、[]或{}。这些括号都可用于声明宏,但不同用途的声明宏使用的括号是不同的,比如是vec![]而不是vec!(),带“()”的更像是函数,这也是println!()使用“()”的原因。不同的括号只是为了满足意义和形式上的统一,实际使用时任何一种都可以。
macro_rules! macro_name {
($matcher) => {
$code_body;
return_value
};
}
18、代码组织及包依赖关系
Rust里面存在包、库、模块、箱(crate)等说法,并且都有对应的实体。应该说,在Rust中,用cargo new生成的就是包,一个包里有多个目录,一个目录可以看成一个crate。一个crate在经过编译后,可能是一个二进制可执行文件,也可能是一个供其他函数调用的库。一个crate里往往有很多“.rs”文件,这些文件被称为模块,使用这些文件或模块时需要用到use命令。下面展示了Rust中的代码组织方式以及对应的各种概念。
\begin{lstlisting}[style=styleRes]
package --> crates (dirs) 一个包里有多个crate(dir)
crate --> modules (lib/EFL) 一个crate包含多个模块
这个crate可编译成库或可执行文件
module --> file.rs (file) 模块包含一个或多个“.rs”文件
package <-- 包
├── Cargo.toml
├── src <-- crate
│ ├── main.rs <-- 模块,主模块
│ ├── lib.rs <-- 模块,库模块(可编译成库或可执行文件)
│ └── math <-- 模块,数学函数模块math
│ ├─ mod.rs <-- 模块,为math模块引入add和sub函数
│ ├─ add.rs <-- 模块,实现math模块的add函数
│ └─ sub.rs <-- 模块,实现math模块的sub函数
└── file <-- crate
├── core <-- 模块,文件操作模块
└── clear <-- 模块,清理模块
Rust 提供的一些标准库:
alloc env i64 pin task
any error i128 prelude thread
array f32 io primitive time
ascii f64 isize process u8
borrow ffi iter raw u16
boxed fmt marker mem u32
cell fs net ptr u64
char future num rc u128
clone hash ops result usize
cmp hint option slice vec
collections i8 os str backtrace
convert i16 panic string intrinsics
default i32 path sync lazy