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-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

 每个有符号数是gif.latex?-%282%5E%7Bn-1%7D%29gif.latex?2%5E%7Bn-1%7D-1,n为位数,如i8范围gif.latex?-%282%5E%7B7%7D%29gif.latex?2%5E%7B7%7D-1

每个无符号数是0到gif.latex?2%5E%7Bn%7D-1

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;  // 48
    let tup_second = tup.1;  // 7.61
    let 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];  // 1
    let a_second = a[1];  // 2
    let a_third = a[2];  // 3
    let 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 有三种循环:loopwhile 和 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);  // 54

    let 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);  // 测试2

    let 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);  // 15
    println!("{}", s);  // 这是个测试
    println!("{}", len);  // 15

    fn test_s(s : &String) -> usize{
        s.len()
    }
}

2.2. 可变引用

fn main() {
    let mut s = String::from("这是个测试");
    test_s(&mut s);  // &mut 变量名,即可对可变变量引用
    println!("{}", s);  // 这是个测试1
    
    fn 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

字符串 slicestring 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("王五"),
        ..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 = 120
        height: 50 
    };
    println!("矩形的面积是{}", area(&rectangle));  // 矩形的面积是6000
    println!("矩形的信息是{:?}", 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<T> {
    None,
    Some(T),
}

在标准库中定义,用来判断某个值是否为空,可以不需要 Option:: 前缀,来直接使用 Some 和 None, T为值的类型

Some使用:

fn main() {
    let _some = Some(5);
    let _number = _some.unwrap_or(0);
    println!("number的值为{}", _number);  // 5

    let 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<i32> = 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<T>

fn get_one(x: Option<i32>) -> Option<i32>{
    match x {
        None => None,
        Some(i) => Some(i + 1)
    }
}
fn main() {
    let five = get_one(Some(5));
    println!("这个值为:{}", five.unwrap());  // 这个值为:6
    let 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<i32> = 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)从当前模块开始,以 selfsuper 或当前模块的标识符开头。

绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。

使用 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<i32> = Vec::new();
}

这样新建Vector时,需要指定其类型,即Vec<T> 中 T 的类型, <T> 这是一个泛型。

还可以使用 vec!宏,创建一个有初始值的Vector:

fn main() {
    let v1 = vec![6, 8, 32, -99];
}

使用vec!宏创建的Vector,会自动根据值,判断其类型;

注:使用vec!创建的Vector,里面的值的类型必须相同;

2. Vector新增元素和获取元素

fn main() {
    let mut v:Vec<i32> = 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<T>类型,然后根据match匹配进行判空处理;

警告:这种操作是错误的:

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<i32> = 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();  // 新建HashMap
    scores.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();  // 新建HashMap
    scores.insert("语文", 98);  // 往HashMap中添加数据
    scores.insert("数学", 90);

    // 获取HashMap中的值
    let chinese_score = scores.get("数学");
    println!("数学成绩是:{}", chinese_score.copied().unwrap_or(0));

    // 遍历HashMap
    for (key, value) in scores {
        println!("{}的成绩是:{}", key, value);
    }
}

使用 参数名.get(key值) 可获得一个 Option<T>类型的值,然后通过 参数名.copied.unwrap_or(预设值) 来获取value值  .unwrap_or(预设值) 是为了防止值为空

3. 更新HashMap

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();  // 新建HashMap
    scores.insert("语文", 98);  // 往HashMap中添加数据
    scores.insert("数学", 90);

    // 更新HashMap
    scores.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));  // 现在的英语成绩是:96
    println!("现在数学成绩是:{}",  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<T, E> {
    Ok(T),
    Err(E),
}
}

它定义有如下两个成员,Ok 和 Err,T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。而且和Option<T>一样,不需要主动引入

示例:

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()和expect
    let 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<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),
    }
}
}

当有调用者调用该函数时,成功则会返回文件中的内容,失败则会返回错误信息,此时不在控制台打印错误信息了

5. 传播错误的简写:?运算符

#![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)
    // 方法二:链式书写
    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<T>类型和Result<T, E>类型一样,同样适用于 ?运算符

八、泛型、Trait和生命周期

8.1 泛型数据类型

泛型:就是用一个参数代替真实的参数类型,他可以代表任何参数类型,只是定义。(个人理解)

1. 函数定义中使用泛型

fn main() {
    let v1 = vec![1, 6, 156, 4685, -145, 456];
    let largest1 = largest(&v1);
    println!("v1中最大的值为:{}", largest1);  // v1中最大的值为:4685

    let v2 = vec!['a', 'u', 'c', 'z', 'p'];
    let largest2 = largest(&v2);
    println!("v2中最大的字母是:{}", largest2);  // v2中最大的字母是:z
}

