Rust学习(自学)
一、常见编程概念
1.变量
1.1.变量的可变性
创建一个变量时,变量类型为“let”关键字时,则值不可改变,当变量类型为“let mut”这两个关键字时,变量值可以改变。
let x = 5; // 变量“x”的类型为“let”时,变量“x”不可变,为常量;
let mut x = 5; // 变量“x”的类型为“let mut”时,变量“x”可变,可为“x”重新赋值;
1.2.常量
创建常量时,通常使用“const”关键字,并且常量名大写,常量名单词之间用下划线连接,而且创建常量要表明数值类型
const X_Y:u32 = 10; // 关键字“const” 变量名单词之间用下划线连接,且要指定数值类型;
1.3.隐藏(重复命名)
重复使用“let”关键字可以对同一变量名进行重复赋值,在rust中,这种方式相当于重新创建了一个相同名字的变量,而且在局部中,用完即刻消除。此方法与使用“mut”关键字不同。使用“let”关键字始终保持变量是不可变的。
fn main() {let x = 5;println!("x的值为{}",x); // x =5;let x = x + 9;{let x = x * 8;println!("x的值为{x}"); // x = 112;}println!("x的值为{x}"); // x = 14;let space = " ";let space= space.len();println!("space的值为{space}") // space的值为 19(空格数量)
}
2.数据类型
2.1.标量类型(整型、浮点型、布尔类型、字符类型)
整型:
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
每个有符号数是到
,n为位数,如i8范围
到
;
每个无符号数是0到;
isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的;
Rust中默认类型为i32;
浮点型:
Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64;
f32 是单精度浮点数,f64 是双精度浮点数;
let f = 2.0; // f64
let f:f32 = 3.0 // f32
数值计算:
fn main() {let a = 5 + 10; // 15(i32)let b = 10f64 - 5.1; // 4.9(f64)let c = 3.0 * 5f32; // 15(f32)let d = 10 / 3; // 3(i32)let e = 10 % 3; // 1(i32)
}
布尔类型:
Rust 中的布尔类型有两个可能的值:true 和 false;
fn main() {let a = true;let b = false;
}
字符类型:
Rust的 char 类型是语言中最原生的字母类型;
我们用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量;
Rust 的 char 类型的大小为四个字节,并代表了一个 Unicode 标量值;
fn main() {let a = 'a'; // a是一个字符类型变量
}
2.2.复合类型
Rust 有两个原生的复合类型:元组(tuple)和数组(array)
元组类型:
定义:元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小;
元组书写:使用包含在圆括号中的逗号分隔的值列表来创建一个元组;
fn main() {let tup:(i32, f64, u8) = (48, 7.61, 17);
}
可以把元组给多个变量赋值
fn main() {let a = (15.165, 100, -39);let (x, y, z) = a;println!("y的值为{}", y); // 100
}
可以用 元组名.n(n为自然数:0,1,2...)获取元组中某一个元素
fn main() {let tup:(i32, f64, u8) = (48, 7.61, 17);let tup_first = tup.0; // 48let tup_second = tup.1; // 7.61let tup_third = tup.2; // 17
}
注:不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型;
数组类型:
一个包含多个相同类型元素的复合类型方式,Rust中的数组长度是固定的;
数组书写1:将数组的值写成在方括号内,用逗号分隔;
fn main(){let b = [1, 2, 3, 4]; // 默认是i32类型 长度为4
}
数组的书写2:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量;
fn main(){let b:[i8; 6] = [15, 98, 3, -33, 26, 0]; // i8 为数组中的元素类型,6 为数组中的元素个数
}
数组书写3:通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组
fn main(){let c = [5; 8]; // 5 代表数组中的每个元素都是“5”,8 代表一共有8个元素
}
可以使用 数组名[n](n为自然数:0,1,2...)来获取数组中的每个元素
fn main() {let a = [1, 2, 3, 4];let a_first = a[0]; // 1let a_second = a[1]; // 2let a_third = a[2]; // 3let a_fourth = a[3]; // 4
}
注:在获取数组中的元素时,获取的数组元素所在位置超过数组中元素个数时会报错;如上面如果获取a[4]则会报错,因为他就4个元素,且从0开始的。
3.函数
定义:在Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数。大括号告诉编译器哪里是函数体的开始和结尾;如main方法
fn main() {}
fn main() {another_function();
}
fn another_function(){println!("这是另一个方法");
}
3.1.参数
定义:参数是特殊变量,一般等号左边的叫形参,等号右边的有实际值的叫实参
在函数中,必须声明每个参数的类型,写法一般为在方法的括号中写 参数名:参数类型,当有多个参数时,一般用逗号分隔;
fn main() {another_function(10,'q'); // x和y的值为10q
}
fn another_function(x:i32, y:char){println!("x和y的值为{x}{y}");
}
3.2.语句和表达式
语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。
let x = 6; // 这是一个语句
let y = (let x = 5); // 这不是语句,会报错,不能这样赋值
函数调用是一个表达式。宏调用是一个表达式。用大括号创建的一个新的块作用域也是一个表达式 ,如:
fn main() {let b = {let a = 10;a + 10};println!("b的值为{b}") // b的值为20
}
作用域中执行的代码,最终的结果会被赋值给b,当最后一行结尾处没有分号时,会把结果返回给b,当加了分号最后一行就变成了一个语句了,不会再有返回值
具有返回值的函数
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->)后声明它的类型,在 Rust 中,函数的返回值等同于函数体最后一个表达式的值(在不使用return等关键字提前返回的情况下)
fn main() {let m = one_function(10);println!("m的值为{m}"); // m的值为20
}
fn one_function(x:i32)->i32{x + 10
}
在有返回值的函数中,函数体里的最后一行结尾没有分号,表示有返回值,带分号后则无返回值;
“->i32”表示返回值的类型为i32;
4.控制流
4.1.if判断表达式
if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”
use std::io;
use rand::Rng;fn main() {let secret_number = rand::thread_rng().gen_range(1..10); // 使用此方法需要在Cargo.toml文件中dependencies下面添加 rand = "0.8.3" 依赖loop {println!("请输入你要输入的数字:");let mut x = String::new();io::stdin().read_line(&mut x).expect("输入异常");let x:i32 = x.trim().parse().expect("转换异常");if x > secret_number {println!("您输入的结果大了");}else if x < secret_number {println!("您输入的结果小了")}else {println!("恭喜您输入正确");break;}}
}
else if 用来处理多个条件时,else是当前面所有条件都不满足时用
if判断语句也可以赋值
fn main() {let boolean = true;let a = if boolean {10 } else { 0 };
}
当满足if条件时 a = 10, 当不满足时,a = 0; 注意:if和else分支内的值类型应该相同;
4.2.使用循环重复执行
多次执行同一段代码为循环;Rust 有三种循环:loop、while 和 for;
loop循环:
fn main() {loop{println!("我的测试");}
}
该方式不能自动停止,只能手动停止,否则处于loop循环中的代码会一直打印;
其实我们可以使用一个关键字:break 来跳出循环:
fn main() {let mut x = 0;'out_loop:loop{println!("x的值为:{x}");let mut y = 10;loop {println!("y的值为{y}");if y < 8 {break;}if x > 3 {break 'out_loop;}y -= 1;}x += 1}println!("x的最终值:{x}");
}
代码中内层循环里y的值一直在减小,当if条件 y<8成立时,则进入执行语句 break 跳出内层循环。
'out_loop 为循环标签 前面一个单引号跟着这层循环的变量名然后 冒号 loop循环;
循环标签一般喜欢和break或continue一起使用,用于跳出指定循环;如代码中,当满足
x>3时,进入执行语句 break 'out_loop; 执行后,可以跳出带有循环标签的循环;
while循环:
fn main() {let mut a = 6;while a != 0 {println!("我的测试");a -= 1;}
}
while循环:当条件不满足时,则停止循环。
for循环:
一般用于遍历集合,不能遍历元组。
fn main() {let x = [1, 2, 3, 4, 5, 6];for element in x {println!("x中的元素有:{element}");}
}
fn main() {for number in (1..4).rev() { // (1..4)左包含右不包含println!("{number}!"); // 输出为:3, 2, 1}println!("LIFTOFF!!!");
}
.rev()方法为翻转,即倒序执行
二、认识所有权
1.什么是所有权
1.1.String类型浅识
fn main() {let mut s = String::from("hello");s.push_str(" world");println!("{s}");
}
String::from("hello") 把括号中的内容转换成String类型, ::是运算符s.push_str() 是s的值后面拼接上 .push_str()方法中括号里的字符串。
注:字符串类型通过“=”赋值给另一个变量后则不能再使用,rust默认赋值后该变量不会再使用,否则会报错(数据存储在堆中的数据都是这种性质)
fn main() {let mut s = String::from("hello");s.push_str(" world");let s1 = s;println!("{s}"); // 会报错,通过s1 = s这样方式的赋值后,s将不能再次使用
}
如果想赋值后还可以使用可以使用clone()方法
fn main() {let mut s = String::from("hello");s.push_str(" world");let s1 = s.clone();println!("{s}"); // 输出:hello world
}
重点:因为String类型的这样的字符串变量,值是放在堆空间的,而标量类型的数据都是放在栈空间,可以随便赋值
fn main() {{let x = 554;let y = x;println!("{x}"); // 554}
}
1.2. 所有权与函数
在函数中的所有权与语句中的使用相同
fn main() {// 所有权与函数let s1 = String::from("测试一下");test_s1(s1);// println!("{}", s1); // 这里会报错,因为s1已经被消除let s2 = 54;test_s2(s2);println!("{}", s2); // 54let s3 = 'a';test_s3(s3);println!("{}", s3); // a
} // 此处s1,s2,s3均被移除作用域,s2已经在方法中使用后就被移除fn test_s1(some_string :String){println!("{}",some_string); // 测试一下
}fn test_s2(x: i32){println!("{}",x); // 54
}fn test_s3(ch: char){println!("{}",ch); // a
}
1.3. 返回值与作用域“”
fn main() {let s1 = test_s1();println!("{}", s1); // 测试2let s2 = String::from("测试1");let s3 = test_s2(s2);println!("{}", s3); // 测试1
}fn test_s1() -> String{let ss = String::from("测试2");
ss
}fn test_s2(a_string : String) -> String{a_string
}
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
2.引用与借用
2.1. 引用
引用:在其他地方使用该变量后,变量不会失去所有权; 在变量前面加 “&”符号表示引用;创建引用的行为叫借用。
fn main() {let s = String::from("这是个测试");let len = test_s(& s); // 变量加上 & 后表示引用,变量不会在此处失去所有权println!("{}", len); // 15println!("{}", s); // 这是个测试println!("{}", len); // 15fn test_s(s : &String) -> usize{s.len()}
}
2.2. 可变引用
fn main() {let mut s = String::from("这是个测试");test_s(&mut s); // &mut 变量名,即可对可变变量引用println!("{}", s); // 这是个测试1fn test_s(s: &mut String) {s.push_str("1");}
}
注意:不能同时创建两个变量的可变引用,因为不允许同时对一个变量进行操作,可以在前一个可变引用的变量的所有权失去后才可以进行第二次变量的可变引用。
fn main() {let mut s = String::from("hello");let r1 = &mut s;println!("{}", r1); // 在此处不会报错,只有当r1失去所有权后,s才可以进行第二次被可变引用let r2 = &mut s; // 会报错// println!("{}", r1); 在此位置 r2 会报错println!("{}, {}", r1, r2);
}
变量可以同时被多次非可变的引用,但是在引用该变量的变量的所有权失去之前不允许再次创建变量的可变引用
fn main() {let mut s = String::from("hello");let r1 = &s; // 没问题let r2 = &s; // 没问题let r3 = &mut s; // 大问题println!("{}, {}, and {}", r1, r2, r3);
}
引用规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
3. Slice类型
例子:返回一个字符串中第一个空格之前的单词
fn main() {let mut s = String::from("hello world");let i = get_first_word(&s);}
fn get_first_word(s : &String) ->usize{let tuple = s.as_bytes();for (i, &item) in tuple.iter().enumerate(){if item == b' ' {return i;}}s.len()
}
s.as_bytes() 可以把一串字符串转换成字节元组类型
tuple.iter() 创建一个迭代器,遍历元组
enumerate() 可以把遍历的元组的每个元素进行包装,同时返回该元素的内容和索引
b' ' 表示空格对应的unicode值(u8类型)
上面代码中,如果在后面再执行s.clear(),这样虽然 i 有效,但是却没什么用了,因为s已经被清空。
3.1.字符串slice
字符串 slice(string slice)是 String 中一部分值的引用,他看起来像:
fn main() {let s = String::from("hello world");let hello = &s[0..5];let world = &s[6..11];
}
&变量名[变量起始索引..变量结尾索引] 为字符串slice; 其中索引为字符串变量中每个字符在字符串变量中的位置;
如果开始为0,如&s[0..5],则0可以省略 简写成 &s[..5]
如果结尾为字符串变量的最后一位,即字符串变量的长度,如 &[6..11](假定11为字符串变量长度) &s[6..len](len为字符串长度),则末尾索引也可以省略,简写成&s[6..]
如果是首尾则都可以简写 &s[..]
上面代码改写:
fn main() {let s = String::from("hello world");let _first_word = get_first_word(&s);println!("{}", _first_word);}
fn get_first_word(s : &String) ->&str{let tuple = s.as_bytes();for (i, &item) in tuple.iter().enumerate(){if item == b' ' {return &s[..i];}}&s[..]
}
通过这样就可以返回一段字符串变量
字符串字面值就是slice
#![allow(unused)]
fn main() {
let s = "Hello, world!"; // 字符串字面值
}
字符串slice也可作为参数
fn main() {let s = String::from("hello world");let _first_word = get_first_word(&s);println!("{}", _first_word);}
fn get_first_word(s : &str) ->&str{ // s:&str 字符串slice作为参数let tuple = s.as_bytes();for (i, &item) in tuple.iter().enumerate(){if item == b' ' {return &s[..i];}}&s[..]
}
3.2.其他类型slice
数组型
fn main() {let array = [1, 2, 3, 4, 5];let slice = &array[..2];assert_eq!(slice, &[1,2]); // 断言,判断两边相等 左边slice = [1, 2], 右边[1, 2]
}
还有其他类型,这里不再列出,后面会学到。
三、使用结构体组织相关联的数据
1.结构体的定义和实例化
1.1. 结构体初识及实例化
定义一个结构体:首先 添加一个struct的关键字,后面紧跟结构体名(首字母大写),然后用大括号把他每一部分数据的名字及数据类型按数据名:类型方式在大括号中写,在大括号中每个数据名,我们叫他字段
struct User{user_name: String,sex: String,age: u64,
}
结构体实例化:创建结构体实例,需要为结构体中每个字段赋值具体的值,类似于key:value格式,key—结构体字段名,value—具体数据值,然后然后赋值给一个变量
struct User{user_name: String,sex: String,age: u64,
}fn main() {let user1 = User{user_name: String::from("霸亚磊"),sex: String::from("男"),age: 27,};
}
上面代码中user1为一个User结构体实例
通过函数返回结构体实例,只需要把返回值类型设置为结构体类型即可
fn get_user(user_name: String) -> User{User {user_name: user_name,sex: String::from("男"),age: 17,}
}
函数返回结构体实例化时,字段初始化简写:当函数的的参数名与结构体字段名相同时,可以直接简写成参数名
fn get_user(user_name: String) -> User{User {// user_name: user_name,user_name, // 上面方式的简写sex: String::from("男"),age: 17,}
}
1.2.从其他实例创建实例
如果要创建的新实例与另一个实例中有某些字段值相同,则可以使用别的别的实例创建新实例
struct User{user_name: String,sex: String,age: u64,
}fn main() {let user1 = User{user_name: String::from("霸亚磊"),sex: String::from("男"),age: 27,};let user2 = User{user_name: String::from("霸亚磊2"),..user1};}
如代码中user2,把和另一个实例不同的字段放在上面,并且赋值,其他和另一个实例相同的字段则可以在最后一行添加 ..另一个实例名,一定要加在最后一行,这样就可以创建一个新实例
1.3.其他类型结构体
元组类型结构体
struct Color(i32, i32, char, String, f64);fn main() {let color1 = Color(88, 864523, 'g', String::from("测试"), 5.56);
}
注:如果是两个类型相同,但结构体名的不同的两个结构体是不能共用。
单元结构体
struct Unit;
fn main() {let unit1 = Unit;
}
具体用处后续学习。
2.结构体示例程序
// 计算一个矩形的面积
#[derive(Debug)]
struct Rectangle {width: u32,height: u32
}
fn main() {let scale = 2;let rectangle = Rectangle { width: dbg! (60 * scale), // [src/main.rs:10] 60 * scale = 120height: 50 };println!("矩形的面积是{}", area(&rectangle)); // 矩形的面积是6000println!("矩形的信息是{:?}", rectangle); // 矩形的信息是Rectangle { width: 120, height: 50 }println!("矩形的信息是{:#?}", rectangle); /*矩形的信息是Rectangle {width: 120,height: 50,} */dbg!(&rectangle);/*[src/main.rs:15] &rectangle = Rectangle {width: 120,height: 50,} */
}fn area(rectangle: &Rectangle) -> u32 {rectangle.width * rectangle.height
}
通过通过实例练习结构体,想要打印结构体实例的具体信息时,可以在结构体上上面添加: #[derive(Debug)]
然后打印语句写 {:?} (在一行打印结构体的实例信息),{:#?} (按照结构体的格式打印结构体实例的信息)
dbg! 宏:打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权
3. 方法语法
3.1. 定义方法和使用
方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,),并且它们第一个参数总是 self,它代表调用该方法的结构体实例。
// 结构体方法
#[derive(Debug)]
struct Rectangle {width:u32,height:u32,
}
// 结构体的方法
impl Rectangle {fn area(&self) -> u32 {self.width * self.height}fn width(&self) -> bool {self.width > 0}
}
fn main() {let rectangle = Rectangle{width: 30,height: 50,};println!("矩形的面积是{}", rectangle.area());println!("矩形的宽是不是大于零:{}", rectangle.width())
}
定义:使用 impl 关键字定义,后面是结构体名,然后是{},在使用impl关键字块中定义的“函数”,就是结构体的方法;里面的每个方法的第一个参数都是 self,在方法中获取结构体自身的字段可以写成self.xxx ; 这里的&self等效于 rectangle: &Rectangle 也是 self &Self写法
使用:创建一个结构体实例,然后可以用 实例名.方法名 的方式调用结构体的方法。
注:我们可以选择将方法的名称与结构中的一个字段相同,如上面代码中
3.2. 带有更多参数的方法
// 结构体方法
#[derive(Debug)]
struct Rectangle {width:u32,height:u32,
}
impl Rectangle {// 带有更多参数的方法fn can_hold(&self, other: &Rectangle) -> bool {self.area() > other.area()}
}
fn main() {let rectangle1 = Rectangle{width: 30,height: 50,};let rectangle2 = Rectangle {width: 20,height: 70};println!("矩形1的面积是否大于矩形2的面积?{}", rectangle1.can_hold(&rectangle2)); // true
}
可以在结构体方法中添加其他参数,如上面代码中,can_hold方法,在调用时,方法的第一个参数 self指的是实例本身,所以不用传参,只用给后面的参数赋值,如代码中的other参数
3.3.关联函数
所有在 impl 块中定义的函数被称为 关联函数(associated functions),因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String 类型上定义的 String::from 函数。
// 结构体方法
#[derive(Debug)]
struct Rectangle {width:u32,height:u32,
}
impl Rectangle {fn square(size:u32) -> Self {Self {width:size,height: size}}
}
fn main() {println!("正方形面积是{}",Rectangle::square(30).area());
}
注:关键字 Self 在函数的返回类型中代指在 impl 关键字后出现的类型,在这里是 Rectangle
使用结构体名和 :: 语法来调用这个关联函数;这个函数位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。
3.4.多个impl块
每个结构体都允许拥有多个 impl 块。每个方法有其自己的 impl 块。
#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}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}
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};let rect2 = Rectangle {width: 10,height: 40,};let rect3 = Rectangle {width: 60,height: 45,};println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
这里没有理由将这些方法分散在多个 impl 块中,不过这是有效的语法。
四、枚举和模式匹配
1.枚举的定义
1.1. 枚举初始使用
枚举:结构体的集合,使用关键字“enum”定义,后面为枚举名,枚举块中的定义的结构体均为该枚举的成员
// 定义枚举
enum Cart {Car{color: String, price: u32}, // 结构体Truck(String),// 元组结构体Bus(i32,i32,i32),// 元组结构体Boat,// 类单元结构体
}
fn main() {// 调用枚举let car = Cart::Car{color:String::from("蓝色"), price:20000}; // 调用结构体类型枚举let bus = Cart::Bus(65,32,98); // 调用元组结构体类型枚举let truck = Cart::Truck(String::from("测试"));let bus1 = Cart::Bus; // 可以这样写不赋值,但结构体形式的不可以这么写transformation(Cart::Boat);transformation(Cart::Truck(String::from("测试11")));
}
fn transformation(cart_type: Cart) {}
枚举中的其一个个成员一般为结构体,(可能还有其他类型,目前暂定),在调用枚举时,其实是对枚举的成员进行“实例化”;
枚举的成员位于其标识符的命名空间中,并使用两个冒号分开,因为枚举的每个成员都是该枚举的类型
结构体和枚举还有另一个相似点:就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法。
// 定义枚举
enum Cart {Car{color: String, price: u32}, // 结构体Truck(String),// 元组结构体Bus(i32,i32,i32),// 元组结构体Boat,// 类单元结构体
}
impl Cart {fn test(&self){println!("这就是个测试");}
}
fn main() {// 调用枚举let car = Cart::Car{color:String::from("蓝色"), price:20000}; // 调用结构体类型枚举car.test(); // 这就是个测试
}
1.2. Option枚举:
enum Option {None,Some(T),
}
在标准库中定义,用来判断某个值是否为空,可以不需要 Option:: 前缀,来直接使用 Some 和 None, T为值的类型
Some使用:
fn main() {let _some = Some(5);let _number = _some.unwrap_or(0);println!("number的值为{}", _number); // 5let a = 'A';let b = Some(a);if b.is_none(){println!("a是空值");}else {println!("a的值为{}", b.unwrap());}}
Some(T)可以用来对某个值进行判空操作以及其他操作
xxx.is_none()方法:判断某个值是否为空
xxx.unwrap()方法:获取传入到Some()的结果值
xxx.unwrap_or(参数)方法:获取传入到Some()的结果值,而且如果为空可以返回参数的值
None的使用:
fn main() {let _none: Option = None;let is_none = _none.unwrap_or(1);if _none.is_none() {println!("这个值是空值");}println!("none的值为{}", is_none);}
设置某个值为空值。
2. match控制流结构
2.1. match控制流结构的定义:
match控制流结构和if-else类似,满足某些条件后输出满足条件的内容。
enum Color {Blue,Red,Green,Yellow,While,Black,}
fn get_color(color: Color) -> String{match color {Color::Blue => {println!("这是测试");String::from("蓝色")}Color::Red => String::from("红色"),Color::Green => String::from("绿色"),Color::Yellow => String::from("黄色"),Color::While => String::from("白色"),Color::Black => String::from("黑色"),}
}
fn main() {println!("花的颜色是{}", get_color(Color::Red)); // 花的颜色是红色println!("这次是{}", get_color(Color::Blue)); // 这是测试 这次是蓝色get_color(Color::Blue); // 这是测试
}
match控制流结构的使用:使用match关键字定义,后面为参数值,可以为任何类型(if-else只能为bool类型),然后在方法块中 使用 =>来表示:当符合某项时,来执行符合该分支的代码。
match控制流结构,每个分支的执行语句可以是某个值,也可以是一些表达式,用大括号表示,当使用大括号时,后面的逗号可写可不写。
2.2. 绑定值的模式
当match匹配到某个分支时,还可传值带入进去。
enum Color {Blue,Red,Green(Special_Green),Yellow,While,Black,}
#[derive(Debug)]
enum Special_Green {Blue_Green,Red_Green,Yellow_Green,While_Green,Black_Green,
}
fn get_color(color: Color) -> String{match color {Color::Blue => {println!("这是测试");String::from("蓝色")}Color::Red => String::from("红色"),Color::Green(special) => {println!("这个绿是:{:?}", special);String::from("绿色")}Color::Yellow => String::from("黄色"),Color::While => String::from("白色"),Color::Black => String::from("黑色"),}
}
fn main() {println!("这个颜色是:{}",get_color(Color::Green(Special_Green::Blue_Green))); // 这个绿是:Blue_Green 这个颜色是:绿色
}
2.3. 匹配Option
fn get_one(x: Option) -> Option{match x {None => None,Some(i) => Some(i + 1)}
}
fn main() {let five = get_one(Some(5));println!("这个值为:{}", five.unwrap()); // 这个值为:6let none = get_one(None);println!("这个是:{}", none.unwrap_or(0)); // 这个是:0}
2.4. 通配模式和_占位符
fn main() {let roll_number = 54;match roll_number {6 => stop_time(),12 => go_time(),other => test(other),_ => reroll(),}fn stop_time(){};fn go_time(){};fn reroll(){println!("这是个测试")};fn test(other: i32){println!("这个值为:{}", other)}}
当使用match控制流结构体时,我们对某些值采取特殊操作,对于剩余的值,我们采用默认操作,这时,我们就可以使用通配模式或_占位符。
处理默认值时,我们在match控制流结构体中的最后一行自定义一个参数,然后可以使用这个值进行处理使用;或者我们也可以使用“_”占位符对默认值处理,使用占位符处理,一定不会使用默认值。在使用自定义参数时,我们也可以不使用其默认值进行数据处理。
注:通配值和“_”占位符都要放到最后一行,表示执行完后不会再匹配后面的值了。
3. if let简洁控制流
为了简写match控制流结构,剔除长代码,和if-else语句类似
fn main() {let x = Some(6u8);if let Some(a) = x {println!("这是一个测试"); // 这句会输出}else {println!("这是空");}let y:Option = None;if let Some(a) = y {println!("这是一个测试");}else {println!("这是空"); // 这句会输出}
}
结构:if let 分支=需要匹配的参数 {};为了满足match的穷尽性检查,然后在后面可以加个else语句
五、使用包、Crate和模块管理不断增长的项目
1. 包和Crate
crate 是 Rust 在编译时最小的代码单位。
crate 有两种形式:二进制项和库。二进制项 可以被编译为可执行程序,比如一个命令行程序或者一个服务器。它们必须有一个 main 函数来定义当程序被执行的时候所需要做的事情。目前我们所创建的 crate 都是二进制项。
库 并没有 main 函数,它们也不会编译为可执行程序,它们提供一些诸如函数之类的东西,使其他项目也能使用这些东西。比如第二章的 rand crate 就提供了生成随机数的东西。大多数时间 Rustaceans 说的 crate 指的都是库,这与其他编程语言中 library 概念一致。
包(package)是提供一系列功能的一个或者多个 crate。一个包会包含一个 Cargo.toml 文件,阐述如何去构建这些 crate。Cargo 就是一个包含构建你代码的二进制项的包。Cargo 也包含这些二进制项所依赖的库。其他项目也能用 Cargo 库来实现与 Cargo 命令行程序一样的逻辑。
包中可以包含至多一个库 crate(library crate)。包中可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。
2. 定义模块来控制作用域和私有性
关键字:
pub:把项定义为公共的
use:将模块引入到作用域
mod:定义模块; 结构 : mod 模块名 {}
cargo new --lib xxx 创建一个名为xxx的库
这里我们提供一个简单的参考,用来解释模块、路径、use关键词和pub关键词如何在编译器中工作,以及大部分开发者如何组织他们的代码。我们将在本章节中举例说明每条规则,不过这是一个解释模块工作方式的良好参考。
- 从 crate 根节点开始: 当编译一个 crate, 编译器首先在 crate 根文件(通常,对于一个库 crate 而言是src/lib.rs,对于一个二进制 crate 而言是src/main.rs)中寻找需要被编译的代码。
- 声明模块: 在 crate 根文件中,你可以声明一个新模块;比如,你用
mod garden声明了一个叫做garden的模块。编译器会在下列路径中寻找模块代码:- 内联,在大括号中,当
mod garden后方不是一个分号而是一个大括号 - 在文件 src/garden.rs
- 在文件 src/garden/mod.rs
- 内联,在大括号中,当
- 声明子模块: 在除了 crate 根节点以外的其他文件中,你可以定义子模块。比如,你可能在src/garden.rs中定义了
mod vegetables;。编译器会在以父模块命名的目录中寻找子模块代码:- 内联,在大括号中,当
mod vegetables后方不是一个分号而是一个大括号 - 在文件 src/garden/vegetables.rs
- 在文件 src/garden/vegetables/mod.rs
- 内联,在大括号中,当
- 模块中的代码路径: 一旦一个模块是你 crate 的一部分,你可以在隐私规则允许的前提下,从同一个 crate 内的任意地方,通过代码路径引用该模块的代码。举例而言,一个 garden vegetables 模块下的
Asparagus类型可以在crate::garden::vegetables::Asparagus被找到。 - 私有 vs 公用: 一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用
pub mod替代mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub。 use关键字: 在一个作用域内,use关键字创建了一个成员的快捷方式,用来减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的作用域,你可以通过use crate::garden::vegetables::Asparagus;创建一个快捷方式,然后你就可以在作用域中只写Asparagus来使用该类型。
3. 引用模块项目的路径
来看一下 Rust 如何在模块树中找到一个项的位置,我们使用路径的方式,就像在文件系统使用路径一样。为了调用一个函数,我们需要知道它的路径。
路径有两种形式:
- 绝对路径(absolute path)是以 crate 根(root)开头的全路径;对于外部 crate 的代码,是以 crate 名开头的绝对路径,对于对于当前 crate 的代码,则以字面值
crate开头。 - 相对路径(relative path)从当前模块开始,以
self、super或当前模块的标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。
使用 pub 关键字可以暴露路径,在 Rust 中,默认所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的。
父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。
使用 super 关键字可以调用到父模块的内容。
如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。
如果我们将枚举设为公有,则它的所有成员都将变为公有。
4. 使用use关键字将路径引入作用域
关键字使用:
1. use 关键字
use:在当前文件中引入其他模块路径,以便更方便使用其他模块;例:use std::fmt::Result;
注:
1. 在使用 use 关键字引入其他模块路径的时候,路径尽量截止到其父模块,否则可能会因为有相同函数名的函数导致调用错误。
// 最大父模块中的其子模块存在相同函数名的函数,所以只引入到函数的父级
use std::fmt;
use std::io;fn function1() -> fmt::Result {// --snip--Ok(())
}fn function2() -> io::Result<()> {// --snip--Ok(())
}
2. use 只能创建 use 所在的特定作用域内的短路径,所以在使用use关键字引入时,不能在子模块中使用。
mod front_of_house {pub mod hosting {pub fn add_to_waitlist() {}}
}use crate::front_of_house::hosting;mod customer {pub fn eat_at_restaurant() { // 不能被编译,因为use的引入在这无效,到不了该作用域hosting::add_to_waitlist();}
}
2. as 关键字
as:使用 as 关键字提供新名称,给相同名字的一个函数定义别名,防止引用冲突
use std::fmt::Result;
use std::io::Result as IoResult;fn function1() -> Result {// --snip--Ok(())
}fn function2() -> IoResult<()> {// --snip--Ok(())
}
3. pub use
pub use:重导出,是引入的模块可以在多个作用域内使用
4. 使用外部包
在 Cargo.toml 中加入依赖信息 例:rand = "0.8.5",然后在项目中使用use引入需要内容;
5. 嵌套路径来消除大量use行
第一种:
use std::cmp::Ordering;
use std::io;
// 可以改写成:
use std::{cmp::Ordering, io};
第二种:
use std::io;
use std::io::Write;
// 可以改写成:
use std::io::{self, Write};
6. 通过glob运算符将所有的公有定义引入到作用域
如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟 *,glob 运算符:
use std::collections::*;
注:使用 glob 运算符时请多加小心!Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。
5. 将模块拆分成多个文件
六、常见集合
6.1. 使用Vector储存列表
1. 新建Vector:
fn main() {let v:Vec = Vec::new();
}
这样新建Vector时,需要指定其类型,即Vec
还可以使用 vec!宏,创建一个有初始值的Vector:
fn main() {let v1 = vec![6, 8, 32, -99];
}
使用vec!宏创建的Vector,会自动根据值,判断其类型;
注:使用vec!创建的Vector,里面的值的类型必须相同;
2. Vector新增元素和获取元素
fn main() {let mut v:Vec = Vec::new();let v1 = vec![6, 8, 32, -99];// 新增元素v.push(8);v.push(-77);v.push(1);v.push(66);v.push(5666);// 获取Vector中的元素// 第一种方法:let second_number:&i32 = &v[1];println!("Vector中第二个数是{}", second_number);// 第二种方法:let second_number:Option<&i32> = v.get(5);match second_number {Some(second_number) => println!("这个值是{}", second_number),None => println!("没有这个值")}
}
使用 参数名.push();方法对Vector新增元素,自动加在末尾;
获取Vector中的元素有两种方法:一是:&参数名[元素位置];二是:参数名.get(元素位置)(注:元素位置都是从0开始数);
上面两种方法中第一种不可以索引越界,即获取的元素位置超过Vector中元素的个数,而第二种就没有这种限制,因为第二种方法参数的类型是Option
警告:这种操作是错误的:
fn main() {let mut v = vec![1, 2, 3, 4, 5];let first = &v[0];// v.push(6); 在这个位置加上这句会执行报错println!("The first element is: {first}");
}
在获取Vector的某个元素后,再进行新增操作是会报错的:因为新增时,可能会因为Vector原来的内存空间位置不够,而存放到新的内存空间,导致索引到的位置的为空(因为该位置已无元素,整个Vector的内存地址已经改变)
3. 遍历Vector
fn main() {let mut v1 = vec![6, 8, 32, -99, 9765, 41, 123];// 遍历并改变其值for i in &mut v1 {*i += 10;println!("i的值为:{}", i)}
}
使用for循环语句可以遍历Vector。
代码中遍历后,为每个元素做了自身加10并赋值给自身的操作,i前面的 * 号为解引用运算符,因为 i是从引用的 Vector中遍历的,只能做读取读取操作,使用解引用运算符后可以做其他处理
注:使用解引用运算符后,会对原来的的值做出修改
4. 使用枚举存储多种类型
fn main() {// 使用枚举来存储多种类型#[derive(Debug)]enum Car{Color(String),Weight(f64),Seat_Num(u32)}let car = vec![Car::Color(String::from("黑色")),Car::Weight(2.6),Car::Seat_Num(5)];
}
因为枚举中的成员都是相同类型的
5. 移除Vector中的元素
fn main() {let mut v:Vec = Vec::new();// 新增元素v.push(8);v.push(-77);v.push(1);v.push(66);v.push(5666);// 移除Vector中的最后一个元素并返回let remove = v.pop();match remove {Some(remove) => println!("被移除的这个值是{}", remove),None => println!("没有这个值")}}
使用 参数名.pop() 可以移除Vector中的最后一个元素,并返回最后一个元素
6.2. 使用字符串存储UTF-8编码的文本
1. 新建字符串
fn main() {// 新建字符串let mut s1:String = String::new();let s2:&str = "这是一个测试"; // 这种是字符串字面值,是rust核心定义的, String类型是rust的标准库定义let s3:String = "再次测试".to_string();let s4:String = String::from("这是一个字符串");
}
第一行是创建一个空的String类型;第二行是创建一个字符串字面值;第三行,第四行都是创建一个有默认值的String类型
2. 字符串新增(拼接)
fn main() {// 新建字符串let mut s4:String = String::from("这是一个字符串");// 字符串新增内容s4.push_str(",这是第二句"); // 末尾添加字符串println!("s4的内容是:{}", & s4); // 输出:s4的内容是:这是一个字符串,这是第二句s4.push('亚'); // 末尾添加字符println!("此时s4的内容是:{}", & s4); // 输出:此时s4的内容是:这是一个字符串,这是第二句亚let a = "。这是使用“+”号添加字符串";let b = String::from("这又是个类型");s4 = s4 + a + &b; // 使用“+”号在末尾新增数据,被加的数据类型只能是 &str 或&String(&String在这会被强转成&str) println!("现在s4的内容是:{}", & s4); // 输出:现在s4的内容是:这是一个字符串,这是第二句亚。这是使用“+”号添加字符串这又是个类型let c = String::from("123");let d = "567";s4 = format!("{}{}{}",s4, c, d); // 使用 format!()宏 做字符串拼接新增println!("最后s4的内容是{}", &s4); // 输出:最后s4的内容是这是一个字符串,这是第二句亚。这是使用“+”号添加字符串这又是个类型123567}
方法:
参数名.push_str()方法可以对原来字符串后面新增括号中的字符串内容;
参数名.push()方法可以丢原来的字符串后面新增括号中的字符类型内容;
可以使用“+”号,对原来字符串拼接,拼接内容为“+”号后面的内容,必须是&str或&String(&String会被强转成&str)
如果是多个拼接,可以使用 format!()宏 来对字符串进行拼接
3. 索引字符串
rust中字符串因为使用的是UTF-8格式,所以一个一个字符对应的unicode码值占用多个字节
可以使用 &参数名[索引初始值..索引截止值] 索引字符串内容,但是非常不建议使用,因为不清楚一个字符占了几个字节,这样索引时,导致程序报错。
4. 遍历字符串
fn main() {// 遍历字符串let ss = "王小明";for x in ss.chars() { // 以字符形式遍历print!("{} ", x); // 输出:王,小,明,}println!();for y in ss.bytes() { // 以字节的形式遍历print!("{} ", y) // 输出:231,142,139,229,176,143,230,152,142,}
}
字符串的遍历有这两种遍历方式,一种是以字符形式遍历,另一种是以字节的形式进行遍历
6.3. 使用Hash Map存储键值对
1. 新建HashMap并添加数据
use std::collections::HashMap;fn main() {let mut scores = HashMap::new(); // 新建HashMapscores.insert(String::from("语文"), 98); // 往HashMap中添加数据scores.insert(String::from("数学"), 90);
}
使用 HashMap::new()方法可以新建HashMap,参数名.insert(k,v)可以往这个HashMap中添加数据
注:和Vector相同,里面的元素类型必须相同
2. 获取HashMap中的值以及遍历
use std::collections::HashMap;fn main() {let mut scores = HashMap::new(); // 新建HashMapscores.insert("语文", 98); // 往HashMap中添加数据scores.insert("数学", 90);// 获取HashMap中的值let chinese_score = scores.get("数学");println!("数学成绩是:{}", chinese_score.copied().unwrap_or(0));// 遍历HashMapfor (key, value) in scores {println!("{}的成绩是:{}", key, value);}
}
使用 参数名.get(key值) 可获得一个 Option
3. 更新HashMap
use std::collections::HashMap;fn main() {let mut scores = HashMap::new(); // 新建HashMapscores.insert("语文", 98); // 往HashMap中添加数据scores.insert("数学", 90);// 更新HashMapscores.insert("语文", 95);println!("现在语文成绩是:{}", scores.get("语文").copied().unwrap_or(0));// 判断没该键值对时新增,有则不做改变scores.entry("英语").or_insert(96);scores.entry("数学").or_insert(97);println!("现在英语成绩是:{}", scores.get("英语").copied().unwrap_or(0)); // 现在的英语成绩是:96println!("现在数学成绩是:{}", scores.get("数学").copied().unwrap_or(0)); // 现在的数学成绩是:90// 根据旧值更新一个值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); // or_insert(0) 该方法返回的是 &value类型的值*count += 1; // 此处一直改变当前word的value值}println!("{:#?}", map);}
HashMap中,当对一个存在的key新增时,则会覆盖掉该key的value值
参数名.entry(key).or_insert(value) 判断该HashMap中是否存在一个名为 key的键,不存在则新增一个键值对,值为 value
参数名.split_whitespace()是字符串按 空格 分割
七、错误处理
7.1. 用panic!处理不可恢复的错误
对应的panic时栈展开或终止
当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止(abort),这会不清理数据就退出程序。
那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:
[profile.release]
panic = 'abort'
简单的程序中调用 panic!宏
fn main() {panic!("crash and burn");
}
在执行时,可以加 RUST_BACKTRACE=full 然后再cargo run 运行程序,这样会得到一个 backtrace。backtrace 是一个执行到目前位置所有被调用的函数的列表。
7.2. 使用Result处理可恢复的错误
1.了解Result
#![allow(unused)]
fn main() {
enum Result {Ok(T),Err(E),
}
}
它定义有如下两个成员,Ok 和 Err,T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。而且和Option
示例:
use std::fs::File;fn main() {let get_file_result = File::open("hello.txt");let get_file = match get_file_result {Ok(file) => file,Err(error) => panic!("错误信息是:{}", error),};
}
成功则会返回文件,主动打印错误信息不能用println!宏,只能用panic!宏打印错误信息
2. 使用match匹配不同的错误
use std::fs::File;
use std::io::ErrorKind;fn main() {let get_file_result = File::open("hello.txt");let get_file = match get_file_result {Ok(file) => file,Err(error) => match error.kind() { // kind()方法获取错误类型ErrorKind::NotFound => match File::create("hello.txt") { // 文件没有找到Ok(fc) => fc,Err(e) => panic!("错误信息是:{}", e),},other_error => panic!("错误信息是:{}", other_error),},};
}
可以通过 Error结构体中的kind()方法获取错误类型
3. 失败是panic的简写:unwrap和expect
use std::fs::File;
use std::io::ErrorKind;fn main() {// 失败时panic的简写:unwrap()和expectlet get_file1 = File::open("hello.txt").unwrap(); // panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }let get_file2 = File::open("hello.txt").expect("根本没有这个文件"); // panicked at '根本没有这个文件: Os { code: 2, kind: NotFound, message: "No such file or directory" }
}
unwrap()和expect()方法在成功时,则会返回Result中OK的值,当失败时则会自动调用panic打印错误信息,这两者不同的是:unwrap()方法默认使用系统的错误信息,而expect()方法则会使用我们自己定义的错误信息。
4. 传播错误(返回错误)
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};fn read_username_from_file() -> Result {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),}
}
}
当有调用者调用该函数时,成功则会返回文件中的内容,失败则会返回错误信息,此时不在控制台打印错误信息了
5. 传播错误的简写:?运算符
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};fn read_username_from_file() -> Result {// 方法一:let mut username_file = File::open("hello.txt")?;let mut username = String::new();username_file.read_to_string(&mut username)?;Ok(username)// 方法二:链式书写let mut username = String::new();File::open("hello.txt")?.read_to_string(&mut username)?;Ok(username)// 方法三:专门的导出函数:fs::read_to_string("hello.txt")
}
}
?运算符 可以返回Result中OK是的结果,也可以返回Err是的结果
? 运算符只能被用于返回值与 ? 作用的值相兼容的函数。因为 ? 运算符被定义为从函数中提早返回一个值。
Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。
Option
八、泛型、Trait和生命周期
8.1 泛型数据类型
泛型:就是用一个参数代替真实的参数类型,他可以代表任何参数类型,只是定义。(个人理解)
1. 函数定义中使用泛型
fn main() {let v1 = vec![1, 6, 156, 4685, -145, 456];let largest1 = largest(&v1);println!("v1中最大的值为:{}", largest1); // v1中最大的值为:4685let v2 = vec!['a', 'u', 'c', 'z', 'p'];let largest2 = largest(&v2);println!("v2中最大的字母是:{}", largest2); // v2中最大的字母是:z
}fn largest(list: &[T]) -> &T{let mut largest = &list[0];for item in list {if item > largest {largest = item;}}largest
}
函数largest中,使用泛型定义了一个参数 需要在函数名后面加上泛型(
std::cmp::PartialOrd 为了让我们开启比较功能,因为泛型的具体类型未知,所以不能判断两边类型
2. 结构体中使用泛型
fn main() {/**结构体中定义泛型*/// 这个结构体的两个参数类型必须相同,因为他们设置的泛型是同一个let first_car = Car{name: String::from("大众"), color: String::from("黑色")};// 这个结构体的两个参数类型可以不同,因为这个结构体两个参数的类型定义的不同let first_student = Student{name: String::from("张三"), weight: 50};}struct Car {name: T,color: T,
}struct Student {name: T,weight: U,
}
在结构体中使用泛型 只需要在定义结构体时结构体名后面加
注:在定义结构体时,如果多个字段使用同一个泛型,那么在创建实例时,必须其字段赋的值的类型相同,否则会报错;如果想多个字段为不同类型,可以在定义结构体时泛型设置为多个类型即
3. 枚举中定义泛型
// rust定义:判断是否非空
enum Option {Some(T),None,
}
// rust定义:判断异常
enum Result {Ok(T),Err(E),
}
4. 方法定义中的泛型
fn main() {/**结构体中定义泛型*/// 这个结构体的两个参数类型必须相同,因为他们设置的泛型是同一个let first_car = Car{name: String::from("大众"), color: String::from("黑色")};// 这个结构体的两个参数类型可以不同,因为这个结构体两个参数的类型定义的不同let first_student = Student{name: String::from("张三"), weight: 50};let name = first_car.x();println!("汽车的名字叫:{}", name); // 汽车的名字叫:大众let get_connect = first_car.connect_test();println!("拼接的内容为:{}", get_connect); // 拼接的内容为:大众黑色let point = Point{x: 2.0, y: 2.0};let get_length = point.get_distance();println!("长度为:{}", get_length); // 长度为:4let student1 = Student{name:String::from("李四"), weight:45};let student2 = Student{name:String::from("王五"), weight:59};let mix = student1.get_mix(student2);println!("mix为:{:?}", mix); // mix为:Student { name: "李四", weight: 59 }}struct Car {name: T,color: T,
}
struct Point {x: T,y:T,
}
#[derive(Debug)]
struct Student {name: T,weight: U,
}
// 此处impl定义的泛型和结构体的泛型不一定要一致,只是个代号而已
impl Car{fn x(&self) -> &T{&self.name}
}impl Car {fn connect_test(self) -> String{let mut a = self.name;a.push_str(&self.color);a}
}impl Point {fn get_distance(&self) -> f64 {(self.x.powi(3) + self.y.powi(3)).sqrt()}
}impl Student {fn get_mix(self, other: Student) -> Student {Student{name: self.name,weight: other.weight}}
}
在方法中定义泛型可以在impl关键字后面加
ps:powi(参数)这个函数是获取浮点数的幂次方的函数,参数处是多少就是多少次方;sqrt()函数是开根号函数;该两个方法只有整型和浮点数可以使用
8.2 Trait:定义共同行为
Trait:定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为;类似于其他语言的接口概念,当然也有些不同
1. 定义Trait
pub trait Summary{fn summarize(&self) -> String;
}
定义trait使用关键字 trait,然后后面跟上名字,(定义时使用pub以方便被其他文件访问),然后再在代码块中加上方法用来公共调用。
trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。
2. 实现trait中的方法
pub trait Summary{fn summarize(&self) -> String;
}pub struct NewsArticle{pub headline: String,pub location: String,pub author: String,pub content: String,
}
// 结构体NewsArticle实现了Summary接口
impl Summary for NewsArticle {fn summarize(&self) -> String {format!("{}, by {} ({})", self.headline, self.author, self.location)}
}pub struct Tweet{pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}// 结构体Tweet实现了Summary接口
impl Summary for Tweet {fn summarize(&self) -> String {format!("{}:{}", self.username, self.content)}
}
结构体实现trait方法:impl trait名 for 结构体名,然后在代码块中写出trait中需要实现方法的具体实现内容
use traits::{Summary, Tweet};fn main(){let tweet = Tweet{username: String::from("张三"),content: String::from("hello everybody, my name is 张三"),reply: false,retweet: false,};println!("这次的推文是:{}", tweet.summarize())
}
在创建实例后,可以直接实例名.xx()方法。
3. 默认实现
pub trait Summary{fn summarize(&self) -> String{String::from("读更多...")}
}pub struct NewsArticle{pub headline: String,pub location: String,pub author: String,pub content: String,
}
// 结构体NewsArticle实现了Summary接口
impl Summary for NewsArticle {}pub struct Tweet{pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}// 结构体Tweet实现了Summary接口
impl Summary for Tweet {fn summarize(&self) -> String {format!("{}:{}", self.username, self.content)}
}
在trait中的方法可以为其默认实现,当结构体去实现这个trait时,就不用必须去实现其中的方法
可以直接写成 Impl trait名 for 结构体名 {}
use traits::{NewsArticle, Summary, Tweet};fn main(){let tweet = Tweet{username: String::from("张三"),content: String::from("hello everybody, my name is 张三"),reply: false,retweet: false,};println!("这次的推文是:{}", tweet.summarize());let newsArticle = NewsArticle{headline: String::from("震惊,光天化日之下,他居然做这种事!!!"),location: String::from("纳奇塔卡塞娜星球"),author: String::from("李四"),content: String::from("千历9848年63月751号,李四在街上发射了他自研的星球制造器"),};println!("这则新闻是:{}", newsArticle.summarize());
}
当创建实例后,可以调用trait中的方法,会默认执行trait中该方法默认实现的内容;
当然该有默认的实现的方法依然可以被实现重写,当被重写后,会调用重写后的方法不会调用trait中被默认实现的那个方法
一个trait中有以默认实现的方法和未实现的方法,当结构体去实现的时候只用实现那些未实现的方法,在trait中已做了实现的方法不是必须实现
4. trait作为参数
pub fn notify(item: &impl Summary) {println!("Breaking news! {}", item.summarize());
}fn main(){notify(&已实现Summary的实例);
}
trait可以作为参数用在函数中,参数为已实现该trait的结构体实例
trait作为参数还可以写成泛型的形式:
pub fn notify(item: &T) {println!("Breaking news! {}", item.summarize());
}
可以通过 + 号实现多个trait
pub fn notify(item: &(impl Summary + Display)) {}pub fn notify(item: &T) {}
通过 where 简写 多个trait形式
pub trait Summary{fn summarize(&self) -> String{String::from("读更多...")}
}pub trait Get{}fn get_content(item: &T) -> i32
whereT: Summary + Get,
{ none}
为了简洁,可以使用 where 关键字简写 多个trait实现
5. 返回实现了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,}
}
可以把trait当做返回值类型返回,当然调用该函数时,不知道其返回的具体类型。这的问题后续会讲解。
注:我们也可以实现标准库中的trait。
8.3 生命周期确保引用有效
1.使用
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
生命周期的使用 'a 此处a只是代号,不唯一 还可以是 'b 'c ... 一般放在&符号后面 然后空格后再加上参数类型
2. 函数参数中的生命周期注解
fn main() {let s1 = String::from("abcd");let s2 = "xyz";let result = get_longest(s1.as_str(), s2);println!("结果是:{}", result)
}fn get_longest<'a>(s1: &'a str, s2: &'a str) -> &'a str{if s1.len() > s2.len() {s1}else {s2}
}
在函数中生命周期注解 要和泛型一样,在函数名后加<'a>
上面示例中的方法:在未加生命周期注解前会报错,因为该方法的返回不确定是返回哪一个,因为这个函数不知道函数中的返回值的存在时间,即生命周期;使用了 生命周期注解后,让他们的周期为一样,这样rust编译器可以知道了。
当多个参数被同一个生命周期注解标注时,生命周期注解默认按参数中生命周期短的那个
这种情况下不用每个参数都加上生命周期注解:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {x
}
因为返回值,只有x,所以y就没必要再加上生命周期注解。
这种情况也是是错误的:
fn longest<'a>(x: &str, y: &str) -> &'a str {let result = String::from("really long string");result.as_str()
}
虽然加了生命周期注解,但是这个函数的返回值生命周期就在函数内,出了函数体就被清理了,所以根本不能返回,运行会报错。
3. 结构体中定义的生命周期注解
fn main() {// 结构体中的生命周期注解let s2 = String::from("hi, my name is 哈哈");let first_sentence = s2.split(',').next().expect("有问题啊");let car = Car{name: first_sentence};println!("{:?}", car); // Car { name: "hi" }
}#[derive(Debug)]
struct Car<'a> {name: &'a str,
}
在结构体中定义生命周期注解和定义泛型一样,在结构体名后面加<'a>生命周期注解,然后在其字段类型上添加生命周期注解。
函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块。
第一条规则是编译器为每一个引用参数都分配一个生命周期参数。换句话说就是,函数有一个引用参数的就有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数就有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。
第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32。
第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明是个对象的方法 (method)(译者注:这里涉及 rust 的面向对象参见 17 章),那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
4. 方法中定义生命周期注解
impl<'a> ImportantExcerpt<'a> {fn level(&self) -> i32 {3}
}
给方法定义生命周期注解和泛型一样。
上面正好适用于第三条,其实生命周期注解是可以省略不写的。不写不代表他没有,只不过是省略了。
5. 静态生命周期注解
let s: &'static str = "I have a static lifetime.";
用 'static 注解的就是静态生命周期注解。
作用:程序全局有效。
use std::fmt::Display;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}
}
泛型,trait bound和生命周期注解的合用
九、编写自动化测试
9.1 如何编写测试
1. 测试函数剖析
Rust 中的测试就是一个带有 test 属性注解的函数。
了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]。当使用 cargo test 命令运行测试时,Rust 会构建一个测试执行程序用来调用被标注的函数,并报告每一个测试是通过还是失败。
#[cfg(test)]
mod tests {#[test]fn method1(){assert_eq!(2 + 2, 4);}#[test]fn method2(){panic!("这是个错误的测试");}}
函数测试,就在函数上添加#[test]就可以把非测试函数变成测试函数,当在终端执行 cargo test后,终端中就会显示每个方法的执行的成功与失败情况
Compiling adder v0.1.0 (/home/byl/IdeaProjects/rustProject/adder)Finished test [unoptimized + debuginfo] target(s) in 0.21sRunning unittests src/lib.rs (target/debug/deps/adder-2be33b9b324550dd)running 2 tests
test tests::method1 ... ok
test tests::method2 ... FAILEDfailures:---- tests::method2 stdout ----
thread 'tests::method2' panicked at '这是个错误的测试', src/lib.rs:16:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::method2test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`
2. 使用 assert! 宏来检查结果
assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 true,assert! 什么也不做,同时测试会通过。如果值为 false,assert! 调用 panic! 宏,这会导致测试失败。
#[derive(Debug)]
struct Rectangle{width: u32,heighth: u32,
}impl Rectangle {fn can_hold(&self, another: &Rectangle) -> bool{self.width > another.width && self.heighth > another.heighth}
}
#[cfg(test)]
mod tests {use crate::Rectangle;#[test]fn method3(){let larger_rectangle = Rectangle{width: 10, heighth: 5};let smaller_rectangle = Rectangle{width: 5, heighth: 1};assert!(larger_rectangle.can_hold(&smaller_rectangle));}#[test]fn method4(){let larger_rectangle = Rectangle{width: 20, heighth: 10};let smaller_rectangle = Rectangle{width: 15, heighth: 7};assert!(!smaller_rectangle.can_hold(&larger_rectangle));}
}
使用cargo test执行结果和上面相同,会判断所有的测试方法的执行成功失败情况
3. 使用assert_eq!和assert_ne!宏来测试相等
assert_eq!宏 判断是当两边相等时提示成功,而assert_ne!宏则是判断当两边不相等时成功。
#[cfg(test)]
mod tests {#[test]fn method5(){assert_eq!(1+3, 4); // success}#[test]fn method6(){assert_ne!(1+3, 4); // fail}
}
4. 自定义失败信息
assert!宏,assert_eq!宏,assert_ne!宏 都可以自定义失败信息
#[cfg(test)]
mod tests {#[test]fn method7(){assert!(1 > 2, "这是错误的1");}#[test]fn method8(){assert_eq!(1+3, 5, "这是错误的2");}#[test]fn method9(){assert_ne!(1+3, 4, "这个是对的3");}}
5. 使用should_panic检查panic
#[should_panic] 属性位于 #[test] 之后,对应的测试函数之前。
pub struct Guess {value: i32,
}impl Guess {pub fn new(value: i32) -> Guess {if value < 1 || value > 100 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess { value }}
}#[cfg(test)]
mod tests {use super::*;#[test]#[should_panic]fn greater_than_100() {Guess::new(200);}
}
should_panic 在测试函数出现panic异常时通过,在没有出现panic异常则测试失败
should_panic还可以指定期望的报错信息,在should_panic后面加(expected = xxx),当测试方法出现panic时,且panic的报错信息里含有“xxx”时,则测试通过,否则测试都失败
pub struct Guess {value: i32,
}
impl Guess {pub fn new(value: i32) -> Guess {if value < 1 {panic!("Guess value must be less than or equal to 100, got {}.",value);} else if value > 100 {panic!("你好 must be greater than or equal to 1, got {}.",value);}Guess { value }}
}
#[cfg(test)]
mod tests {use crate::Guess;#[should_panic(expected = "你好")]#[test]fn greater_than_100() {Guess::new(200); // success}
}
如示例代码,程序执行出现了panic,而且panic的信息里的 “你好”和should_panic中的expected的值相等,则测试通过。
6. 将 Result用于测试
#[cfg(test)]
mod tests {#[test]fn method10() -> Result<(), String>{if 2 + 2 == 4{Ok(())}else {Err(String::from("这是错的"))}}
}
当正确时什么都不返回,当错了,返回Err()中的报错信息。
注:不能对这些使用 Result 的测试使用 #[should_panic] 注解。要断言操作返回Err变量,请不要在Result<T,E>值上使用问号(?)运算符。相反,请使用assert!(value.is_err())
9.2 控制测试如何运行
cargo test -h可以查看关于测试相关的指令
1. 并行或连续运行测试
当运行多个测试时,Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。
如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 --test-threads 参数和希望使用线程的数量给测试二进制文件。例如:
$ cargo test -- --test-threads=1
2. 显示函数输出
默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。
运行这个测试语句可以看到函数输出
$ cargo test -- --show-output
3. 通过指定名字来运行部分测试
$ cargo test 函数名
指定函数名字,测试时,就只会测试这个函数。
测试时,还可以根据要测试单元的所包含的某个字来执行,过滤掉其他测试
例如:有测试:ABC, ABD, BCD
$ cargo test AB
这个则会只测试 ABC, ABD 这两个测试
4. 忽略某些测试
在要测试的函数上加 #[ignore] 属性就可以忽略这个测试函数,执行cargo test 就只运行没有 标记 #[ignore]的测试函数。
当你需要运行 ignored 的测试时,可以执行 cargo test -- --ignored
如果你希望不管是否忽略都要运行全部测试,可以运行 cargo test -- --include-ignored。
9.3 测试的组织结构
测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与 集成测试(integration tests)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
1. 单元测试
单元测试与他们要测试的代码共同存放在位于 src 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 tests 模块,并使用 cfg(test) 标注模块。
在模块上添加 #[cfg(test)] 则表示这个模块是测试模块,只会在执行cargo test时才会编译和运行,在编译,打包时也不会打包此处代码
rust支持测试私有函数
pub fn add_two(a: i32) -> i32 {internal_adder(a, 2)
}fn internal_adder(a: i32, b: i32) -> i32 {a + b
}#[cfg(test)]
mod tests {use super::*;#[test]fn internal() {assert_eq!(4, internal_adder(2, 2));}
}
internal_adder 函数并没有标记为 pub。测试也不过是 Rust 代码,同时 tests 也仅仅是另一个模块。子模块的项可以使用其上级模块的项。在测试中,我们通过 use super::* 将 test 模块的父模块的所有项引入了作用域,接着测试调用了 internal_adder。
2. 集成测试(看懂了,不会描述)
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
创建成类似的文件结构
如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 目录创建集成测试并使用 extern crate 导入 src/main.rs 中定义的函数。只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行。
十、一个I/O项目:构建一个命令行程序
10.1 接受命令行参数
使用标准库中函数 std::env::args函数可以获取命令行输入内容:
use std::env;
fn main() {let args: Vec = env::args().collect();let query = &args[1];let file_path = &args[2];println!("搜索内容:{}", query);println!("路径是:{}", file_path);
}
env::args().collect() 会返回一个迭代器(集合),可以生成一个vector 当然迭代器生成的类型未定义,所以需要参数指定类型。
在命令行使用 cargo run执行时:
cargo run -- test sample.txt
上面代码则会打印 :搜索内容:test 路径是:sample.txt
获取了cargo run -- 后面的内容,以空格分一个字符串
10.2 读取文件
通过 fs::read_to_string(文件路径) 就可以读取文件中的内容,是一次性全部读取出来
use std::{env, fs};fn main() {let args:Vec = env::args().collect();let query = &args[1];let file_path = &args[2];println!("文件路径是:{}", file_path);let contents = fs::read_to_string(file_path).expect("读取失败");println!("读取的内容是\n{}", contents);
}
其他内容方法:
fs:read(文件路径) 以文件内容的unicode值的方式读取
10.3 重构以改进模块化与错误处理
1. 重构以错误处理
为了让代码读取更方便,更易理解,所以我们对代码进行修改优化
use std::{env, process};fn main() {let args:Vec = env::args().collect();let config = Config::build(&args).unwrap_or_else(|error|{println!("错误信息是:{}", error);process::exit(1);});println!("要查询的内容是:{}",config.query);println!("从 {} 文件中查找", config.file_path);}struct Config{query: String,file_path: String,
}impl Config {fn build(args: &[String]) -> Result{if args.len() < 3 {return Err("没有足够的参数,不能正常打印");}let query = args[1].clone();let file_path = args[2].clone();Ok(Config{query, file_path})}
}
我们首先使用一个结构体Config来表明我们要获取的内容,然后定义其方法来获取参数值;针对错误处理,我们使用Result
unwrap_or_else方法:为了获取方法中返回的错误结果,然后进行打印;
process::exit(1)方法:可以立即停止程序,并且不会再有额外的输出。
10.4 采用测试驱动开发完善库的功能
我们将遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 minigrep 的搜索逻辑。它遵循如下步骤:
- 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
- 编写或修改足够的代码来使新的测试通过。
- 重构刚刚增加或修改的代码,并确保测试仍然能通过。
- 从步骤 1 开始重复!
use std::error::Error;
use std::fs;
use std::fs::read_to_string;#[cfg(test)]
mod tests {use super::*;#[test]fn one_result() {let query = "duct";let contents = "\Rust:\safe, fast, productive.\Pick three";assert_eq!(vec!["safe, fast, productive"], search(query, contents));}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{let mut result = Vec::new();for line in contents.lines() {if line.contains(query) {result.push(line)}}result
}pub struct Config{pub query: String,pub file_path: String,
}impl Config {pub fn build(args: &[String]) -> Config{let query = args[1].clone();let file_path = args[2].clone();Config{query, file_path}}
}
pub fn run(config: Config) -> Result<(), Box>{let content = fs::read_to_string(config.file_path)?;for line in search(&config.query, &content) {println!("这一行是:{}", line);}Ok(())
}
use std::env;
use test_function::Config;fn main() {let args: Vec = env::args().collect();let config = Config::build(&args);test_function::run(config);
}
String中的一个方法:lines()方法:可以获取一段文字的每一行(获取文字的每一行)
10.5 处理环境变量
本节是测试忽略命令行大小写,一律都给转换成小写
use std::error::Error;
use std::{env, fs};
use std::fs::read_to_string;#[cfg(test)]
mod tests {use super::*;#[test]fn case_sensitive() {let query = "duct";let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";assert_eq!(vec!["safe, fast, productive."], search(query, contents));}#[test]fn case_insensitive(){let query = "rUst";let contents = "\
Rust:
safe, fast, productive.
pick three.
Trust me.";assert_eq!(vec!["Rust:", "Trust me."], search_case_insensitive(query, contents));}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{let mut result = Vec::new();for line in contents.lines() {if line.contains(query) {result.push(line)}}result
}pub struct Config{pub query: String,pub file_path: String,pub ignore_case: bool,
}impl Config {pub fn build(args: &[String]) -> Result{if args.len() < 3 {return Err("没有足够的参数")}let query = args[1].clone();let file_path = args[2].clone();let ignore_case = env::var("IGNORE_CASE").is_ok();Ok(Config{query, file_path, ignore_case})}
}
pub fn run(config: Config) -> Result<(), Box>{let content = fs::read_to_string(config.file_path)?;let result = if config.ignore_case{search_case_insensitive(&config.query, &content)}else {search(&config.query, &content)};for line in result {println!("这一行是:{}", line);}Ok(())
}pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{let mut result = Vec::new();let query = query.to_lowercase();for line in contents.lines() {if line.to_lowercase().contains(&query) {result.push(line)}}result
}
use std::env;
use test_function::Config;fn main() {let args: Vec = env::args().collect();let config = Config::build(&args);test_function::run(config.unwrap());
}
结构体中添加了第三个参数,是否设置忽略大小写,使用env::var(参数).isok,命令行输入 参数=值, cargo run -- 要搜索内容 被搜索的文件,该方法是判断是否设置了值,没设置一律按false。
字面值slice类型(&str).to_lowcase()会生成一个String类型
10.6 将错误信息输出到标准错误而不是标准输出
大部分终端都提供了两种输出:标准输出(standard output,stdout)对应一般信息,标准错误(standard error,stderr)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。
cargo run > output.txt
会将错误信息输出到这个文件中
标准库提供了 eprintln! 宏来打印到标准错误流,替换掉println!
十一、Rust中的函数式语言功能:迭代器与闭包
11.1 闭包:可以捕获其环境的匿名函数
例子:有时 T 恤公司会赠送限量版 T 恤给邮件列表中的成员作为促销。邮件列表中的成员可以选择将他们的喜爱的颜色添加到个人信息中。如果被选中的成员设置了喜爱的颜色,他们将获得那个颜色的 T 恤。如果他没有设置喜爱的颜色,他们会获赠公司现存最多的颜色的款式。
// 定义枚举,代表颜色类型
#[derive(Debug,PartialEq, Copy, Clone)]
enum ShirtColor{Red,Blue,
}
// 定义结构体代表公司衬衫的数量
struct Inventory{shirts: Vec,
}impl Inventory {// 公司给成员们的衬衫颜色fn giveaway(&self, user_preference: Option) -> ShirtColor{// 用户所喜爱的或库存剩余最多的(调用的方法是获取库存剩余最多的颜色)user_preference.unwrap_or_else(|| self.most_stocked())}// 获取库存最多的颜色,目前假设有2件红色和1件蓝色fn most_stocked(&self) -> ShirtColor{// 初始化每种颜色的数据量let mut num_red = 0;let mut num_blue = 0;// 遍历获取库存中两种颜色各多少件for color in &self.shirts {match color {ShirtColor::Red => num_red += 1,ShirtColor::Blue => num_blue += 1,}}// 判断哪种颜色的最多然后返回哪个颜色的if num_red > num_blue {ShirtColor::Red}else {ShirtColor::Blue}}
}
fn main() {// 初始化公司的库存衬衫let store = Inventory{shirts: vec![ShirtColor::Red, ShirtColor::Blue, ShirtColor::Red]};// 定义用户1喜欢的颜色let user1 = Some(ShirtColor::Blue);let user1_color = store.giveaway(user1);println!("用户1喜欢的颜色是{:?},得到的颜色是:{:?}", user1.unwrap(), user1_color);// 定义用户2喜欢的颜色(无)let user2 = None;let user2_color = store.giveaway(user2);println!("用户2喜欢的颜色是:{:?},得到的颜色是:{:?}", user2, user2_color);
}
我们将被闭包表达式 || self.most_stocked() 用作 unwrap_or_else 的参数。这是一个本身不获取参数的闭包(如果闭包有参数,它们会出现在两道竖杠之间)。
1.2 闭包的类型推断和注解
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
第一行是一个函数,第二行到第四行都是闭包的定义;不同之处是:第二行是完整标注的闭包定义,指定了参数的类型,在被调用时,参数只能是该类型,而第三行和第四行则不限制参数类型,三四行是闭包的简写
注:在多次调用闭包时,参数只能是同一类型,即如果是String类型调用过,则后续调用只能是String类型,其他类型调用则会报错
1.3 捕获引用或移动所有权
闭包可以通过三种方式捕获其环境,它们直接对应到函数获取参数的三种方式:不可变借用,可变借用和获取所有权。
不了变借用:
fn main() {// 不可变引用let list1 = vec![1, 2];println!("闭包使用前list1的值为:{:?}", list1); // 闭包使用前list1的值为:[1, 2]let borrow1 = || println!("闭包调用时list1的值{:?}", list1); // 闭包调用时list1的值[1, 2]borrow1();println!("闭包调用后的list1的值:{:?}", list1); // 闭包调用后的list1的值:[1, 2]
}
不可变借用:在借用前后值都是不变的
可变借用:
fn main() {// 可变借用let mut list2 = vec![1, 2];println!("闭包调用前list2的值:{:?}", list2); // 闭包调用前list2的值:[1, 2]let mut borrow2 = || list2.push(3);// println!("此时的值为:{:?}", list2); // 此处不可打印,因为上面发生了可变借用,此处又发生了不可变借用,报错,可变借用未结束调用前,不可有其他的不可变借用borrow2();println!("闭包调用后list2的值:{:?}", list2); // 闭包调用后list2的值:[1, 2, 3]
}
可变借用:在借用前后值可能是会发生改变的
个人理解:borrow2之所以是let mut 是因为闭包做了值的改变,所以其参数性质也是要可变的
获取所有权:
fn main() {// 获取所有权let mut list3 = vec![1, 2];println!("使用闭包前list3的值{:?}", list3);thread::spawn(move || println!("此时list3的值为:{:?}", list3)).join().unwrap();
}
获取所有权:使用move可以获取参数的所有权
此处在线程中使用,必须要获取list3的所有权,因为线程中,不清楚是主线程先执行完还是新线程先执行完,如果主线程先执行完,然后把list3给弃用,则新线程调用时会发现不了报错,所以新线程要获取这个参数的所有权。
1.4 将被捕获的值移出闭包和Fn trait
闭包捕获和处理环境中的值的方式影响闭包实现的 trait。Trait 是函数和结构体指定它们能用的闭包的类型的方式。取决于闭包体如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait。
FnOnce适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值移出闭包体的闭包只实现FnOncetrait,这是因为它只能被调用一次。FnMut适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次。Fn适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要。
在 Option 上的 unwrap_or_else 方法的定义
impl Option {pub fn unwrap_or_else(self, f: F) -> TwhereF: FnOnce() -> T{match self {Some(x) => x,None => f(),}}
}
11.2 使用迭代器处理元素序列
迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。
在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果。
fn main() {let list = vec![1, 2, 3];let list_iter = list.iter();// list_iter未被使用,则迭代器不会被创建
}
2.1 Iterator trait和next方法
迭代器都实现了一个叫做 Iterator 的定义于标准库的 trait。这个 trait 的定义看起来像这样:
pub trait Iterator {type Item;fn next(&mut self) -> Option;// 此处省略了方法的默认实现
}
type Item是后面内容先不讲,只知道是个类型,next方法的返回类型为Option
next 是 Iterator 实现者被要求定义的唯一方法。next 一次返回迭代器中的一个项,封装在 Some 中,当迭代器结束时,它返回 None。
写个简单的测试:
#[cfg(test)]
mod tests{#[test]fn iterator_test(){let vec = vec![1, 2];let mut vec_iter = vec.iter();assert_eq!(vec_iter.next(), Some(&1));assert_eq!(vec_iter.next(), Some(&2));assert_eq!(vec_iter.next(), None);}
}
注意:vec_iter是可变的,因为在调用 next方法时,迭代器中用来记录序列位置的状态改变了。使用 for 循环时无需使 vec_iter 可变因为 for 循环会获取 vec_iter 的所有权并在后台使 vec_iter 可变。
使用iter()迭代器,next方法调用时获得的是vec的不可变引用。如果想要获得vec的所有权,并返回拥有所有权的值,则使用into_iter()迭代器。如果想获得可变引用,则调用iter_mut()迭代器。
迭代器的方法:
sum方法:把迭代中的每一项都加起来(一般用于标量类型)。且sum方法获取迭代器的所有权
map(闭包方法).collect():迭代器中的每一项都执行闭包方法。(collect()方法可以再返回一个Vec<_>类型的值,类型未知,该方法非必须调用,根据情况来调用)
filter(闭包方法):执行闭包方法,并返回bool类型,如果为true,则包含进新的迭代器中,如果为false,则不包含进去。
十二、进一步认识Cargo和Crate.io
12.1 采用发布配置自定义构建
在运行或打包过程中,我们可以对Cargo.toml文件中的 [profile.*]进行配置,以来对包进行优化,默认时,该文件中不显示[profile.*]的配置。
[profile.dev]
opt-level = 0[profile.release]
opt-level = 3
opt-level 设置控制 Rust 会对代码进行何种程度的优化。这个配置的值从 0 到 3。越高的优化级别需要更多的时间编译,所以如果你在进行开发并经常编译,可能会希望在牺牲一些代码性能的情况下减少优化以便编译得快一些。因此 dev 的 opt-level 默认为 0。当你准备发布时,花费更多时间在编译上则更好。只需要在发布模式编译一次,而编译出来的程序则会运行很多次,所以发布模式用更长的编译时间换取运行更快的代码。这正是为什么 release 配置的 opt-level 默认为 3。
在执行命令 cargo build时,默认使用 opt-level = 0,在执行命令 cargo build --release时,则使用opt-level = 3 。
当我们修改了这些配置时,会覆盖掉默认配置
12.2 将crate发布到Crates.io
文档注释 ///(三斜杠)
在文档注释中增加示例代码块是一个清楚的表明如何使用库的方法,这么做还有一个额外的好处:cargo test 也会像测试那样运行文档中的示例代码
文档注释风格 //! 为包含注释的项,而不是位于注释之后的项增加文档。这通常用于 crate 根文件(通常是 src/lib.rs)或模块的根文件为 crate 或模块整体提供文档。这种文档通常是介绍这个结构体的整体介绍,一般写于这个文件的开头
2.1 使用pub use 导出合适的公有API
创建一个crate:
//! # Publish_ctrates
//!
//! 这是一个测试,一辆汽车的颜色和座位数pub use self::car_type::SeatCounts;
pub use self::car_type::Color;
pub use self::action::assemble;
pub mod car_type{/// 这个是汽车的颜色#[derive(Debug)]pub enum Color{红色,蓝色,黑色,白色}/// 这个是汽车的座位数#[derive(Debug)]pub enum SeatCounts {二,四,六,七,三十}
}pub mod action{use crate::car_type::{Color, SeatCounts};/// 组装一辆汽车的颜色和座位数pub fn assemble(color: Color, seat_counts: SeatCounts){println!("这是一辆{:?}的{:?}座汽车", color, seat_counts);}
}
这是创建一个车的例子,执行cargo doc --open后,则会生成一个介绍文档:

然后,我们可以在main函数中调用这个,做测试
在main函数中调用时,我们导入模块往往需要导入很长一串名字例如 publish::car_type::Color,当我们lib目录中的最上面加上 pub use self::car_type::Color时,我们就可以在其他文件中引入时直接写publish::Color,不用再写很长的引入。
2.2 发布crate前的准备工作
1、crate的名称是唯一的,所以如果当前的这个名称在crates库中存在则不能发布
2、需要在 Cargo.toml文件的[package]下面添加协议标识符:lisence = "MIT"

然后就可以尝试使用 cargo publish 发布了
2.3 撤回操作
在文件中运行 cargo yank --vers 版本号 即可撤回
执行 cargo yank --vers 版本号 --undo 则可以取消撤回操作
12.3 Cargo工作空间
3.1 创建工作空间
1、新建文件夹(工作空间)
2、在文件夹内创建Cargo.toml文件,在文件中添加成员:

以这种形式写,members中都是一个成员,即其他二进制crate(main函数文件)或库crate(lib文件)
3、在文件夹内执行cargo new 添加成员,结构如下

在工作空间中,各个包之间不是自动互相依赖的,所以需要手动添加依赖,比如adder中调用add_one中的方法,则在adder的Cargo.toml文件的依赖那添加:add_one = {path = "../add_one"},因为都是在本地调用,所以是写成path,又因为adder与add_one是平级,所以地址是:../add_one ..默认上层及更上层路径
在工作空间中执行某个包中的文件则可以执行 cargo run -p 包名
在工作空间中测试某个包中的文件则可以执行 cargo test -p 包名
在同一工作空间中,某个包引入外部依赖,其他包不能使用的,除非其他包也引入,引入后不会再去下载
12.4 使用 cargo install 安装二进制文件
cargo install 命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。二进制目标 文件是在 crate 有 src/main.rs 或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标文件。通常 crate 的 README 文件中有该 crate 是库、二进制目标还是两者都是的信息。
Cargo 的设计使得开发者可以通过新的子命令来对 Cargo 进行扩展,而无需修改 Cargo 本身。如果 $PATH 中有类似 cargo-something 的二进制文件,就可以通过 cargo something 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 cargo --list 来展示出来。能够通过 cargo install 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是 Cargo 设计上的一个非常方便的优点!
十三、智能指针
智能指针(smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。 例如我们学过的 String 和 Vec
13.1 使用Box指向堆上的数据
Box
Box
1、当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
2、当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
3、当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
1.1 使用Box在堆上存储数据
fn main() {let a = Box::new(1);println!("a = {}", a); // a = 1
}
box把原本存在栈上的数据 1 存到了堆上
1.2 递归类型
递归类型(recursive type)的值可以拥有另一个同类型的值作为其的一部分。
结构:(1, (2, (3, Nil))) ,其中Nil表示没有下一项
use crate::List::{Cons, Nil};fn main() {let list = Cons(1, Cons(2, Cons(3, Nil)));println!("list = {:?}", list)
}
#[derive(Debug)]
enum List{Cons(i32, List),Nil
}
使用rust简单创建了一个递归,但是不能运行,原因是 List 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 List 值到底需要多少空间。
enum所需的空间是其中成员最大的空间大小
使用Box改装改装一下这递归:
use crate::List::{Cons, Nil};fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));println!("list = {:?}", list) // list = Cons(1, Cons(2, Cons(3, Nil)))}
#[derive(Debug)]
enum List{Cons(i32, Box),Nil
}
因为Box
13.2 通过Deref Trait将智能指针当做常规引用处理
我们通常解引用只能解那种使用 & 符号引用的值,但是当实现 Deref Trait后,这个也会被当做是常规引用(相当于使用了 &),可以使用 * 来解引用
示例:
use std::ops::Deref;fn main() {let x = 5;let y = MyBox::new(x);assert_eq!(5, *y);
}
//这是一个只含一个元素的元组结构体
struct MyBox(T);impl Deref for MyBox {type Target = T;fn deref(&self) -> &Self::Target {// 获取元组的第一个元素&self.0}
}impl MyBox {fn new (x: T) -> MyBox{MyBox(x)}
}
我们自定义了一个只含一个元素的元组结构体,然后让他实现了 Deref Trait;type Target = T; 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式(还没学到);
当我们在执行 *y时,相当于执行了*(y.deref())方法
2.1 Deref Trait的强制类型转换
String中实现了Deref Trait,所以可以把 &String转换成&str
use std::ops::Deref;fn main() {let s = MyBox::new(String::from("world"));hello(&s);
}
//这是一个只含一个元素的元组结构体
struct MyBox(T);impl Deref for MyBox {type Target = T;fn deref(&self) -> &Self::Target {// 获取元组的第一个元素&self.0}
}impl MyBox {fn new (x: T) -> MyBox{MyBox(x)}
}fn hello(s: &str){println!("hello {}!", s);
}
&s是&String类型,而hello方法需要的类型是&str,所以&s调用了deref方法,使&String类型转换成&str类型
类似于如何使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
- 当
T: Deref时从&T到&U。 - 当
T: DerefMut时从&mut T到&mut U。 - 当
T: Deref时从&mut T到&U。
头两个情况除了第二种实现了可变性之外是相同的:第一种情况表明如果有一个 &T,而 T 实现了返回 U 类型的 Deref,则可以直接得到 &U。第二种情况表明对于可变引用也有着相同的行为。
第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 不可能 的:不可变引用永远也不能强转为可变引用。
13.3 使用Drop Trait 运行清理代码
Drop Trait是在值在离开作用域时自动执行的代码。rust会自动调用代码中实现了Drop Trait的值的drop方法
fn main() {let myStruct = MyStruct{data: String::from("这才是最后一句话")};println!("这一句是什么?");// 输出的语句:// 这一句是什么?// 被释放前要执行的代码代码:这才是最后一句话
}struct MyStruct{data:String
}impl Drop for MyStruct {fn drop(&mut self) {println!("被释放前要执行的代码代码:{}", self.data)}
}
在执行时,先执行println!的输出语句,然后rust再自动调用myStruct变量实现Drop Trait的drop方法。
有时我们需要提前释放掉某个变量,但是drop方法只会在离开作用域时才会执行,所以我们可以调用std::men::drop方法可以提前释放掉变量,drop方法在此时也会被执行。
use std::mem::drop;
fn main() {let myStruct = MyStruct{data: String::from("我被提前释放掉了")};println!("这一句是什么?");drop(myStruct);println!("这次这句变成了最后一行了");/*输出代码:这一句是什么?被释放前要执行的代码代码:我被提前释放掉了这次这句变成了最后一行了*/
}struct MyStruct{data:String
}impl Drop for MyStruct {fn drop(&mut self) {println!("被释放前要执行的代码代码:{}", self.data)}
}
当我们使用了std::men::drop方法后,myStruct的drop方法被提前执行了,没有在变量离开作用域时执行。
13.4 Rc引用计数智能指针
为了启用多所有权需要显式地使用 Rust 类型 Rc,其为 引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。
Rc
use std::rc::Rc;
use crate::List::{Cons, Nil};fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));let b = Cons(3, Rc::clone(&a));let c = Cons(4, Rc::clone(&a));
}enum List{Cons(i32, Rc),Nil
}
使用Rc::new创建变量,然后使用 Rc::clone(&变量)可以多个地方同时引用该变量,他是一个克隆,但是他不同于String的克隆,这里的克隆只是计数,每次调用时,则计数+1,这个变量相当于共享给其他使用。
使用 Rc::strong_coount(&变量),可以获取被引用的次数
use std::rc::Rc;
use crate::List::{Cons, Nil};fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));println!("初始引用次数:{}", Rc::strong_count(&a)); // 初始引用次数:1let b = Cons(3, Rc::clone(&a));println!("第一次被引用后,引用次数:{}", Rc::strong_count(&a)); // 第一次被引用后,引用次数:2{let c = Cons(4, Rc::clone(&a));println!("第二次在局部作用域中引用次数:{}", Rc::strong_count(&a)); // 第二次在局部作用域中引用次数:3}println!("出了局部作用域后引用次数:{}", Rc::strong_count(&a)); // 出了局部作用域后引用次数:2
}enum List{Cons(i32, Rc),Nil
}
初始创建,默认引用次数为1,每次调用则会+1,当离开作用域时,则会-1
13.5 RefCell与内部可变性模式
内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。
如下为选择 Box,Rc 或 RefCell 的理由:
Rc允许相同数据有多个所有者;Box和RefCell有单一所有者。Box允许在编译时执行不可变或可变借用检查;Rc仅允许在编译时执行不可变借用检查;RefCell允许在运行时执行不可变或可变借用检查。- 因为
RefCell允许在运行时执行可变借用检查,所以我们可以在即便RefCell自身是不可变的情况下修改其内部的值。
有时在测试中程序员会用某个类型替换另一个类型,以便观察特定的行为并断言它是被正确实现的。这个占位符类型被称为 测试替身(test double)。测试替身在运行测试时替代某个类型。mock 对象 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
pub trait Messenger{fn send(&self, msg: &str);
}pub struct LimitTracker<'a, T: Messenger>{messenger: &'a T,value: usize,max: usize
}impl <'a, T> LimitTracker<'a, T>
where
T: Messenger,
{pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T>{LimitTracker{messenger,value: 0,max,}}pub fn set_value(&mut self, value: usize){self.value = value;let percentage_of_max = self.value as f64 / self.max as f64;if percentage_of_max >= 1.0 {self.messenger.send("已经超出限制");}else if percentage_of_max >= 0.9 {self.messenger.send("已经达到90%了");}else if percentage_of_max >= 0.75 {self.messenger.send("已经达到75%了");}}
}#[cfg(test)]
mod tests{use std::cell::RefCell;use crate::{LimitTracker, Messenger};struct MockMessenger{sent_messages: RefCell>,}impl MockMessenger {fn new() -> MockMessenger{MockMessenger{sent_messages: RefCell::new(vec![]),}}}impl Messenger for MockMessenger {fn send(&self, message: &str) {self.sent_messages.borrow_mut().push(String::from(message));}}#[test]fn t_sends_an_over_75_percent_warning_message(){let mock_messenger = MockMessenger::new();let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);limit_tracker.set_value(80);assert_eq!(mock_messenger.sent_messages.borrow_mut().len(), 1);}
}
对于 send 方法的实现,第一个参数仍为 self 的不可变借用,这是符合方法定义的。我们调用 self.sent_messages 中 RefCell 的 borrow_mut 方法来获取 RefCell 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 push 以便记录测试过程中看到的消息。
Recell在运行时记录借用
当创建不可变和可变引用时,我们分别使用 & 和 &mut 语法。对于 RefCell 来说,则是 borrow 和 borrow_mut 方法,这属于 RefCell 安全 API 的一部分。borrow 方法返回 Ref 类型的智能指针,borrow_mut 方法返回 RefMut 类型的智能指针。这两个类型都实现了 Deref,所以可以当作常规引用对待。
结合Rc和Recell来拥有多个可变数据的所有者
RefCell 的一个常见用法是与 Rc 结合。回忆一下 Rc 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell 的 Rc 的话,就可以得到有多个所有者 并且 可以修改的值了!
#[derive(Debug)]
enum List {Cons(Rc>, Rc),Nil,
}use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;fn main() {let value = Rc::new(RefCell::new(5));let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));*value.borrow_mut() += 10;println!("a after = {:?}", a);println!("b after = {:?}", b);println!("c after = {:?}", c);
}
在创建时使用RefCell
13.6 引用循环与内存泄漏
1. 通过循环制造内存泄漏
use std::cell::RefCell;
use std::rc::Rc;
use crate::List::{Cons, Nil};#[derive(Debug)]
enum List{Cons(i32, RefCell>),Nil,
}impl List {fn tail(&self) -> Option<&RefCell>>{match self {Cons(_, item) => Some(item),Nil=> None}}
}
fn main() {let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));println!("a被引用次数:{}", Rc::strong_count(&a)); // a被引用次数:1println!("a的下一次item是{:?}", a.tail()); // a的下一次item是Some(RefCell { value: Nil })let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));println!("a此时的被引用次数:{}", Rc::strong_count(&a)); // a此时的被引用次数:2println!("b被引用的次数:{}", Rc::strong_count(&b)); // b被引用的次数:1println!("b的下一个item是:{:?}", b.tail()); // b的下一个item是:Some(RefCell { value: Cons(5, RefCell { value: Nil }) })if let Some(link) = a.tail(){*link.borrow_mut() = Rc::clone(&b);}println!("b此时被引用次数:{}", Rc::strong_count(&b)); // b此时被引用次数:2println!("a此时被引用次数:{}", Rc::strong_count(&a)); // a此时被引用次数:2
}
当a被b引用后,a的引用次数就变成了2,然后b在if let这又被a引用,所以a和b被引用的次数都是2,所以当程序结束时,a和b,被引用的次数只能放掉1个,所以a和b的实例都还得存在。
注:出现循环的情况主要是RcRc 的 RefCell 值或类似的嵌套结合了内部可变性和引用计数的类型,这都可能会造成内存溢出
2. 使用Weak消除循环
调用 Rc::clone 会增加 Rc 实例的 strong_count,和只在其 strong_count 为 0 时才会被清理的 Rc 实例。你也可以通过调用 Rc::downgrade 并传递 Rc 实例的引用来创建其值的 弱引用(weak reference)。强引用代表如何共享 Rc 实例的所有权。弱引用并不属于所有权关系,当 Rc 实例被清理时其计数没有影响。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
调用 Rc::downgrade 时会得到 Weak 类型的智能指针。不同于将 Rc 实例的 strong_count 加 1,调用 Rc::downgrade 会将 weak_count 加 1。Rc 类型使用 weak_count 来记录其存在多少个 Weak 引用,类似于 strong_count。其区别在于 weak_count 无需计数为 0 就能使 Rc 实例被清理。
因为 Weak 引用的值可能已经被丢弃了,为了使用 Weak 所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak 实例的 upgrade 方法,这会返回 Option。如果 Rc 值还未被丢弃,则结果是 Some;如果 Rc 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option,Rust 会确保处理 Some 和 None 的情况,所以它不会返回非法指针
use std::cell::RefCell;
use std::rc::{Rc, Weak};#[derive(Debug)]
struct Node{value: i32,parent: RefCell>,children: RefCell>>,
}
fn main() {let leaf = Rc::new(Node{value: 3,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![])});println!("leaf的强引用次数:{},弱引用次数:{}",Rc::strong_count(&leaf),Rc::weak_count(&leaf)); // leaf的强引用次数:1,弱引用次数:0println!("leaf的父级是:{:?}", leaf.parent.borrow().upgrade()); // leaf的父级是:None{let branch = Rc::new(Node{value:5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("leaf的强引用次数:{},弱引用次数:{}",Rc::strong_count(&leaf),Rc::weak_count(&leaf)); // leaf的强引用次数:2,弱引用次数:0println!("leaf的父级是:{:#?}", leaf.parent.borrow().upgrade());
/// leaf的父级是:Some(
/// Node {
/// value: 5,
/// parent: RefCell {
/// value: (Weak),
/// },
/// children: RefCell {
/// value: [
/// Node {
/// value: 3,
/// parent: RefCell {
/// value: (Weak),
/// },
/// children: RefCell {
/// value: [],
/// },
/// },
/// ],
/// },
/// },
/// )println!("branch的强引用次数:{},弱引用次数:{}",Rc::strong_count(&branch),Rc::weak_count(&branch)); // branch的强引用次数:1,弱引用次数:1println!("branch的父级是:{:?}", branch.parent.borrow().upgrade()); //branch的父级是:None}println!("leaf的强引用次数:{},弱引用次数:{}",Rc::strong_count(&leaf),Rc::weak_count(&leaf)); // leaf的强引用次数:1,弱引用次数:0println!("leaf的父级是:{:?}", leaf.parent.borrow().upgrade()); // leaf的父级是:None
}
就是使用 Rc::downgrade 会生成一个 Weak
十四、无畏并发
14.1 使用线程同时地运行代码
1. 使用thread::spawn()闭包创建一个线程
use std::thread;
use std::time::Duration;fn main() {thread::spawn(|| {for i in 1..10 {println!("线程内的{}号", i);thread::sleep(Duration::from_secs(1));}});for i in 1..5 {println!("这是主线程{}号", i);thread::sleep(Duration::from_secs(1));}
}
thread::sleep()方法是线程睡眠,上面方法中是每隔一秒钟执行一次
2. 使用join方法等待线程执行结束
thread::spawn 的返回值类型是 JoinHandle。JoinHandle 是一个拥有所有权的值,当对其调用 join 方法时,它会等待其线程结束。
use std::thread;
use std::time::Duration;fn main() {let handle = thread::spawn(|| {for i in 1..10 {println!("线程内的{}号", i);thread::sleep(Duration::from_secs(1));}});handle.join().unwrap(); // 当join方法放在主线程之前,rust会阻塞主线程,先执行完分线程,再执行主线程for i in 1..5 {println!("这是主线程{}号", i);thread::sleep(Duration::from_secs(1));}handle.join().unwrap(); // 当join方法放在主线程后,rust会同时执行主线程和分线程,但是会把分线程执行完。
}
当join方法放在主线程后面时,不管分线程比主线程耗时多多少,都会执行完分线程后,程序才结束
3. 使用move关键字强制获取参数的所有权
在分线程中,在参数前面添加move关键字可以强制获取参数的所有权,防止参数在分线程中还未执行完,而主线程中该参数已经失效。
use std::thread;fn main() {let v = vec![1, 2, 3];let handle2 = thread::spawn(move ||{println!("向量v是:{:?}", v);});handle2.join().unwrap(); // 向量v是:[1, 2, 3]
}
14.2 使用消息传递在线程间通信
为了实现消息传递并发,Rust 标准库提供了一个 信道(channel)实现。信道是一个通用编程概念,表示数据从一个线程发送到另一个线程。
1. 创建信道并发送信息
这里使用 mpsc::channel 函数创建一个新的信道;mpsc 是 多个生产者,单个消费者(multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 发送(sending)端,但只能有一个消费这些值的 接收(receiving)端。
use std::sync::mpsc;
use std::thread;fn main() {// 创建一个信道let (tx, rx) = mpsc::channel();// 创建一个线程thread::spawn(move || {let message = String::from("你好啊");// 通过信道发送消息tx.send(message).unwrap();});// 接收信道消息let receive = rx.recv().unwrap();println!("接收到的消息是:{}", receive); // 接收到的消息是:你好啊
}
使用 mpsc::channel()方法创建一个信道,其返回值是一个元组。生产者在线程内发送消息,消费者在主线程接收线程内生产者发送的消息,生产者和消费者的返回值类型都是Result
2. 多值发送与接收者接收
use std::sync::mpsc;
use std::thread;
use std::time::Duration;fn main() {// 创建一个信道let (tx, rx) = mpsc::channel();// 创建一个线程thread::spawn(move || {for i in 0..10 {// 通过信道发送消息tx.send(i).unwrap();thread::sleep(Duration::from_secs(1));}});for receive in rx {// 消息接收println!("接收到的消息是:{}", receive)}
}
当发送者有多个值发送时,接收者会一一等待,等待所有的值都接收完毕后程序才结束。
注:当有多值时,接收者不再使用显式方法 .recv()方法接收,在for循环中会声场一个迭代器,直接返回打印每次接收到的值,当不再有值返回时,迭代器关闭
3. 通过克隆创建多个发送者
发送这可以通过 clone()方法创建多个发送者。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;fn main() {// 创建一个信道let (tx, rx) = mpsc::channel();// 克隆一个发送者let tx1 = tx.clone();// 创建线程1thread::spawn(move || {for i in 0..10 {// 通过信道发送消息tx.send(i).unwrap();thread::sleep(Duration::from_secs(1));}});// 创建线程2thread::spawn(move || {for i in (0..10).rev() {tx1.send(i).unwrap();thread::sleep(Duration::from_secs(1));}});for receive in rx {// 消息接收println!("接收到的消息是:{}", receive);}
}
14.3 共享状态并发
有时我们需要在多个线程中同时使用一组数据,但是每个线程我们要单独获取其所有权,这样就会造成问题,因此我们有了互斥器,通过互斥器的锁来限制多个线程中同时只能有一个线程获取到这组数据。
1. 互斥器
互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护(guarding)其数据。
互斥器以难以使用著称,因为你不得不记住:
- 在使用数据之前尝试获取锁。
- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
可以使用 Mutex
use std::sync::Mutex;fn main() {let m = Mutex::new(3);{let mut num = m.lock().unwrap();*num = 5;}println!("m的值为:{:?}", m); // m的值为:Mutex { data: 5, poisoned: false, .. }
}
通过new()方法创建一个互斥器,然后使用lock()方法获取锁,获取后,其他线程同一时间不能再获取该变量的所有权;使用unwrap()方法然他在有问题是panic报错,这样可以导致程序结束,这样其他线程也不会再调用该变量。 Mutex
2. 在多个线程中同时获取数据所有权
在多个线程同时获取变量所有权
use std::sync::Mutex;
use std::thread;fn main() {// 在10个线程中对 变量count执行+1操作let count = Mutex::new(0);let mut handles = vec![];for _ in 0..10 {let handle = thread::spawn(move || {let mut num = count.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("count的值为:{:?}", count);
}
join()为了保证所有线程都已经结束。
上面代码会报错,因为线程会获取变量count的所有权,因为是多个线程同时要获取,然而根据rust的特性,一个变量的所有权同时只能存在一个; 所以我们可以用Rc
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;fn main() {// 在10个线程中对 变量count执行+1操作let count = Rc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let count = Rc::clone(&count);let handle = thread::spawn(move || {let mut num = count.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("count的值为:{:?}", count);
}
当使用Rc
因此我们使用一个和Rc
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::thread;fn main() {// 在10个线程中对 变量count执行+1操作let count = Arc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let count = Arc::clone(&count);let handle = thread::spawn(move || {let mut num = count.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("count的值为:{:?}", count); // count的值为:Mutex { data: 10, poisoned: false, .. }
}
RefCell
14.4 使用Sync与Send Traits的可扩展并发
Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。
有两个并发概念是内嵌于语言中的:std::marker 中的 Sync 和 Send trait。
1. 通过Send允许在线程间转移所有权
Send 标记 trait 表明实现了 Send 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是Send 的,不过有一些例外,包括 Rc:这是不能 Send 的,因为如果克隆了 Rc 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,Rc 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
2. Sync允许多线程访问
Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T,如果 &T(T 的不可变引用)是 Send 的话 T 就是 Sync 的,这意味着其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。
智能指针 Rc 也不是 Sync 的,RefCell(第十五章讨论过)和 Cell 系列类型不是 Sync 的。RefCell 在运行时所进行的借用检查也不是线程安全的。
注:手动实现Send和Sync是不安全的。
十五、Rust的面向对象编程特性
15.1 面向对象语言的特征
面向对象的程序是由对象组成的。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法 或 操作。
封装(encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到。在代码中不同的部分使用 pub 与否可以封装其实现细节。
15.2 顾及不同类型值的trait对象
1. 调用trait的两种方式:
// 定义一个trait
pub trait Draw{fn draw(&self);
}
// 结构体使用trai的两种方式
// 方式1 通过结构体的某个字段调用trait,可以时该结构体在使用时可以被定义成任何类型
pub struct Screen{pub components:Vec>,
}
impl Screen {pub fn run(&self){for component in self.components.iter() {component.draw();}}
}// 方法2 该方式使用类实现trait,有局限性,这种方法代码中结构体只能用同一类型。
pub struct Screen{pub components: Vec,
}
impl Screenwhere T: Draw,
{pub fn run(&self){for component in self.components.iter() {component.draw();}}
}
2. 实现trait:
因为结构体的字段调用了trait,所以我们在创建实例时,可以使用实现了trait的结构体,然后通过调用实例自己的方法去实现使用了实现了trait结构体的该方法。
use trait_objects::{Draw, Screen};fn main(){// 创建一个实例let screen = Screen{components: vec![Box::new(Button{width: 10,height: 20,label: String::from("测试一下"),}),Box::new(SelectBox{width: 20,height: 30,option: vec![String::from("二测1"),String::from("二测2"),String::from("二测3"),]})]};// 执行实例的方法来实现trait的方法screen.run();
}// 实现了 Draw Trait的结构体一
struct Button{width: u32,height: u32,label: String,
}impl Draw for Button {fn draw(&self) {println!("实现一")}
}// 实现了 Draw Trait的结构体二
struct SelectBox{width: u32,height: u32,option: Vec,
}
impl Draw for SelectBox {fn draw(&self) {println!("实现二")}
}
3. Trait对象执行动态分发
当对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行 静态分发(static dispatch)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。
当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
ps:本小节就是创建的Trait对象
4. Trait对象需要类型安全
只有对象安全(object-safe)的 trait 可以实现为特征对象。这里有一些复杂的规则来实现 trait 的对象安全,但在实践中,只有两个相关的规则。如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:
- 返回值不是
Self - 没有泛型类型的参数
Self 关键字是我们在 trait 与方法上的实现的别称,trait 对象必须是对象安全的,因为一旦使用 trait 对象,Rust 将不再知晓该实现的返回类型。如果一个 trait 的方法返回了一个 Self 类型,但是该 trait 对象忘记了 Self 的确切类型,那么该方法将不能使用原本的类型。当 trait 使用具体类型填充的泛型类型时也一样:具体类型成为实现 trait 的对象的一部分,当使用 trait 对象却忘了类型是什么时,无法知道应该用什么类型来填充泛型类型。
一个非对象安全的 trait 例子是标准库中的 Clone trait。Clone trait 中的 clone 方法的声明如下:
pub trait Clone {fn clone(&self) -> Self;
}
15.3 面向对象设计模式的实现
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