fn largest<T:std::cmp::PartialOrd>(list: &[T]) -> &T{
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

函数largest中,使用泛型定义了一个参数 需要在函数名后面加上泛型(<T>)  

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<T> {
    name: T,
    color: T,
}

struct Student<T, U> {
    name: T,
    weight: U,
}

在结构体中使用泛型 只需要在定义结构体时结构体名后面加<T>,即可,然后就可以创建实例时给其字段赋值。

注:在定义结构体时,如果多个字段使用同一个泛型,那么在创建实例时,必须其字段赋的值的类型相同,否则会报错;如果想多个字段为不同类型,可以在定义结构体时泛型设置为多个类型即<T, U, ...>这样就可以给不同的字段赋值不同的类型了;但是也不要定义过多,否则会因为过于复杂造成混乱。

3. 枚举中定义泛型

// rust定义:判断是否非空
enum Option<T> {
    Some(T),
    None,
}
// rust定义:判断异常
enum Result<T, E> {
    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); // 长度为:4

    let 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<T> {
    name: T,
    color: T,
}
struct Point<T> {
    x: T,
    y:T,
}
#[derive(Debug)]
struct Student<T, U> {
    name: T,
    weight: U,
}
// 此处impl定义的泛型和结构体的泛型不一定要一致,只是个代号而已
impl<T> Car<T>{
    fn x(&self) -> &T{
        &self.name
    }
}

impl Car<String> {
    fn connect_test(self) -> String{
        let mut a = self.name;
        a.push_str(&self.color);
        a
    }
}

impl Point<f64> {
    fn get_distance(&self) -> f64 {
        (self.x.powi(3) + self.y.powi(3)).sqrt()
    }
}

impl<T1, U1> Student<T1, U1> {
    fn get_mix<T2, U2>(self, other: Student<T2, U2>) -> Student<T1, U2> {
        Student{
            name: self.name,
            weight: other.weight
        }
    }
}

在方法中定义泛型可以在impl关键字后面加<T>;定义方法时可以具体方法的参数类型,即当结构体创建的实例的参数和定义的方法的参数类型相同时才能调用该方法。

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<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

可以通过 + 号实现多个trait

pub fn notify(item: &(impl Summary + Display)) {}


pub fn notify<T: Summary + Display>(item: &T) {}

通过  where  简写 多个trait形式

pub trait Summary{
    fn summarize(&self) -> String{
        String::from("读更多...")
    }
}

pub trait Get{}

fn get_content<T>(item: &T) -> i32
where
    T: 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
where
    T: 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.21s
     Running unittests src/lib.rs (target/debug/deps/adder-2be33b9b324550dd)

running 2 tests
test tests::method1 ... ok
test tests::method2 ... FAILED

failures:

---- tests::method2 stdout ----
thread 'tests::method2' panicked at '这是个错误的测试', src/lib.rs:16:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::method2

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

2. 使用 assert! 宏来检查结果

assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 trueassert! 什么也不做,同时测试会通过。如果值为 falseassert! 调用 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<T, E>用于测试

#[cfg(test)]
mod tests {
    #[test]
    fn method10() -> Result<(), String>{
        if 2 + 2 == 4{
            Ok(())
        }else {
            Err(String::from("这是错的"))
        }
    }
}

当正确时什么都不返回,当错了,返回Err()中的报错信息。

注:不能对这些使用 Result<T, E> 的测试使用 #[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<String> = 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<String> = 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<String> = 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<Config, &'static str>{
        if args.len() < 3 {
            return Err("没有足够的参数,不能正常打印");
        }
        let query = args[1].clone();
        let file_path = args[2].clone();
        Ok(Config{query, file_path})
    }
}

我们首先使用一个结构体Config来表明我们要获取的内容,然后定义其方法来获取参数值;针对错误处理,我们使用Result<Ok, Error>系统自带返回错误处理

unwrap_or_else方法:为了获取方法中返回的错误结果,然后进行打印;
process::exit(1)方法:可以立即停止程序,并且不会再有额外的输出。

10.4 采用测试驱动开发完善库的功能

我们将遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 minigrep 的搜索逻辑。它遵循如下步骤:

  1. 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
  2. 编写或修改足够的代码来使新的测试通过。
  3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
  4. 从步骤 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<dyn Error>>{
    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<String> = 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<Config, &'static str>{
        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<dyn Error>>{
    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<String> = 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 outputstdout)对应一般信息,标准错误standard errorstderr)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。

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<ShirtColor>,
}

impl Inventory {
    // 公司给成员们的衬衫颜色
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> 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。

  1. FnOnce 适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值移出闭包体的闭包只实现 FnOnce trait,这是因为它只能被调用一次。
  2. FnMut 适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次。
  3. Fn 适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要。

在 Option<T> 上的 unwrap_or_else 方法的定义

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: 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<Self::Item>;

    // 此处省略了方法的默认实现
}

type Item是后面内容先不讲,只知道是个类型,next方法的返回类型为Option<T>类型

 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<T> 也是智能指针。

13.1 使用Box<T>指向堆上的数据

Box<T>是可以把存在栈上的数据放到堆上

Box<T>的使用场景:

1、当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候

2、当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候

3、当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

1.1  使用Box<T>在堆上存储数据

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<List>),
    Nil
}

 因为Box<T>是指针,所以他的空间大小是一定的。

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>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        // 获取元组的第一个元素
        &self.0
    }
}

impl <T> MyBox<T> {
    fn new (x: T) -> MyBox<T>{
        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>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        // 获取元组的第一个元素
        &self.0
    }
}

impl <T> MyBox<T> {
    fn new (x: T) -> MyBox<T>{
        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<Target=U> 时从 &T 到 &U
  • 当 T: DerefMut<Target=U> 时从 &mut T 到 &mut U
  • 当 T: Deref<Target=U> 时从 &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<T>引用计数智能指针

为了启用多所有权需要显式地使用 Rust 类型 Rc<T>,其为 引用计数reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

Rc<T>作用:可以重复不可变引用某个值

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<List>),
    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));  // 初始引用次数:1
    let 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<List>),
    Nil
}

初始创建,默认引用次数为1,每次调用则会+1,当离开作用域时,则会-1

13.5 RefCell<T>与内部可变性模式

内部可变性Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。

如下为选择 Box<T>Rc<T> 或 RefCell<T> 的理由:

  • Rc<T> 允许相同数据有多个所有者;Box<T> 和 RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

有时在测试中程序员会用某个类型替换另一个类型,以便观察特定的行为并断言它是被正确实现的。这个占位符类型被称为 测试替身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<Vec<String>>,
    }

    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<T>在运行时记录借用

当创建不可变和可变引用时,我们分别使用 & 和 &mut 语法。对于 RefCell<T> 来说,则是 borrow 和 borrow_mut 方法,这属于 RefCell<T> 安全 API 的一部分。borrow 方法返回 Ref<T> 类型的智能指针,borrow_mut 方法返回 RefMut<T> 类型的智能指针。这两个类型都实现了 Deref,所以可以当作常规引用对待。

结合Rc<T>和Recell<T>来拥有多个可变数据的所有者

RefCell<T> 的一个常见用法是与 Rc<T> 结合。回忆一下 Rc<T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T> 的 Rc<T> 的话,就可以得到有多个所有者 并且 可以修改的值了!

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    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<T>创建一个局部可变的变量,即可改变其内部值。

13.6 引用循环与内存泄漏

1. 通过循环制造内存泄漏

use std::cell::RefCell;
use std::rc::Rc;
use crate::List::{Cons, Nil};

#[derive(Debug)]
enum List{
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>>{
        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被引用次数:1
    println!("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此时的被引用次数:2
    println!("b被引用的次数:{}", Rc::strong_count(&b));  // b被引用的次数:1
    println!("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此时被引用次数:2
    println!("a此时被引用次数:{}", Rc::strong_count(&a));  // a此时被引用次数:2
}

当a被b引用后,a的引用次数就变成了2,然后b在if let这又被a引用,所以a和b被引用的次数都是2,所以当程序结束时,a和b,被引用的次数只能放掉1个,所以a和b的实例都还得存在。

注:出现循环的情况主要是Rc<T>和RefCell<T>一起被使用,如果你有包含 Rc<T> 的 RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,这都可能会造成内存溢出

2. 使用Weak<T>消除循环

调用 Rc::clone 会增加 Rc<T> 实例的 strong_count,和只在其 strong_count 为 0 时才会被清理的 Rc<T> 实例。你也可以通过调用 Rc::downgrade 并传递 Rc<T> 实例的引用来创建其值的 弱引用weak reference)。强引用代表如何共享 Rc<T> 实例的所有权。弱引用并不属于所有权关系,当 Rc<T> 实例被清理时其计数没有影响。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。

调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针。不同于将 Rc<T> 实例的 strong_count 加 1,调用 Rc::downgrade 会将 weak_count 加 1。Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,类似于 strong_count。其区别在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。

因为 Weak<T> 引用的值可能已经被丢弃了,为了使用 Weak<T> 所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>>。如果 Rc<T> 值还未被丢弃,则结果是 Some;如果 Rc<T> 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option<Rc<T>>,Rust 会确保处理 Some 和 None 的情况,所以它不会返回非法指针

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node{
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}
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,弱引用次数:0
    println!("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,弱引用次数:0
        println!("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,弱引用次数:1
        println!("branch的父级是:{:?}", branch.parent.borrow().upgrade());  //branch的父级是:None
    }
    println!("leaf的强引用次数:{},弱引用次数:{}",
             Rc::strong_count(&leaf),
             Rc::weak_count(&leaf));  // leaf的强引用次数:1,弱引用次数:0
    println!("leaf的父级是:{:?}", leaf.parent.borrow().upgrade());  // leaf的父级是:None
}

就是使用 Rc::downgrade 会生成一个 Weak<T>, 这样会生成一个弱引用,而弱引用是在强引用为0时,不管此时弱引用几次都会直接丢弃,所以不会内存溢出。

十四、无畏并发

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 的返回值类型是 JoinHandleJoinHandle 是一个拥有所有权的值,当对其调用 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<T, E>类型。

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();
    // 创建线程1
    thread::spawn(move || {
        for i in 0..10 {
            // 通过信道发送消息
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }

    });
    // 创建线程2
    thread::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)其数据。

互斥器以难以使用著称,因为你不得不记住:

  1. 在使用数据之前尝试获取锁。
  2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。

可以使用 Mutex<T>来创建一个互斥器

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<T>有和 Deref Trait类似的特性,即可以强制改变其变量的值。

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<T>智能指针修改

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<T>时,系统又会报错,使用Rc<T>在多线程间不安全。

因此我们使用一个和Rc<T>功能类似且安全的指针  Arc<T>(原子引用计数),他一般搭配 Mutex<T>使用。但是Arc<T>不能随便用,他是牺牲了系统性能来实现线程安全的。

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<T>/Rc<T>与Mutex<T>/Arc<T>具有相似特性,Mutex<T>像RefCell<T>提供了内部可变性,所以,Mutex<T>/Arc<T>也会造成死锁导致内存泄漏。

14.4 使用Sync与Send Traits的可扩展并发

Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。

有两个并发概念是内嵌于语言中的:std::marker 中的 Sync 和 Send trait。

1. 通过Send允许在线程间转移所有权

Send 标记 trait 表明实现了 Send 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是Send 的,不过有一些例外,包括 Rc<T>:这是不能 Send 的,因为如果克隆了 Rc<T> 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,Rc<T> 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。

2. Sync允许多线程访问

Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T,如果 &TT 的不可变引用)是 Send 的话 T 就是 Sync 的,这意味着其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。

智能指针 Rc<T> 也不是 Sync 的,RefCell<T>(第十五章讨论过)和 Cell<T> 系列类型不是 Sync 的。RefCell<T> 在运行时所进行的借用检查也不是线程安全的。

注:手动实现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<Box<dyn Draw>>,
}
impl Screen {
    pub fn run(&self){
        for component in self.components.iter() {
            component.draw();
        }
    }
}

// 方法2 该方式使用类实现trait,有局限性,这种方法代码中结构体只能用同一类型。
pub struct Screen<T: Draw>{
    pub components: Vec<T>,
}
impl <T> Screen<T>
    where 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<String>,
}
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 面向对象设计模式的实现

十六、模式与模式匹配

模式Patterns)是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:

  • 字面值
  • 解构的数组、枚举、结构体或者元组
  • 变量
  • 通配符
  • 占位符

16.1 所有可能会用到模式的位置

主要是在讲一些匹配的东西,例如match,if-else等这种匹配的内容

match 表达式必须是 穷尽exhaustive)的,意为 match 表达式所有可能的值都必须被考虑到。

16.2 Refutability(可反驳性):模式是否会匹配失效

模式有两种形式:refutable(可反驳的)和 irrefutable(不可反驳的)。能匹配任何传递的可能值的模式被称为是 不可反驳的irrefutable)。

16.3 模式语法

1. match匹配

match匹配中

1. 可以使用 |(或) 来匹配多个

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

2.  可以使用 ..= 匹配区间来匹配多个 (数字类型和char类型都可以)

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

 注:match 表达式一旦找到一个匹配的模式就会停止检查其它分支

2. 解构并分解值

解构结构体
fn main() {
    let p = Point{
        x: 1,
        y: 2,
    };
    let Point{x,y} = p;
    println!("x的值为:{}", x);  /// x的值为:1
    println!("y的值为:{}", y)  /// y的值为:2
}

struct Point{
    x:i32,
    y:i32,
}
解构枚举
fn main() {
    let msg = Message::Color(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::Color(r, g, b) => {
            println!("Change the color to red {r}, green {g}, and blue {b}",)
        }
    }
}
enum Message{
    Quit,
    Move{x:i32, y:i32},
    Write(String),
    Color(i32, i32, i32),
}

3. 忽略模式中的值

_ 通配符

,也可以使用在方法中

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

fn main() {
    foo(3, 4);
}

没理解他的描述,个人感觉,要是不用直接不用不可以吗,为什么还要把这个函数创建成两个参数的

也可以用于一些其他地方:

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {:?}", setting_value);
}
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}")
        }
    }
}
用 .. 忽略剩余值

..在结构体匹配时,只能用在结尾;在元组中,可以用在开头,结尾,中间(只能是代表开始和结尾中间的所有),在元组时,其他地方不可以

fn main() {
    let p = Point{
        x: 1,
        y: 2,
        z: 3,
    };
    match p {
        Point {x,..} => println!("这是测试"),
        _ => println!("有问题")
    }

    let a = (2,'a', 7, "s");
    match a {
        (..,d) => println!("测试"),
    }
}

struct Point{
    x:i32,
    y:i32,
    z:i32,
}

4. 匹配守卫提供额外条件

匹配守卫match guard)是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。

匹配守卫是可以调用match外部的变量的,如代码中的y 

if n==y 中的 y 则是外部的  y=10的 y。

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

5. @绑定

at 运算符(@)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {}", id),
    }
}

其实绑定就是,当匹配到到这个条件时,把值赋给另一个参数变量。

十七、高级特征

17.1 不安全的Rust

1. 解引用裸指针

安全 Rust 有两个被称为 裸指针raw pointers)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T 和 *mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变 意味着指针解引用之后不能直接赋值。

裸指针与引用和智能指针的区别在于

  • 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
  • 不保证指向有效的内存
  • 允许为空
  • 不能实现任何自动清理功能

使用 as 将不可变和可变引用强转为对应的裸指针类型;且解引用裸指针只能在unsafe模块中使用。

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32; /// 不可变裸指针
    let r2 = &mut num as *mut i32;  /// 可变裸指针

    let address = 0x012345usize;
    let r = address as *const i32;

    unsafe {
        println!("r1是:{},地址是:{:?}", *r1, r1);  /// r1是:5,地址是:0x7ffc1b96b6ec
        println!("r2是:{},地址是:{:?}", *r2, r2);  /// r2是:5,地址是:0x7ffc1b96b6ec
    }
}

2. 调用不安全函数或方法

主要以rust的标准库中的方法split_at_mut(mid: usize)方法为例,该方法使用了unsafe

fn main(){
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let r = &mut v[..];
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1,2,3]);
    assert_eq!(b, &mut [4,5,6]);
}

使用extern函数调用外部代码

/// 使用extern函数调用外部代码
extern "C"{
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("C语言中取绝对值的执行的方法:{}", abs(-3));
    }
}

外部代码调用rust代码

示例是允许C调用Rust代码

fn main() {
    #[no_mangle]
    pub extern "C" fn call_from_c() {
        println!("Just called a Rust function from C!");
    }
}

3. 访问或修改可变静态变量

常量与不可变静态变量的一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是 不安全 的。

/// 访问或修改可变静态变量
static mut COUNT:u32 = 0;
fn add_count(addCount: u32){
    unsafe {
        COUNT += addCount;
    }
}
fn main() {
    add_count(15);
    
    unsafe {
        println!("可变静态变量的值为:{}", COUNT);  /// 可变静态变量的值为:15
    }
}

4. 实现不安全trait

当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe

/// 实现不安全trait
unsafe trait Foo{}

unsafe impl Foo for i32{}

fn main(){}

5. 访问联合体中的字段

仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。

17.2 高级trait

1. 关联类型

关联类型associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

例如,在标准库提供的Iterator trait 里面有个 占位符 Item,当我们在指定占位符类型后,后续则不需要再指定。

struct Counter{
    count:u32,
}

struct Person{
    name:String,
}

impl Person {
    fn new () -> Person{
        Person { name: String::from("测试") }
    }
}

impl Counter {
    fn new() -> Counter{
        Counter { count: 0 }
    }
}

/// 占位符类型指定u32类型
impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5{
            self.count += 1;
            Some(self.count)
        }else {
            None
        }
    }
}

/// 占位符类型指定String类型
impl Iterator for Person {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        todo!()
    }
}

我们在实现trait时,指定其占位符类型,则我们在后续的操作中,不需要再去定义该类型了

2. 默认泛型类型参数

当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。

这种情况的一个非常好的例子是使用 运算符重载Operator overloading),这是指在特定情况下自定义运算符(比如 +)行为的操作。

Add trait,如果不指定泛型类型,则使用实现它的结构体或其他的默认类型。

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

使用<Rhs=Self>去表示使用默认参数类型

用Add trait的默认参数的实现结构体:

use std::ops::Add;

// trait Add<Rhs = Self> {}  Add trait是这样的

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point{
    x:i32,
    y: i32,
}
impl Add for Point {
    type Output = Point;

    fn add(self, other: Self) -> Self::Output {
        Point { x: self.x + other.x,
            y: self.y + other.y }
    }
}
fn main(){
    assert_eq!(Point{x:1,y:0} + Point{x:0, y:1},
    Point{x:1, y:1});
}

默认参数类型主要用于如下两个方面:

  • 扩展类型而不破坏现有代码。
  • 在大部分用户都不需要的特定情况进行自定义。

3. 完全限定语法与消歧义:调用相同名称的方法

情况一:以结构体为例说明:

当结构体本身的方法和他所实现的trait中的方法同名时,优先调用结构体本身的方法,如果想调用trait中的同名方法,可以写成 Trait::方法(&实现此trait的实例)

/// 完全限定语法与消歧义:调用相同名称的方法
trait Pilot{
    fn fly(&self);
}
trait Wizard{
    fn fly(&self);
}
struct Human;
impl Pilot for Human {
    fn fly(&self) {
        println!("Pilot的fly方法!");
    }
}
impl Wizard for Human {
    fn fly(&self) {
        println!("Wizard的fly方法!");
    }
}
impl Human {
    fn fly(&self){
        println!("本身的fly方法!");
    }
}
fn main(){
    let person = Human;
    person.fly();  // 本身的fly方法!
    Pilot::fly(&person);  // Pilot的fly方法!
    Wizard::fly(&person);  // Wizard的fly方法!
}

情况二:前提条件:结构体和所实现的trait存在相同的方法

当存在不包含self的方法函数时,在直接使用的情况下,默认使用结构体本身的该名字的方法,为了消歧义并告诉 Rust 我们希望使用的是 Dog 的 Animal 实现而不是其它类型的 Animal 实现,需要使用 完全限定语法,这是调用函数时最为明确的方式。  如果想使用trait中的该方法,则<Type as Trait>::方法

/// 完全限定语法
trait Animal {
    fn baby_name() -> String;
}
struct Dog;
impl Dog {
    fn baby_name() ->String{
        String::from("小黑")
    }
}
impl Animal for Dog {
    fn baby_name() -> String {
        String::from("大黄")
    }
}
fn main(){
    println!("这个小狗的名字叫:{}", Dog::baby_name()); // 这个小狗的名字叫:小黑
    println!("这个小狗的名字叫:{}", <Dog as Animal>::baby_name());  // 这个小狗的名字叫:大黄
}

3. 在另一个trait中使用父trait的方法

这块有点没明白(可能是汉化的问题),个人理解感觉是,当你的所实现的一个trait,它依赖于另一个trait,我们也要实现。没理解它父trait的概念

/// 在另一个trait中使用父trait的方法
trait OutlinePrint: fmt::Display {
    fn outline_print(&self){
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point{
    x:i32,
    y: i32,
}
impl OutlinePrint for Point {}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f,"({}, {})",self.x,self.y)
    }
}

fn main(){
  let point = Point{x:2, y: 3};
    point.outline_print();
}

4. newtype模式用以在外部类型上实现外部trait

我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用 newtype 模式newtype pattern),它涉及到在一个元组结构体(第五章 “用没有命名字段的元组结构体来创建不同的类型” 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。

简单的来说就是, 一个结构体和一个trait都不是我们本地定义的,比如,都是来自标准库,那这个结构体不能实现这个trait,然而,我们可以使用一个我们自定义单个元素元组结构体包含这个来自标准库的结构体,用这个自定义的结构体去实现这个来自标准库的结构体,这样这个来自标准库的结构体就间接实现了这个trait了

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

此方法的缺点是,因为 Wrapper 是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper 上实现 Vec<T> 的所有方法,这样就可以代理到self.0 上 —— 这就允许我们完全像 Vec<T> 那样对待 Wrapper。(不是很理解)

17.3 高级类型

1. 类型别名

就是可以自定义一个名字,用来代替rust自己定义的类型,使用type关键字

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

这里 Kilometers等同于i32类型,一般自定义的类型首字母大写。

此情况主要用于类型过多过长时

fn main() {
    type Thunk = Box<dyn Fn() + Send + 'static>;

    let f: Thunk = Box::new(|| println!("hi"));

    fn takes_long_type(f: Thunk) {
        // --snip--
    }

    fn returns_long_type() -> Thunk {
        // --snip--
        Box::new(|| ())
    }
}

2. 从不返回的never type

Rust 有一个叫做 ! 的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。

fn bar() -> ! {
    // --snip--
    panic!();
}

个人理解就是原来那种无返回参数的函数或者方法,这个!有没有都无所谓的。

3.  动态大小类型和Sized trait

ust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 动态大小类型dynamically sized types)。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。

为了处理 DST,Rust 提供了 Sized trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 Sized bound。也就是说,对于如下泛型函数定义:

fn generic<T: Sized>(t: T) {
    // --snip--
}

泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

?Sized 上的 trait bound 意味着 “T 可能是也可能不是 Sized” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。这种意义的 ?Trait 语法只能用于 Sized ,而不能用于任何其他 trait。

另外注意我们将 t 参数的类型从 T 变为了 &T:因为其类型可能不是 Sized 的,所以需要将其置于某种指针之后。在这个例子中选择了引用。

17.4 高级函数与闭包

1. 函数指针

我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数满足类型 fn(小写的 f),不要与闭包 trait 的 Fn 相混淆。fn 被称为 函数指针function pointer)。通过函数指针允许我们使用函数作为另一个函数的参数。

就是一个函数的参数是另一个函数。可以通过调用一个函数来调用多个函数来进行数据处理。

fn add_one(x:i32) -> i32{
    x + 1
}
fn do_twice(f : fn(i32)->i32, args: i32)-> i32{
    f(args) + f(args)
}
fn main(){
    let answer = do_twice(add_one, 4);
    println!("结果是:{}", answer);  // 结果是:10
}

这个示例中,add_one是一个简单的函数,do_twice是另一个函数,然而其第一个参数的类型是一个函数类型,满足此类型的函数可被使用,即这个当做参数的函数中的参数是i32类型,且返回值是i32类型,正好add_one函数满足,do_twice函数是让这个函数相加。

迭代器中的map方法的参数也可以这样使用

#[derive(Debug)]
enum Status {
    Value(u32),
    Stop,
}
fn main(){
  /// 把number类型的vector转换成String类型的vector
  let list_number = vec![1, 2, 3];
    let list_string:Vec<String> = list_number.iter().map(ToString::to_string).collect();
    println!("这个集合中有:{:?}",list_string);  // 这个集合中有:["1", "2", "3"]

    /// 把u32类型数字转换成 Status::Value类型的vector
    let list_status: Vec<Status> = (0u32..20).map(Status::Value).collect();
    println!("这里面有:{:?}", list_status);
}

在map方法中,这种无参数的函数,其参数括号要去掉

2. 返回闭包

闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;

fn returns_closure() -> dyn Fn(i32) -> i32 {  // 这样写不行
    |x| x + 1
}

 上面的写法不符合rust的格式,编译前就会报 不知道这个返回值具体预留的内存大小,所以面对这个情况,我们可以使用智能指针Box<T> trait

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {  // 这样写可以
    Box::new(|x| x + 1)
}

17.5 宏

Macro)指的是 Rust 中一系列的功能:使用 macro_rules! 的 声明Declarative)宏,和三种 过程Procedural)宏:

  • 自定义 #[derive] 宏在结构体和枚举上指定通过 derive 属性添加的代码
  • 类属性(Attribute-like)宏定义可用于任意项的自定义属性
  • 类函数宏看起来像函数不过作用于作为参数传递的 token

1. 宏和函数的区别

从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程。所有的这些宏以 展开 的方式来生成比你所手写出的更多的代码。

区别:

1.一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用 println!("hello") 或用两个参数调用 println!("hello {}", name) 。而且,宏可以在编译器翻译代码前展开。  例如:宏可以在一个给定类型上实现 trait。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。

2.实现宏不如实现函数的一面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。

3.在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。

2. 使用macro_rules!声明的宏用于通用元编程 (了解)

例如vec!宏定义(简化的)

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

#[macro_export] 注解表明只要导入了定义这个宏的 crate,该宏就应该是可用的。如果没有该注解,这个宏不能被引入作用域。

接着使用 macro_rules! 和宏名称开始宏定义,且所定义的宏并 不带 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec 。

vec! 宏的结构和 match 表达式的结构类似。

首先,一对括号包含了整个模式。我们使用美元符号($)在宏系统中声明一个变量来包含匹配该模式的 Rust 代码。美元符号明确表明这是一个宏变量而不是普通 Rust 变量。之后是一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。$() 内则是 $x:expr ,其匹配 Rust 的任意表达式,并将该表达式命名为 $x

$() 之后的逗号说明一个可有可无的逗号分隔符可以出现在 $() 所匹配的代码之后。紧随逗号之后的 * 说明该模式匹配零个或更多个 * 之前的任何模式。

当以 vec![1, 2, 3]; 调用宏时,$x 模式与三个表达式 12 和 3 进行了三次匹配。

现在让我们来看看与此分支模式相关联的代码块中的模式:匹配到模式中的$()的每一部分,都会在(=>右侧)$()* 里生成temp_vec.push($x),生成零次还是多次取决于模式匹配到多少次。

3.用于从属性生成代码的过程宏

过程宏:它比较像一种函数过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。

创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。

一个过程宏示例,其中 some_attribute 是一个使用特定宏变体的占位符

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

4. 编写自定义的derive宏(过程宏)

此处暂时略过,大概能看懂,但是没有完全理解,感觉有点像Spring的AOP(了解点java)

5.类属性宏

类属性宏与自定义派生宏相似,不同的是 derive 属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;derive 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。

示例:

创建一个名为 route 的属性用于注解 web 应用程序框架(web application framework)的函数:

#[route(GET, "/")]
fn index() {

#[route] 属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

这里有两个 TokenStream 类型的参数;第一个用于属性内容本身,也就是 GET, "/" 部分。第二个是属性所标记的项:在本例中,是 fn index() {} 和剩下的函数体。

类属性宏与自定义派生宏工作方式一致:创建 proc-macro crate 类型的 crate 并实现希望生成代码的函数!

6.类函数宏

类函数(Function-like)宏的定义看起来像函数调用的宏。类似于 macro_rules!,它们比函数更灵活;类函数宏获取 TokenStream 参数,其定义使用 Rust 代码操纵 TokenStream,就像另两种过程宏一样。

示例: 这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules! 可以做到的更为复杂的处理

let sql = sql!(SELECT * FROM posts WHERE id=1);

定义:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

这类似于自定义派生宏的签名:获取括号中的 token,并返回希望生成的代码。

十八、构建多线程web server

18.1 构建单线程web server

web server 中涉及到的两个主要协议是 超文本传输协议Hypertext Transfer ProtocolHTTP)和 传输控制协议Transmission Control ProtocolTCP)。这两者都是 请求 - 响应request-response)协议,也就是说,有 客户端client)来初始化请求,并有 服务端server)监听请求并向客户端提供响应。请求与响应的内容由协议本身定义。

1.监听TCP连接

监听某个ip端口,使用Listener.bind(ip地址).unwrap()方法来监听

TcpListener 的 incoming 方法返回一个迭代器,它提供了一系列的流(更准确的说是 TcpStream 类型的流)。stream)代表一个客户端和服务端之间打开的连接。连接connection)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。

use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        
        println!("这是在测试监听");
    }
}

2. 读取请求

1.使用BufReader::new()封装stream流,

2.然后使用std::io::Read中的方法lines()一行行读取(BufReader实现Read中的trait),返回一个迭代器

3.使用map()方法收集数据,生成另一个迭代器可进行数据处理的迭代器

4.使用take_while()方法,把满足闭包参数条件的数据进行返回(过滤掉不满足条件的数据)

5.使用collect()方法,把迭代器转换成集合形式

use std::{net::{TcpListener, TcpStream}, io::{prelude::*,BufReader}};


fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        
        handle_connection(stream);
    }
}

fn handle_connection(mut stream:TcpStream){
    let buf_reader = BufReader::new(&mut stream);
    let http_request:Vec<_> = buf_reader
    .lines()
    .map(|result| result.unwrap())
    .take_while(|line| !line.is_empty())
    .collect();

    println!("Request:{:#?}", http_request);

}
// 打印内容

Request:[
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"Connection: keep-alive",
"Cache-Control: max-age=0",
"sec-ch-ua: \"Chromium\";v=\"93\", \" Not;A Brand\";v=\"99\"",
"sec-ch-ua-mobile: ?0",
"sec-ch-ua-platform: \"Linux\"",
"Upgrade-Insecure-Requests: 1",
"User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 UOS Home",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Sec-Fetch-Site: none",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-User: ?1",
"Sec-Fetch-Dest: document",
"Accept-Encoding: gzip, deflate, br",
"Accept-Language: zh-CN,zh;q=0.9",
]

该数据打印的是一些协议和请求头信息

第一行叫做 请求行request line),它存放了客户端请求了什么的信息。请求行的第一部分是所使用的 method,比如 GET 或 POST,这描述了客户端如何进行请求。

请求行接下来的部分是 /,它代表客户端请求的 统一资源标识符Uniform Resource IdentifierURI) —— URI 大体上类似,但也不完全类似于 URL(统一资源定位符Uniform Resource Locators)。

最后一部分是客户端使用的 HTTP 版本,然后请求行以 CRLF 序列 (CRLF 代表回车和换行,carriage return line feed,这是打字机时代的术语!)结束。

3.编写响应

返回一个状态行, 返回协议版本HTTP/1.1  状态码 200(一般默认200表示成功) 响应短语 OK

使用stream流中的write_all()方法可以返回一个&[u8]类型的一些字节

use std::{net::{TcpListener, TcpStream}, io::{prelude::*,BufReader}};


fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        
        handle_connection(stream);
    }
}

fn handle_connection(mut stream:TcpStream){
    let buf_reader = BufReader::new(&mut stream);
    let http_request:Vec<_> = buf_reader
    .lines()
    .map(|result| result.unwrap())
    .take_while(|line| !line.is_empty())
    .collect();

    let response = "HTTP/1.1 200 OK\r\n\r\n";
    stream.write_all(response.as_bytes()).unwrap();

}


因为write_all方法只能接收&[u8]类型,所以,response使用as_bytes()方法转换

4. 返回真正的HTML

就是读取html文件中的内容,然后再页面展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi form Rust.</p>
</body>
</html>
use std::{net::{TcpListener, TcpStream}, io::{prelude::*, BufReader}, fs};


fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        
        handle_connection(stream);
    }
}

fn handle_connection(mut stream:TcpStream){
    let buf_reader = BufReader::new(&mut stream);
    let http_request:Vec<_> = buf_reader
    .lines()
    .map(|result| result.unwrap())
    .take_while(|line| !line.is_empty())
    .collect();

    let status_lines = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();
    let response = format!("{status_lines}\r\nContent_Length:{length}\r\n\r\n{contents}");
    stream.write_all(response.as_bytes()).unwrap();

}


 status_lines 是状态码,这个必返回,contents是读取html文件中的内容,length查了一下说是为了分配空间用的,本人测试一下,不添加暂时也未发现问题。

5. 验证请求并有选择的进行相应

通过获取请求头中,根据请求的路径进行筛选进行响应返回

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>抱歉,您访问的内容不存在!</p>
</body>
</html>
use std::{net::{TcpListener, TcpStream}, io::{prelude::*, BufReader}, fs};


fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        
        handle_connection(stream);
    }
}

fn handle_connection(mut stream:TcpStream){
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();
    println!("打印一下第一行:{}", &request_line);
    if request_line == "GET / HTTP/1.1" {
        let status_lines = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();
        let response = format!("{status_lines}\r\nContent_Length:{length}\r\n\r\n{contents}");
        stream.write_all(response.as_bytes()).unwrap();
    }else {
        let status_lines = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();
        let length = contents.len();
        let response = format!("{status_lines}\r\nContent_Length:{length}\r\n\r\n{contents}");
        stream.write_all(response.as_bytes()).unwrap();
    }
}


代码精简优化:

use std::{net::{TcpListener, TcpStream}, io::{prelude::*, BufReader}, fs};


fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        
        handle_connection(stream);
    }
}

fn handle_connection(mut stream:TcpStream){
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();
    let (status_lines, file_name) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK","hello.html")
    }else {
        ("HTTP/1.1 404 NOT FOUND","404.html")
    };
    let contents = fs::read_to_string(file_name).unwrap();
    let length = contents.len();
    let response = format!("{status_lines}\r\nContent_Length:{length}\r\n\r\n{contents}");
    stream.write_all(response.as_bytes()).unwrap();

}


18.2 将单线程server变成多线程server

勉强看懂,暂时不会描述,不是特别理解。

lib.rs

use std::{thread, sync::{mpsc, Arc, Mutex}};



// 创建线程
pub struct ThreadPool{
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;


impl ThreadPool{
    /// 创建线程池
    ///
    /// 线程池中线程数量
    ///
    /// # Panics
    ///
    /// ‘new’ 函数在size为0时会panic。
    pub fn new(size: usize) -> ThreadPool{
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }
        ThreadPool{workers, sender}
    }

    pub fn execute<F>(&self, f:F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

struct Worker{
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker{
        //  生成一个线程
        let thread = thread::spawn(move || loop {
            // 信道
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {id} got a job; executing.");

            job();
        });

        Worker{id, thread}
    }
}

main.rs

use std::{net::{TcpListener, TcpStream}, io::{prelude::*, BufReader}, fs, thread};
use std::time::Duration;
use web_server1::ThreadPool;


fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();


        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream:TcpStream){
    let buf_reader = BufReader::new(&mut stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();
    let (status_lines, file_name) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK","hello.html"),
        "GET /sleep HTTP/1.1" =>{  // 休眠5秒钟
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK","hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND","404.html")
    };
    let contents = fs::read_to_string(file_name).unwrap();
    let length = contents.len();
    let response = format!("{status_lines}\r\nContent_Length:{length}\r\n\r\n{contents}");
    stream.write_all(response.as_bytes()).unwrap();

}


18.3 优雅停机与处理

......不理解

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值