Rust 学习随笔

一、变量可变性

C/C++ 中,变量默认是可变的。而在 Rust 中,变量默认是不可变的。

    let x = 10;
    //x = 11;		//错误的
    println!("x = {}", x);

它和 const 类型不一样,它可以使用 let 关键字重新绑定新的值,以是同一个变量可以拥有不同的值,并将旧的值隐藏掉了。在 rust 中,这种操作被称作 隐藏。使用 let 时,相当于创建了新的变量,可以改变变量的类型。

    let x = 9;
    let x = x + 11;
    let x = "Rust";
    println!("x = {}", x);   // x = Rust

Rust 中的常量用法如下,它不能使用 let 关键字,因为他们有本质区别。注意 Rust 中的常量声名时必须指定类型,例如 i32

    const MAX:i32 = 10000;
    //let MAX = 100;
    println!("MAX = {}", MAX);

如果想要变量可变,则需要在 let 关键字后面加上 mut 关键字。这种方式是将绑定到 x 的值从 9 改成 10,并没有创建新的变量。与使用 let 关键词隐藏有所区别。

    let mut x = 9;
    x = 10;
    println!("x = {}", x);   // x = 10

1、代码及运行结果

fn main() {
    const MAX:i32 = 10000;
    //let MAX = 100;
    println!("MAX = {}", MAX);

    let x = 10;
    //x = 11;
    println!("x = {}", x);

    let mut x = 9;
    println!("x = {}", x);
    x = 10;
    println!("x = {}", x);

    let x = 9;
    //x = 10;
    let x = x + 11;
    println!("x = {}", x);
    let x = "Rust";
    println!("x = {}", x);
}
C:/Users/uidn2775/.cargo/bin/cargo.exe run --color=always --package study --bin study
   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target\debug\study.exe`
MAX = 10000
x = 10
x = 9
x = 10
x = 20
x = Rust

进程已结束,退出代码0

二、数据类型

Rust 通常可以推断出我们想要的类型,但当多种类型均有可能时,就必须增加注解。因此需要程序员自己去了解想要的类型。

1、标量类型

rust 中的标量类型包括整形、浮点型、布尔类型和字符类型

1.1 整形
长度有符号无符号
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

前几个一看便知,另外isizeusize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。

数字字面值例子
Decimal (十进制)98_222
Hex (十六进制)0xff
Octal (八进制)0o77
Binary (二进制)0b1111_0000
Byte (单字节字符)(仅限于u8)b'A'
1.2 浮点型

Rust 中有两种 浮点数 类型,默认使用 f64,如需使用 f32 需要显示指定类型。

    let f_x = 1.1;
    let f_x1 :f32 = 2.2;
    println!("f_x = {}", f_x);
    println!("f_x1 = {}", f_x1);
1.3 布尔型
    let bo_flag = true;
    println!("bo_flag = {}", bo_flag);
    let bo_flag :bool = false;
    println!("bo_flag = {}", bo_flag);
1.4 字符类型

Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000U+D7FFU+E000U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。

    let c = 'c';
    println!("c = {}", c);
    let z :char = 'ℤ';
    println!("z = {}", z);
    let a:char = '爱';
    println!("a = {}", a);
    let heart_eyed_cat = '😻';
    println!("heart_eyed_cat = {}", heart_eyed_cat);

2、复合类型

Rust 有两种复合类型,即 元组数组

2.1 元组

元组 是一个及将多个不同类型的值进行组合的一个复合类型。元组一旦声明,其长度就固定,不会增大或缩小。

    let tup = (3,'c',"string");
    let (x,y,z) = tup;
    println!("{}", z);   //输出 string

加注解的方式:

    let tup:(i32,char,&str) = (3,'c',"string");
    let (x,y,z) = tup;
    println!("{}", z);   //输出 string

x, y, z 三个变量绑定元组的三个元素,这叫做 模式匹配解构。编译器会进行自动类型推导。除此之外,还可以用 . + 索引的方式来直接访问元组的元素。

    let tup:(i32,char,&str) = (3,'c',"string");
    println!("{}", tup.0);
    println!("{}", tup.1);
    println!("{}", tup.2);
2.2 数组

数组 是多个相同类型的值的组合。数组一旦声明,其长度就固定,不能增大或缩小。可以像这样编写数组:

    let str = ["hello", "world", "hello", "Rust"];
    let string :[i32; 4] = [1, 2, 3, 4];		//带类型注解
    let nums = [9; 5];

	//访问数组
    println!("str[0] = {}", str[0]);			//hello
    println!("string[1] = {}", string[1]);		//2
    println!("nums[2] = {}", nums[2]);			//9

如果数组访问越界,

    let nums = [9; 5];						//[9,9,9,9,9]
    println!("nums[2] = {}", nums[5]);

则会出现如下错误:

   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
error: this operation will panic at runtime
  --> src\main.rs:58:30
   |
58 |     println!("nums[2] = {}", nums[5]);
   |                              ^^^^^^^ index out of bounds: the len is 5 but the index is 5
   |
   = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to previous error

error: could not compile `study`.

To learn more, run the command again with --verbose.

三、函数

Rust 中使用 fn 关键字来声明新函数,括号的使用和 C/C++ 类似。不同的是 Rust 不关心函数定义在之前或之后。

fn main() {
    func();
}

fn func() {
    println!("Hello Rust!");
}

1、带参数

参数的位置与 C/C++ 相同,都在函数名后面的小括号里。需要注意的是必须声明每个参数的类型。这意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。

fn main() {
    func(32,46);
}

fn func(x: i64, y: i32) {
    println!("Hello Rust! X= {}", x);
    println!("Hello YYYY = {}", y);
}

执行结果如下:

   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target\debug\study.exe`
Hello Rust! X= 32
Hello YYYY = 46

进程已结束,退出代码0

如果在函数声明时,不确定参数类型,则会报错:

fn main() {
    func(32,46);
}

fn func(x, y) {
    println!("Hello Rust! X= {}", x);
    println!("Hello YYYY = {}", y);
}
   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
error: expected one of `:`, `@`, or `|`, found `,`
 --> src\main.rs:7:10
  |
7 | fn func(x, y) {
  |          ^ expected one of `:`, `@`, or `|`
  ......

2、返回值

Rust 是一门基于表达式的语言。程序员需要了解表达式和语句的区别以及对函数体的影响。

语句 是执行一些操作但不返回值的指令。

语句不返回值。因此,不能把 let 语句赋值给另一个变量,如:

    let x = (let y = 6);  //错误,(let y = 6) 不是一个表达式而是一个语句

表达式 是计算并产生一个值。

上面代码中语句 let y = 6; 中的 6 是一个表达式,它计算出的值是 6。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块),{},也是一个表达式。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。

Rust 中,不需要为返回值命名,但要在小括号后面加 -> 和声明他的类型。函数的返回值等同于函数体最后一个表达式的值。可以使用 return 关键字和指定值,从函数中提前返回。如下代码:

fn main() {
    println!("func return value = {}", func(32,46));
    println!("func1 return value = {}", func1());
}

fn func(x: i32, y: i32) -> i32 {
    println!("x = {}", x);
    println!("y = {}", y);
    let x = 3;
    let y = 99;
    println!("x = {}", x);
    println!("y = {}", y);
    
    //return x + 1;   //如果解注释的话,就会该函数就会返回 x+1, 即 4
    
    {
        y/x
    }
}

fn func1() -> i32 {
    5
}

执行结果:

   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
     Running `target\debug\study.exe`
x = 32
y = 46
x = 3
y = 99
func return value = 33		// y/x
func1 return value = 5		// 5

进程已结束,退出代码0

四、控制流

1、分支

if 分支以 if 关键字开头,可选的 else ifelse。用法和 C/C++ 用法类似,不同的是判断条件不需要用小括号括起来。例如:

fn main() {
    let number = 9;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

需要注意:Rustif 条件必须是 bool 类型的

1.1 在 let 中使用 if

因为 if 是一个表达式,我们可以在 let 语句的右侧使用它。需要注意的是 if 的每个分支的可能返回值都必须是相同类型

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
        //"six"		//会报错
    };

    println!("The value of number is: {}", number);
}
   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.51s
     Running `target\debug\study.exe`
The value of number is: 5

2、循环

Rust 有三种循环:loopwhilefor

2.1 loop

loop {} 相当于C/C++ 中的 while(1) {},是一个死循环,需要手动停止。

可以用关键字 break 结合 if 检查从循环中返回,并返回检查结果。例如:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

执行结果:

   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.78s
     Running `target\debug\study.exe`
The result is 20

进程已结束,退出代码0
2.2 while

loop 结合 if 退出循环的方式比较麻烦,可以使用条件循环 while

fn main() {
    let mut flag = 5;
    while flag > 0 {
        println!("{} - Hello Rust!", flag);
        flag = flag - 1;
    }
}

执行结果:

   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target\debug\study.exe`
5 - Hello Rust!
4 - Hello Rust!
3 - Hello Rust!
2 - Hello Rust!
1 - Hello Rust!

进程已结束,退出代码0
2.3 for

for 循环一般用来遍历集合。使用 loopwhile 循环来遍历集合不安全或比较麻烦。

如果直接用集合的 index 作为条件,容易出现越界或者遍历长度不够的情况:

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index = index + 1;
    }
}

结合集合的成员函数获取长度可以避免越界,但是比较麻烦:

fn main() {
    let numbers = [1,2,3,4,5,6,7,8,9];
    let mut i = 0;
    while i < numbers.len() {
        println!("numbers[{}] = {}", i, numbers[i]);
        i = i + 1;
    }
}

for 循环可以避免这些问题:

fn main() {
    let numbers = [1,2,3,4,5,6,7,8,9];
    for element in numbers.iter() {
        println!("number = {}", element);
    }
}

执行如下:

   Compiling study v0.1.0 (D:\MyFiles\rustCode\study)
    Finished dev [unoptimized + debuginfo] target(s) in 1.16s
     Running `target\debug\study.exe`
number = 1
number = 2
number = 3
number = 4
number = 5
number = 6
number = 7
number = 8
number = 9

进程已结束,退出代码0

五、所有权

1、所有权规则

  • Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  • 值在任一时刻有且只有一个所有者。
  • 当所有者(变量)离开作用域,这个值将被丢弃。

2、数据作用域

我学习过 C/C++,作用域及内存分配回收的知识不作笔记。

fn main() {
    let a = 99;
    let b = a;
    println!("a = {}, b = {}.", a, b);
}

我们很容易知道,上面的代码是 “将 99 绑定到 x;接着生成一个值 x 的拷贝并绑定到 y”。现在有了两个变量,xy,都等于 99。因为整数是有已知固定大小的简单值,所以这两个 99 被放入了栈中。执行如下

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/study`
a = 99, b = 99.

如果是复杂一点的数据类型,比如带有指针:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("s1 = {}, s2 = {}.", s1, s2);
}

看起来会打印出两个相同的字符串,但其实直接执行这段程序的话会出错。原因是在执行 let s2 = s1 时,变量 s1 就失去了对数据 "hello"所有权。即将所有权交给了 s2Rust 如此的设定叫 移动,既避免了类似深拷贝的问题(数据较大时复制数据对性能的影响),也避免了类似浅拷贝的问题(对同一块内存进行二次释放)。

但是如果我们想继续 s1 的话呢?Rust 也提供类似深拷贝的操作(或引用)可以实现这一点:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    //let s2 = &s1;  //引用
    println!("s1 = {}, s2 = {}.", s1, s2);
}

这样就可以顺利执行:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/study`
s1 = hello, s2 = hello.

注意: 在第一段代码中,像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。所以没有必要在创建变量 b 后使 a 无效。

3、所有权与函数

抄一段代码展示一下函数调用时所有权的状态:

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
    //println!("s = {}", s);      // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
    println!("x = {}", x);        // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("some_string = {}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("some_integer = {}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

再抄一段代码展示 转移返回值的所有权

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值移给 s1
    println!("s1 = {}",s1);

    let s2 = String::from("hello");     // s2 进入作用域
    println!("s2 = {}",s2);

    let s3 = takes_and_gives_back(s2);  // s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
    //println!("s2 = {}",s2); //到这里s2已经失去所有权,不再有效
    println!("s3 = {}",s3);
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -> String {
    
    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

总结:变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

六、引用与借用

1、引用与借用

接所有权那一篇,把变量传递给函数时,其所有权也被移交给函数参数。故在调用函数后,该变量就无法使用了。比如如下代码,获取字符串长度后仍需打印该字符串,编译会报错。

fn main() {
    let str1 = String::from("HAHA");

    let lenth = get_lenth(str1); //这一步 str1 将所有权转移,后面无法使用 str1 访问字符串
	
    println!("str1 = {}, lenth = {}", str1, lenth);
}

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

我们可以在调用完函数后,再用原来的变量或另一个变量接收字符串的所有权。这样便可在调用函数后访问字符串,粗暴地解决这个问题:

fn main() {
    let str1 = String::from("HAHA");

    let (str1, lenth) = get_lenth(str1);

    println!("str1 = {}, lenth = {}", str1, lenth);
}

fn get_lenth(s: String) -> (String, usize){
    let len = s.len();
    (s, len)
}
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/study`
str1 = HAHA, lenth = 4

但是这样每次都传进去再返回来就有点烦人了。Rust对此提供一个功能,就是 引用。使用引用修改上述代码,如下的 get_lenth 函数以对象的引用作为参数,而不是获取字符串的所有权。

fn main() {
    let str1 = String::from("HAHA");

    let lenth = get_lenth(&str1);

    println!("str1 = {}, lenth = {}", str1, lenth);
}

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

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/study`
str1 = HAHA, lenth = 4

&str1 语法让我们创建一个 指向 字符串 str1 的引用,但是并不拥有它。因为并不拥有这个字符串,当引用离开作用域时其指向的数据也不会被丢弃。

上文中获取引用作为函数参数称为 借用borrowing)。需要注意的是,正如变量默认是不可变的,引用/借用也是默认无法修改的。

fn main() {
    let str1 = String::from("HAHA");

    let lenth = get_lenth(&str1);

    println!("str1 = {}, lenth = {}", str1, lenth);
}

fn get_lenth(s: &String) -> usize {
    s.push_str(", Nihao!");  	// 报错
    s.len()
}

2、可变引用

对上面失败的代码略作修改便可以通过编译:

fn main() {
    let mut str1 = String::from("HAHA");	//必须改为 mut

    let lenth = get_lenth(&mut str1);	//必须创建一个可变引用

    println!("str1 = {}, lenth = {}", str1, lenth);
}

fn get_lenth(s: &mut String) -> usize {		//必须接收一个可变引用
    s.push_str(", Nihao!");
    s.len()
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/study`
str1 = HAHA, Nihao!, lenth = 12

注意: 可变引用有一个很大的限制:在特定作用域中的特定数据只能使用一个可变引用。这些代码会失败:

fn main() {
    let mut str1 = String::from("HAHA");

    let str2 = &mut str1;
    let str3 = &mut str1;

    println!("{}, {}", str1, str2);
}

这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race)类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!(来自Rust程序设计语言)

总结: 不论是可变引用还是不可变引用,同一变量在同一作用域可以创建多个引用。需要确保的是,在后一个引用创建出来后,不要 使用 前面的引用。

fn main() {
    let mut a = 9;

    let b = &a;
    println!("{}", b);
    let c = &a;
    println!("{}", c);
    let d = &mut a;
    println!("{}", d);
    let e = &mut a;
    println!("{}", e);
}

执行得:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/study`
b = 9
c = 9
d = 9
e = 9

3、悬垂引用

悬垂引用类似与悬垂指针,意为其指向的内存可能已经被分配给其它持有者。Rust 编译器不允许出现悬垂引用,比如创建一个悬垂引用编译一下:

fn main() {
    let mut str = gen_string();
    println!("{}",str);
}

fn gen_string() -> &String {		//返回一个字符串的引用
    let s = String::from("Love");	//一个新字符串
    &s								//返回字符串
}									//离开作用域并被丢弃,其内存被释放

函数 main 中使用了被释放的变量的引用,这个引用会指向一个无效的 String。这很危险。

不过解决办法很简单,即直接返回 String

fn main() {
    let str = gen_string();
    println!("{}",str);
}

fn gen_string() -> String {
    let s = String::from("Love");
    s
}

执行得:

    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
Love

七、Slice

slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

1、Slice 基本使用

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];		//hello
    println!("{}",hello);
    let world = &s[6..11];		//world
    println!("{}",world);

    let s = String::from("hello");
    let slice = &s[0..2];		//he
    println!("{}",slice);
    let slice = &s[..2];		//he
    println!("{}",slice);

    let len = s.len();
    let slice = &s[3..len];		//lo
    println!("{}",slice);
    let slice = &s[3..];		//lo
    println!("{}",slice);

    let slice = &s[0..len];		//hello
    println!("{}",slice);
    let slice = &s[..];			//hello
    println!("{}",slice);

    let a = [1, 2, 3, 4, 5];
    let b = &a[1..3];			//[2,3]
    for i in b.iter() {
        println!("{}",i);
    }
}
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/study`
hello
world
he
he
lo
lo
hello
hello
2
3

2、Slice 作函数返回值

编写一个函数,该函数接收一个字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应该返回整个字符串。

fn main() {
    let str = String::from("Hello world!");
    let fstwd = first_world(&str);		//String 的引用

    println!("{}",fstwd);
}

fn first_world(s: &String) -> &str {	//返回值为 &str
    let bytes = s.as_bytes();			// as_bytes()方法将 String 转化为字节数组

    for (i, &iter) in bytes.iter().enumerate() {	// enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回
        if iter == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/study`
Hello

3、Slice 作为函数参数

字符串字面值 的类型是 &str (如: let s = "hello";):s 就是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

知道了能够获取字面值和 String 的 slice 后,我们对 first_word 函数进行改进:

fn main() {
    let str = "Hello world!";
    let fstwd = first_world(str);

    println!("{}",fstwd);
}

fn first_world(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &iter) in bytes.iter().enumerate() {
        if iter == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/study`
Hello

八、结构体的基本用法

1、结构体的定义和实例化

结构体的定义和 C\C++ 中结构体的定义很相似,只是成员变量(Rust 中称为字段)用 Rust 的定义方式:

struct Man {
    name:   String,
    age:    u8,
    phone:  String,
    email:  String
}

实例化结构体需要以结构体名字开头,接着使用大括号,并在大括号中使用 键-值对 提供字段:

let xiao_ming = Man {
        name:   String::from("Xiao Ming"),
        age:    27,
        phone:  String::from("18888888888"),
        email:  String::from("18888888888@gmail.com")
    };

完整的例子:

struct Man {
    name:   String,
    age:    u8,
    phone:  String,
    email:  String
}

fn main() {
    let xiao_ming = Man {
        name:   String::from("Xiao Ming"),
        age:    27,
        phone:  String::from("18888888888"),
        email:  String::from("18888888888@gmail.com")
    };

    println!(" name = {}\n age = {}\n phone = {}\n email = {}",xiao_ming.name, xiao_ming.age, xiao_ming.phone, xiao_ming.email);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/study`
 name = Xiao Ming
 age = 27
 phone = 18888888888
 email = 18888888888@gmail.com

与普通变量一样,可以用 mut 关键词实例化一个可变实例,随后可以使用点号为对应的字段赋值:

struct Man {
    name:   String,
    age:    u8,
    phone:  String,
    email:  String
}

fn main() {
    let mut xiao_ming = Man {
        name:   String::from("Xiao Ming"),
        age:    27,
        phone:  String::from("18888888888"),
        email:  String::from("18888888888@gmail.com")
    };

    xiao_ming.name = String::from("Xiao Gong");

    println!(" name = {}\n age = {}\n phone = {}\n email = {}",xiao_ming.name, xiao_ming.age, xiao_ming.phone, xiao_ming.email);

}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/study`
 name = Xiao Gong   //被改变了
 age = 27
 phone = 18888888888
 email = 18888888888@gmail.com

2、从其他实例创建实例

使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有帮助的。这可以通过 结构体更新语法 实现。只需要显示地写出不同的字段,相同的字段则可以使用 ..old_name 指定:

struct Man {
    name:   String,
    age:    u8,
    phone:  String,
    email:  String
}

fn main() {
    let xiao_gong = Man {
        name:   String::from("Xiao Gong"),
        age:    27,
        phone:  String::from("18888888888"),
        email:  String::from("18888888888@gmail.com")
    };

    let xiao_ming = Man {
        name: String::from("Xiao Ming"),	//两个实例不同的字段,如果完全相同,可省略这一行
        ..xiao_gong							//结构体更新语法
    };

    println!(" name = {}\n age = {}\n phone = {}\n email = {}",xiao_ming.name, xiao_ming.age, xiao_ming.phone, xiao_ming.email);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/study`
 name = Xiao Ming
 age = 27
 phone = 18888888888
 email = 18888888888@gmail.com

3、元组结构体

也可以定义与元组类似的结构体,称为 元组结构体 。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。— Rust 程序设计语言

fn main(){
    struct Color(i32,i32,i32);
    
    let blue = Color(0,0,255);

    println!("{}",blue.0);
    println!("{}",blue.1);
    println!("{}",blue.2);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/study`
0
0
255

九、结构体的方法

1、一个示例

如何用 Rust 表示一个矩形?我们知道矩形有 两个属性。考虑使用 struct 去表示矩形的话,struct 中应该含有这两个 字段 。通过这两个属性就可以求出周长和面积。

#[derive(Debug)]
struct Rect {
    len : u32,
    wid : u32
}
fn main() {
    let rect = Rect {
        len : 3,
        wid : 4
    };

    println!(" 矩形 = {:#?}", rect);
    println!(" 周长 = {}", rect_l(&rect));
    println!(" 面积 = {}", rect_s(&rect));
}

fn rect_s(rect : &Rect) -> u32 {
    rect.len * rect.wid
}

fn rect_l(rect : &Rect) -> u32 {
    2 * (rect.len + rect.wid)
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/study`
 矩形 = Rect {
    len: 3,
    wid: 4,
}
 周长 = 14
 面积 = 12

注: println! 宏能处理很多类型的格式,不过,{} 默认告诉 println! 使用被称为 Display 的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display,因为它就是向用户展示 1 或其他任何基本类型的唯一方式。不过对于结构体,println! 应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?由于这种不确定性,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个 Display 实现。可以用 {:#?}{:?} 来输出。— Rust程序设计语言

2、方法语法

上述的例子中,计算面积和周长的函数像是第三方工具,在 main 里拿过来去计算。而和矩形似乎没有直接关系。但是周长和面积是属于矩形的,有没有办法可以让面积和周长与矩形直接关联?

Rust 中结构体有 方法 语法可以满足这一点。

#[derive(Debug)]
struct Rect {
    len : u32,
    wid : u32
}
fn main() {
    let rect = Rect {
        len : 3,
        wid : 4
    };

    println!(" 矩形 = {:#?}", rect);
    println!(" 周长 = {}", rect.rect_l());
    println!(" 面积 = {}", rect.rect_s());
}

impl Rect {
    fn rect_s(&self) -> u32 {
        self.len * self.wid
    }
    
    fn rect_l(&self) -> u32 {
        2 * (self.len + self.wid)
    }
}

使用 impl struct_name{} 定义一个属于结构体的块,并将他的函数在大括号中实现。其中参数 self 是指矩形自身。这些方法的使用方式是 实例名 . 方法名(参数) 执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/study`
 矩形 = Rect {
    len: 3,
    wid: 4,
}
 周长 = 14
 面积 = 12

注: 方法可以选择获取 self 的所有权,所以当并不想获取所有权,只希望能够读取结构体中的数据,而不是写入时,使用不可变引用 &self 。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self

3、多个 impl 块

每个结构体都允许拥有多个 impl 块。例如上述代码更改为下面这段代码,其执行结果与上面的程序一致:

#[derive(Debug)]
struct Rect {
    len : u32,
    wid : u32
}
fn main() {
    let rect = Rect {
        len : 3,
        wid : 4
    };

    println!(" 矩形 = {:#?}", rect);
    println!(" 周长 = {}", rect.rect_l());
    println!(" 面积 = {}", rect.rect_s());
}

impl Rect {
    fn rect_s(&self) -> u32 {
        self.len * self.wid
    }
}

impl Rect {    
    fn rect_l(&self) -> u32 {
        2 * (self.len + self.wid)
    }
}

4、获取另一个实例

这个例子是让矩形提供一个方法,判断 self 矩形是否可以包含住另一个矩形:

#[derive(Debug)] 
struct Rect {
    len : u32,
    wid : u32
}
fn main() {
    let rect1 = Rect {len : 3, wid : 4};
    let rect2 = Rect {len : 5, wid : 6};

    println!(" Rect1 = {:#?}", rect1);
    println!(" Rect2 = {:#?}", rect2);

    println!(" rect1 hold rect2 ? {}", rect1.hold_rect(&rect2));
    println!(" rect2 hold rect1 ? {}", rect2.hold_rect(&rect1));
}

impl Rect {
    // &Rect : 不可变借用,所有权不会转移,
    // 这样就可以在调用这个方法后继续使用被借用的实例
    fn hold_rect(&self, other : &Rect) -> bool {
        self.wid > other.wid && self.len > other.len
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/study`
 Rect1 = Rect {
    len: 3,
    wid: 4,
}
 Rect2 = Rect {
    len: 5,
    wid: 6,
}
 rect1 hold rect2 ? false
 rect2 hold rect1 ? true

5、关联函数

impl 块的另一个有用的功能是:允许在 impl 块中定义 self 作为参数的函数。这被称为 关联函数associated functions),因为它们与结构体相关联。它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过 String::from 关联函数了。

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rect 而不必指定两次同样的值:

#[derive(Debug)]
struct Rect {
    len : u32,
    wid : u32
}
fn main() {
    let rect = Rect::create_cube(5);
    println!("rect = {:#?}", rect);
}

impl Rect {
    fn create_cube(size: u32) -> Rect {
        Rect { wid: size, len: size }
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/study`
rect = Rect {
    len: 5,
    wid: 5,
}

十、枚举的使用

1、枚举的定义

1.1 简单定义

C/C++ 中,枚举的用法相对比较单一。Rust 对枚举这个类型进行了很大的改进。最简单的枚举定义:

#[derive(Debug)]

enum Gender {
    Man,
    Woman
}

fn main() {
    let xiao_ming = Gender::Man;
    println!("{:#?}", xiao_ming);
    let xiao_hong = Gender::Woman;
    println!("{:#?}",xiao_hong);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/study`
Man
Woman
1.2 复杂点的定义

枚举的 每一个成员都可以有他自己的类型 ,可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!比如 Rust 程序设计语言中给出的例子:

#[derive(Debug)]

enum IpAddr {
    V4(String),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(String::from("127.0.0.1"));
    println!("{:#?}", home);
    let loopback = IpAddr::V6(String::from("::1"));
    println!("{:#?}", loopback);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/study`
V4(
    "127.0.0.1",
)
V6(
    "::1",
)

还可以像下面这样定义:

//每个成员拥有不同的类型
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));
//结构体也可以作为枚举的成员,事实上枚举的成员几乎可以是任何类型
struct Ipv4Addr {
    addr : String
}

struct Ipv6Addr {
    addr : String
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
//各种类型成员
enum Message {
    Quit,						//没有关联任何数据。
    Move { x: i32, y: i32 },	//包含一个匿名结构体。
    Write(String),				//包含单独一个 `String`。
    ChangeColor(i32, i32, i32),	//包含三个 `i32`。
}

上面这个枚举相当于下面这个四个结构体的整合,:

struct QuitMessage; // 类单元结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

如果使用不同的结构体,由于它们都有不同的类型,我们将不能像使用 Message 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数,因为枚举是单独一个类型。

2、枚举的方法

就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法。例如:

#[derive(Debug)]

enum IpAddr {
    V4(String),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(String::from("127.0.0.1"));
    home.print();
    let loopback = IpAddr::V6(String::from("::1"));
    loopback.print();
}

impl IpAddr {
    fn print(&self) {
        println!("{:#?}", self);
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/study`
V4(
    "127.0.0.1",
)
V6(
    "::1",
)

3、match 控制流运算符

一个例子,展示不带类型的枚举成员及带类型的枚举成员在 match 中的使用方式:

#[derive(Debug)]

enum IpAddr {
    V4(String),
    V6(String),
    ErrorIp,
}

fn main() {
    let home = IpAddr::V4(String::from("127.0.0.1"));
    println!("{}", get_string(home));
    
    let home = IpAddr::V6(String::from("::1"));
    get_string(home);		//get_string 中 Ip V6 有打印,不需要再次打印

    let home = IpAddr::ErrorIp;
    println!("{}", get_string(home));
}

fn get_string(ip : IpAddr) -> String {		//返回一个 String
    match ip {
        IpAddr::V4(ss) => ss,		//返回
        IpAddr::V6(ss) => {
            println!("{}", ss);
            ss						//打印并返回
        },
        IpAddr::ErrorIp => String::from("Error Ip"),	//返回
    }
}

执行如下:

    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
127.0.0.1
::1
Error Ip

Rust 也提供了一个模式用于不想列举出所有可能值的场景。例如,u8 可以拥有 0 到 255 的有效的值,如果我们只关心 1、3、5 和 7 这几个值,就并不想必须列出 0、2、4、6、8、9 一直到 255 的值。所幸我们不必这么做,可以使用特殊的模式 _ 替代:

fn main() {
    let some_u8_value = 8;
    match some_u8_value {
        1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),				//这个程序不会有任何输出
    }
}

4、if-let 简洁控制流

if let 语法让我们以一种不那么冗长的方式结合 iflet,来处理 只匹配一个 模式的值而忽略其他模式的情况。当不匹配时,还可以用 else 去处理。

#[derive(Debug)]
enum Message {
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
    RGB(u32,u32,u32),
}

fn main() {
    let mut msg = Message::Write(String::from("Hello Rust"));
    if let Message::Write(str) = msg {
       println!("{}",str)
    }
    msg = Message::ChangeColor(5, 5, 255);
    if let Message::RGB(r,g,b) = msg {
        println!("{} {} {}", r, g, b)
    }
    else {
        println!("Not Messge::RGB()");
    }
    msg = Message::Move{x:3, y:4};
    if let Message::Move{x, y} = msg {
        println!("{} {}", x, y)
    }
}

执行如下:

Hello Rust
Not Messge::RGB()
3 4

十一、项目的管理

1、包和 crate

crate 是一个 二进制项 或者 crate root 是一个 源文件Rust 编译器以它为起始点,并构成你的 crate 的根模块。 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件。

中含有内容的规则:

  • 一个包中至多 只能 包含一个库 crate (library crate);
  • 包中可以包含任意多个二进制 crate(binary crate);
  • 包中至少包含一个 crate,无论是库的还是二进制的。

2、模块

先提出几个关键词,方便后面的理解:

mod :用于定义一个模块,模块中可以内嵌模块,也可以实现函数;

pub :用于修饰模块,函数,变量等,使其在外部也可以访问使用;

use :用于将外部名称引入作用域。

用一个例子来理解所有的知识点。注意看代码注释。首先看我的目录结构。使用如下个命令就可以得到:

cargo new study
cd study
cargo new --lib libs/front
cargo new --lib libs/back

在这里插入图片描述

./libs/back/src/lib.rs 中写入代码:

pub mod back_of_house {
    pub mod cooker {
        pub fn do_cook() {
            println!("do cook");
        }
        fn add_oil(){
            println!("add oil");
        }
    }

    mod assiter {
        fn assit() {
            println!("assit");
        }
    }
}

模块树如下,后面的代码模块树以此类推,很好理解:

crate
 └── back_of_house
     ├── cooker
     │   ├── do_cook
     │   └── add_oil
     └── assiter
         └── assit

./libs/front/src/lib.rs 中写入代码:

pub mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("add to wait list");
        }
    }
}

mod others {
    fn function() {
        println!("other function");
    }
    pub fn function1(){
        println!("other function1");
    }
}

pub fn welcome() {
    println!("welcome");
    //模块others与函数welcome同级,默认可访问,但模块内部仍需看是否pub
    //others::function();  //函数function未用pub修饰,不可访问
    others::function1();   //函数function1用pub修饰,可访问
    front_of_house::hosting::add_to_waitlist();
}

//use front_of_house::hosting;  //相对路径
use crate::front_of_house::hosting;   //绝对路径
pub fn welcome1() {
    hosting::add_to_waitlist();
}

backfront 对于 ./src/main.rs 来说是外部包,需要改写 ./Cargo.toml 文件:

[package]
name = "study"
version = "0.1.0"
authors = ["wlb"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
back = { path = "libs/back" }
front = { path = "libs/front" }

对于自己实现的本地包,需要用以上的指定路径方式引入。如果是 crates.io 中的包,则可用 包名 = “版本” 引入。例如 rand = "0.8.0"

此时在 ./src/main.rs 中用 use 关键词就可以将 backfront 包中的内容引入:

use back::back_of_house::cooker;
//use back::back_of_house::assiter;  //错误,assiter模块未用pub修饰,为私有模块
use front::front_of_house::hosting;
//use front::others;   //错误,front包中,others模块未用pub修饰,则为私有,故无法引入

fn main() {
    cooker::do_cook();
    //cooker::do_oil();   //错误,函数do_oil为私有
    hosting::add_to_waitlist();
    front::welcome();
}

执行如下,有一些函数未使用的警告,将其删除以免影响阅读:

   Compiling back v0.1.0 (/home/wlb/Documents/codes/rust/study/libs/back)
   Compiling front v0.1.0 (/home/wlb/Documents/codes/rust/study/libs/front)
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/study`
do cook									//cooker::do_cook(); 打印
add to wait list						//hosting::add_to_waitlist(); 打印
welcome									//front::welcome(); 打印
other function1							//front::welcome(); 打印
add to wait list						//front::welcome(); 打印

3、将模块拆分进不同文件

当项目越来越大时,模块不可能都放在同一个文件中。所幸 Rust 提供了解决方案。将目录结构拆分成如下:

在这里插入图片描述

拆分时需要注意,不要改变其原来的模块树的路径关系。

./libs/back/src/lib.rs :

pub mod back_of_house;

./libs/back/src/back_of_house.rs :

pub mod cooker;

mod assiter;

./libs/back/src/back_of_house/assiter.rs :

fn assit() {
    println!("assit");
}

./libs/back/src/back_of_house/cooker.rs :

pub fn do_cook() {
    println!("do cook");
}
fn add_oil(){
    println!("add oil");
}

./libs/front/src/lib.rs :

pub mod front_of_house;

mod others {			//这个模块也可以拆分,我懒
    fn function() {
        println!("other function");
    }
    pub fn function1(){
        println!("other function1");
    }
}

pub fn welcome() {
    println!("welcome");
    //模块others与函数welcome同级,默认可访问,但模块内部仍需看是否pub
    //others::function();  //函数function未用pub修饰,不可访问
    others::function1();   //函数function1用pub修饰,可访问
    front_of_house::hosting::add_to_waitlist();
}

//use front_of_house::hosting;  //相对路径
use crate::front_of_house::hosting;   //绝对路径
pub fn welcome1() {
    hosting::add_to_waitlist();
}

./libs/front/src/front_of_house.rs :

pub mod hosting;

./libs/front/src/front_of_house/hosting.rs :

pub fn add_to_waitlist() {
    println!("add to wait list");
}

./src/main.rs 不需要变,执行结果和拆分之前是相同的。

如此,就算项目后期由于各模块体积不断增大,也可以在不修改主要代码的前提下对模块进行拆分。使项目的后期维护更加容易。

十二、Vector

1 新建 vector 的两种方式

vector 是用泛型实现的,类似 C++ 的模板,需在尖括号里指明数据类型。不过大部分情况 Rust 都可以自动推导出来,无需程序员手动指明。

//用宏新建,直接给了数据,Rust可以自行推算出类型    
let mut v = vec![1,2,3];
//使用 new 函数,没有直接给数据,需要进行类型标注
let mut vec: Vec<i32> = Vec::new();

2 更新 vector

对新建的 vector 可以使用 push 方法追加数据元素。可以使用 [index] 的方式修改已有的元素。

let mut vec: Vec<i32> = Vec::new();
vec.push(0);
vec.push(1);
vec.push(2);
vec[0] = 100;

3 读取 vector 的两种方式

fn main() {
    let mut v = vec![1,2,3];

    v.push(3);
    v.push(4);
    v.push(5);
    for i in v.iter() {
        println!("{}", i);
    }

    v[0] = 999;
    for i in v.iter() {
        println!("{}", i);
    }

    let first = &v[0];		//1. 使用索引语法
    println!("v[0] = {}", first);

    match v.get(0) {		//2. 使用 get 方法
        Some(value) => println!("v[0] = {}", value),
        None => println!("Error: No this value"),	//如果请求的index不存在
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/study`
1
2
3
3
4
5
999
2
3
3
4
5
v[0] = 999
v[0] = 999

当运行这段代码,你会发现对于第一个 [] 方法,当引用一个不存在的元素时 Rust 会造成 panic。

get 方法被传递了一个数组外的索引时,它不会 panic 而是返回 None。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 Some(&element)None 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 None 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!

尝试编译以下两段程序,会发现第 2 段程序无法编译,因为其同时拥有不可变引用和一个可变引用:

fn main() {
    let mut v = vec![1,2,3];

    let first = &v[0];
    println!("v[0] = {}", first);

    v.push(4);
}
fn main() {
    let mut v = vec![1,2,3];

    let first = &v[0];
    v.push(4);
    println!("v[0] = {}", first);
}

4 Vector 的遍历

fn main() {
    let mut v = vec![1,2,3];

    for index in &mut v {	//可变引用,遍历时可以修改其元素
        *index += 1;
    }

    for index in &v {		//不可变引用,元素只读
        println!("{}", index);
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/study`
2
3
4

:遍历时一般不用 for index in v {} ,因为这种方式会取得 vector 的所有权。例如:

fn main() {
    let mut v = vec![1,2,3];

    for index in v {
        println!("{}", index);
    }

    v.push(4);
}

编译时会报错 error[E0382]: borrow of moved value: 'v'

5 使用枚举使 vector 存储更多类型的数据

vector 只能储存相同类型的值,这很不方便使用。还好 Rust 中有枚举可以帮助 vector 来存储不同类型的数据。

enum Data {
    Int(i32),
    Double(f64),
    Str(String),
}
fn main() {
    let mut vec: Vec<Data> = Vec::new();
    vec.push(Data::Int(99));
    vec.push(Data::Double(99.99));
    vec.push(Data::Str(String::from("Hello Rust")));
    for index in &vec {
        match index {
            Data::Int(value) => println!("{}", value),
            Data::Double(value) => println!("{}", value),
            Data::Str(value) => println!("{}", value),
        }
    }

    let vv = vec![
        Data::Int(99),
        Data::Double(99.99),
        Data::Str(String::from("Hello Rust"))
    ];
    for index in &vv {
        match index {
            Data::Int(value) => println!("{}", value),
            Data::Double(value) => println!("{}", value),
            Data::Str(value) => println!("{}", value),
        }
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.44s
     Running `target/debug/study`
99
99.99
Hello Rust
99
99.99
Hello Rust

后记: Rust 的文档很不错,更多集合的 API 可以去网站上查看:https://doc.rust-lang.org/stable/std/vec/

十三、字符串

1、定义字符串

fn main() {
    //新建一个空的 String
    let mut s1 = String::new();
	//使用 to_string 方法从字符串字面值创建 String
    let s2 = "Hello Rust".to_string();
	//使用 String::from 函数从字符串字面值创建 String
    let s3 = String::from("Hello Rust");
}

2、追加字符串

2.1 push_str

可以通过 push_str 方法来附加字符串 slice,从而使 String 变长:

fn main() {
    let mut s3 = String::from("Hello Rust");
    println!("{}", s3);

    s3.push_str("!");
    println!("{}", s3);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/study`
Hello Rust
Hello Rust!

push_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权。(Slice 是一种没有所有权的数据类型,也就不存在丢失所有权而无法访问)。比如如下代码,s 并不会在作为参数传递后就无法访问:

fn main() {
    let mut s3 = String::from("Hello Rust");
    println!("{}", s3);

    let s = "!!!";
    s3.push_str(s);
    println!("{}", s3);
    println!("{}", s);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/study`
Hello Rust
Hello Rust!!!
!!!
2.2 push

push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中,单字符使用单引号。

fn main() {
    let mut s3 = String::from("Hello Rust");
    println!("{}", s3);

    s3.push('!');
    println!("{}", s3);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/study`
Hello Rust
Hello Rust!

3、字符串拼接

3.1 加号运算符
fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rust");

    println!("{}", s1 + &s2);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/study`
HelloRust

可以注意到一点,s2 使用了 &,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 + 运算符使用了 add 函数,这个函数签名看起来像这样:

fn add(self, s: &str) -> String {

add 函数的 s 参数:只能将 &strString 相加,不能将两个 String 值相加。不过等一下 —— 正如 add 的第二个参数所指定的,&s2 的类型是 &String 而不是 &str。为什么没有编译出错?

是因为 &String 可以被 强转coerced)成 &str。当add函数被调用时,Rust 使用了一个被称为 解引用强制多态deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]

由于 add 没有获取参数 s2 的所有权,所以 s2 在这个操作后仍然是有效的 String。而 add 获取了 self 的所有权,因为 self 没有 使用 &。这意味着 s1 的所有权将被移动到 add 调用中,之后就不再有效。比如

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rust");

    println!("{}", s1 + &s2);
    println!("{}", s1);
}

编译会报错:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:7:20
  |
3 |     let s1 = String::from("Hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
...
6 |     println!("{}", s1 + &s2);
  |                    -- value moved here
7 |     println!("{}", s1);
  |                    ^^ value borrowed here after move
......
3.2 format!

对于更为复杂的字符串链接,或级联多个字符串,+ 的行为就显得不太好用。可以使用 format! 宏:

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rust");
    let s3 = String::from("!");

    let s4 = format!("{} {}{}", s1, s2, s3);
    println!("{}", s4);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.44s
     Running `target/debug/study`
Hello Rust!

4、字符串的索引

大部分语言中,都可以通过中括号进行索引,从而获取字符串中的数据。但是 Rust 不允许索引,可以编译如下代码以获得报错信息:

fn main() {
    let s1 = String::from("Hello");
    println!("{}", s1[0]);
}

编译如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
error[E0277]: the type `String` cannot be indexed by `{integer}`
 --> src/main.rs:4:20
  |
4 |     println!("{}", s1[0]);
  |                    ^^^^^ `String` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for `String`
  。。。。。。

String 本质上是一个 Vec<u8> 的封装。而 String 使用 UTF-8 编码,每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。即便这个字符串只有拉丁字母,为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。

下面摘抄一段文字解释一下 字节,标量值 和 字形簇

这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。

比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 u8 值看起来像这样:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]

这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char 类型那样,这些字节看起来像这样:

['न', 'म', 'स', '्', 'त', 'े']

这里有六个 char,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:

["न", "म", "स्", "ते"]

Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。

最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。

5、字符串 slice

索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice

fn main() {
    let s1 = String::from("Здравствуйте");
    let s2 = "Здравствуйте";
    println!("{}", &s1[..4]);
    println!("{}", &s2[..4]);
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/study`
Зд
Зд

如果获取 &s1[0..5] 会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样:

fn main() {
    let s1 = String::from("Здравствуйте");
    println!("{}", &s1[..5]);
}

编译会报错如下,意为切片的位置不在字符的边界值:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/study`
thread 'main' panicked at 'byte index 5 is not a char boundary; it is inside 'р' (bytes 4..6) of `Здравствуйте`', src/main.rs:4:21
。。。。。。

6、遍历字符串

6.1 操作单独的 Unicode 标量值

如果你需要操作单独的 Unicode 标量值,最好的选择是使用 chars 方法。

fn main() {
    let s1 = String::from("Здравствуйте");
    for c in (&s1).chars() {	//&的优先级低于 . 号,需要括号
        println!("{}", c);
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/study`
З
д
р
а
в
с
т
в
у
й
т
е
6.2 操作每一个原始字节
fn main() {
    let s1 = String::from("Здравствуйте");
    for c in (&s1).bytes() {
        println!("{}", c);
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33s
     Running `target/debug/study`
208
151
208
180
209
128
208
176
208
178
209
129
209
130
208
178
209
131
208
185
209
130
208
181

有效的 Unicode 标量值可能会由不止一个字节组成 。从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。

十四、哈希 map

1、一个程序搞定哈希 map基本操作

一些知识点:

  • 可以使用 new 创建一个空的 HashMap,并使用 insert 增加元素。
  • 对于像 i32 这样的实现了 Copy trait 的类型,其值可以拷贝进哈希 map。对于像 String 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。
  • 可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 for 循环。
  • 可以通过 get 方法并提供对应的键来从哈希 map 中获取值, get 返回 Option<V>
  • 如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即 insert 可以用来覆盖一个值。
  • entry(key) 方法检查是否有值,没有就用 or_insert(value) 插入。
  • 可以获取 HashMap 中某个值的可变引用,并对其进行修改。

尝试阅读如下程序去理解上述知识点:

use std::collections::HashMap;

fn main() {
    let mut hm = HashMap::new();
    let key = 3;
    let value = String::from("C");

    hm.insert(1, String::from("A"));
    hm.insert(2, String::from("B"));
    hm.insert(key, value);

    println!("key = {}\n", key);
    //println!("{}", value); //错误,已经丢失了所有权

    //用循环遍历HashMap
    for iter in &hm {
        println!("{}:{}", iter.0, iter.1);
    }
    println!("");

    //用 get(key) 方法获取 value
    match hm.get(&2) {
        Some(val) => println!("2:{}\n", val),
        None => println!("No this value"),
    };

    //覆盖一个值
    hm.insert(2, String::from("b"));

    //只在map中没有对应值时才插入
    hm.entry(1).or_insert(String::from("a")); //不会插入
    hm.entry(4).or_insert(String::from("D")); //会插入

    //用循环遍历HashMap,模式匹配
    for (k,v) in &hm {
        println!("{}:{}", k, v);
    }
    println!("");

    //获取key=4对应的值的可变引用,并修改其值
    let str = hm.entry(4).or_insert(String::from("D"));
    *str = String::from("d");
    
    //用循环遍历HashMap,模式匹配
    for (k,v) in &hm {
        println!("{}:{}", k, v);
    }
}

执行如下:

   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/study`
key = 3

3:C		//遍历获取的
1:A
2:B

2:B  	//get方法获取的

2:b		//B被覆盖为b
4:D
3:C
1:A

2:b		
4:d		//D被通过可变引用修改为d
3:C
1:A

十五、错误处理

Rust 将错误组合成两个主要类别:可恢复错误recoverable)和 不可恢复错误unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是,有可恢复错误 Result<T, E> ,和不可恢复(遇到错误时停止程序执行)错误 panic!

1、panic! 与不可恢复的错误

Rust 有 panic! 宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug,而且程序员并不清楚该如何处理它。

注: 当出现 panic 时,程序默认会开始 展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml[profile] 部分增加 panic = 'abort',可以由展开切换为终止。

[profile.release]
panic = 'abort'
1.1 使用 panic! 的 backtrace

backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。

来看看另一个因为我们代码中的 bug 引起的别的库中 panic! 的例子:

fn main() {
    let integer = vec![1,2,3,4];
    integer[100];
}

编译执行如下:

[wlb@Arco study]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 100', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

可见,当 缓冲区溢出 了,Rustpanic! 。并指出错误的位置在 第四行,第五个字符。如果我们想看出现错误的地方在库里的位置,可以将 RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取 backtrace 看看。

[wlb@Arco study]$ RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 100', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:493:5
   1: core::panicking::panic_fmt
             at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/panicking.rs:92:14
   2: core::panicking::panic_bounds_check
             at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/core/src/panicking.rs:69:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /home/wlb/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/index.rs:184:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /home/wlb/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/index.rs:15:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /home/wlb/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:2384:9
   6: study::main
             at ./src/main.rs:4:5
   7: core::ops::function::FnOnce::call_once
             at /home/wlb/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

注: 为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样。

2、Result 与可恢复的错误

2.1 匹配不同错误

例如一个打开文件的程序。它可能由于文件不存在,文件没有权限等引起打开失败。这里有些失败是可以恢复的,例如文件不存在。我们只需要创建他就可以了。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = match File::open("./hello.rs") {
        Ok(file) => file,	//打开文件成功
        Err(e) => match e.kind() {	//匹配错误类型
            ErrorKind::NotFound => match File::create("./hello.rs") { //文件不存在则创建
                Ok(file_c) => file_c,	//创建文件成功
                Err(error) => panic!("{:?}", error),	//创建失败
            },
            other_error => panic!("{:?}", other_error),  //其他错误类型      
        }
    };
    println!("{:?}", f);	//打印变量
}

确认当前目录下没有 hello.rs ,然后编译执行以上程序,再检查当前目录。可见成功解决了文件未找到的错误,此为可恢复:

[wlb@Arco study]$ ls
Cargo.lock  Cargo.toml  src  target
[wlb@Arco study]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
File { fd: 3, path: "/home/wlb/Documents/codes/rust/study/hello.rs", read: false, write: true }
[wlb@Arco study]$ ls
Cargo.lock  Cargo.toml  hello.rs  src  target
2.2 失败时 panic 的简写:unwrap 和 expect
  • unwrap :如果返回的 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!。不允许自定义错误信息。
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  • expectexpectunwrap 的使用方式一样,不过它允许我们选择 panic! 的错误信息。
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("cannot open this file!");
}

执行如下,可以见到我们自定义的错误信息:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
thread 'main' panicked at 'cannot open this file!: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2.3 传播错误

当我们写库时,我们无从得知调用者会如何处理返回的值。例如,如果他们得到了一个 Err 值,他们可能会选择 panic! 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。

例如一个打开文件并获取文件中字符串的程序:

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("./hello.rs");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

fn main(){
    match read_username_from_file(){
        Ok(s) => println!("{}", s),
        Err(e) => println!("{:?}", e),
    }
}

接口 read_username_from_file 并没有处理 File::open 函数和 read_to_string 方法的返回值,而是将其返回,交由调用者去处理。执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/study`
Os { code: 2, kind: NotFound, message: "No such file or directory" }  //文件不存在,panic
[wlb@Arco study]$ echo wlb >> hello.rs 	//创建文件并写入字符
[wlb@Arco study]$ ls
Cargo.lock  Cargo.toml  hello.rs  src  target
[wlb@Arco study]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
wlb			//成功获得

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

Result 值之后的 ? 被定义为与处理 Result 值的 match 表达式有着完全相同的工作方式。如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 ErrErr 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

上述接口可以用 运算符缩短代码:

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("./hello.rs")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main(){
    match read_username_from_file(){
        Ok(s) => println!("{}", s),
        Err(e) => println!("{:?}", e),
    }
}

甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码:

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("./hello.rs")?.read_to_string(&mut s)?;

    Ok(s)
}

fn main(){
    match read_username_from_file(){
        Ok(s) => println!("{}", s),
        Err(e) => println!("{:?}", e),
    }
}

这三个程序的行为是一致的,执行结果也是一致的。

:将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了。

use std::io;
use std::fs;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("./hello.rs")
}

fn main(){
    match read_username_from_file(){
        Ok(s) => println!("{}", s),
        Err(e) => println!("{:?}", e),
    }
}

执行得:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
Os { code: 2, kind: NotFound, message: "No such file or directory" }
[wlb@Arco study]$ echo wlb >> hello.rs 	//创建文件并写入字符
[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
wlb

3、何时 panic!

https://rust.bootcss.com/ch09-03-to-panic-or-not-to-panic.html

十六、泛型

1、在函数定义中使用泛型

fn return_self<T>(value: T) -> T {
    value
}
fn main() {
    let a = 5;
    let b = 5.0;
    let s = "Hello";
    println!("{}", return_self(a));
    println!("{}", return_self(b));
    println!("{}", return_self(s));
}

编译执行如下:

[wlb@Arco study]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/study`
5
5
Hello

2、结构体定义中的泛型

struct Point<T> {
    x: T,
    y: T,
}
fn main() {
    let m = Point{x: 3, y: 5};
    let n = Point{x: 4, y: 4};
    println!("({},{})", m.x, m.y);
    println!("({},{})", n.x, n.y);
}

编译执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
(3,5)
(4,4)

3、枚举定义中的泛型

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

4、方法定义中的泛型

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn get_x(&self) -> &T {
        &self.x
    }

    fn get_y(&self) -> &T {
        &self.y
    }
}

fn main() {
    let m = Point{x: 3, y: 5};
    let n = Point{x: 4, y: 4};
    println!("({},{})", m.get_x(), m.get_y());
    println!("({},{})", n.get_x(), n.get_y());
}

编译执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
(3,5)
(4,4)

十七、trait

1、trait 基本使用

//公共声明
pub trait Show {
    fn show(&self) -> bool;
}

struct Point {
    x: i32,
    y: i32,
}
//为Point结构体独自实现
impl Show for Point {
    fn show(&self) -> bool {
        println!("({},{})", &self.x, &self.y);
        true
    }
}

struct Line {
    m: Point,
    n: Point,
}
//为Line结构体独自实现
impl Show for Line {
    fn show(&self) -> bool {
        println!("({},{}) -> ({},{})", &self.m.x, &self.m.y, &self.n.x, &self.n.y);
        true
    }
}

fn main() {
    let mm = Point{x: 3, y: 5};
    mm.show();
    let nn = Point{x: 4, y: 4};
    let mn = Line{m: mm, n: nn};
    mn.show();
}

编译执行结果:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
(3,5)
(3,5) -> (4,4)

2、默认实现

//带默认实现的公共签名
pub trait Show {
    fn show(&self) -> bool {
        println!("hahahahahahahahaha..........");
        true
    }
}

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

// 通过 impl Summary for NewsArticle {} 指定一个空的 impl 块
// 使 Point 实例可以使用默认实现
impl Show for Point { }

struct Line {
    m: Point,
    n: Point,
}

impl Show for Line {
    fn show(&self) -> bool {
        println!("({},{}) -> ({},{})", &self.m.x, &self.m.y, &self.n.x, &self.n.y);
        true
    }
}

fn main() {
    let mm = Point{x: 3, y: 5};
    mm.show();
    let nn = Point{x: 4, y: 4};
    let mn = Line{m: mm, n: nn};
    mn.show();
}

编译执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/study`
hahahahahahahahaha..........
(3,5) -> (4,4)

3、trait 作为参数

use std::fmt::Display;

//带默认实现的公共签名
pub trait Show {
    fn show(&self) -> bool {
        println!("hahahahahahahahaha..........");
        true
    }
}

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

// 通过 impl Summary for NewsArticle {} 指定一个空的 impl 块
// 使 Point 实例可以使用默认实现
impl Show for Point { }

struct Line {
    m: Point,
    n: Point,
}

impl Show for Line {
    fn show(&self) -> bool {
        println!("({},{}) -> ({},{})", &self.m.x, &self.m.y, &self.n.x, &self.n.y);
        true
    }
}

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

//可以获取两个参数,一个实现了 Summary 的参数,一个实现了 Display 的参数
//类型可以相同,只要实现了对应的 trait 就行
fn show(item: &impl Show, item1: &impl Display) {
    item.show();
    println!("{}", item1);
}

//如果你希望强制它们都是相同类型
//T 必须是实现了 Display 和 Show 的类型
//这里显得冗长
fn show1<T: Display + Show>(item: &T, item1: &T) {
    item.show();
    println!("{}", item1);
}

//使用 where 关键词,使得可阅读性增高
fn show2<T, U>(item: &T, item1: &U) 
    where T: Show,       //T 必须是实现 Show 的数据类型
          U: Display     //U 必须是实现 Display 的数据类型
{
    item.show();
    println!("{}", item1);
}

fn main() {
    let pp = Point{x: 9, y: 9};
    let mm = Point{x: 3, y: 5};
    let nn = Point{x: 4, y: 4};
    let mn = Line{m: mm, n: nn};
    show(&mn, &pp);
    show1(&pp, &pp);
    show2(&mn, &pp);
}

编译执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/study`
(3,5) -> (4,4)
(9,9)
hahahahahahahahaha..........
(9,9)
(3,5) -> (4,4)
(9,9)

4、返回实现了 trait 的类型

//带默认实现的公共签名
pub trait Show {
    fn show(&self) -> bool;
}

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

// 通过 impl Summary for NewsArticle {} 指定一个空的 impl 块
// 使 Point 实例可以使用默认实现
impl Show for Point {
    fn show(&self) -> bool {
        println!("({},{})", &self.x, &self.y);
        true
    }
}

struct Line {
    m: Point,
    n: Point,
}

impl Show for Line {
    fn show(&self) -> bool {
        println!("({},{}) -> ({},{})", &self.m.x, &self.m.y, &self.n.x, &self.n.y);
        true
    }
}

//返回值 impl Show 表示,只要是实现了 Show trait 的类型都可以被返回
fn return_class<T: Show>(obj: T) -> impl Show {
    obj
}

fn main() {
    let mm = Point{x: 3, y: 5};
    mm.show();
    return_class(mm).show();  //可以返回 Point 类型
    
    let mm = Point{x: 3, y: 5};
    let nn = Point{x: 9, y: 9};
    let mn = Line{m:mm, n:nn};
    mn.show();
    return_class(mn).show();  //可以返回 Line 类型
}

编译执行如下:

[wlb@Arco study]$ cargo run
   Compiling study v0.1.0 (/home/wlb/Documents/codes/rust/study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/study`
(3,5)
(3,5)    //return_class返回值调用的 Show 打印的
(3,5) -> (9,9)
(3,5) -> (9,9)     //return_class返回值调用的 Show 打印的

十八、生命周期

1、函数

函数签名有一些限制:

  • 任何引用都必须拥有标注好的生命周期。
  • 任何被返回的引用都必须有和某个输入量相同的生命周期或是静态类型(static)。

另外要注意,如果没有输入的函数返回引用,有时会导致返回的引用指向无效数据,这种 情况下禁止它返回这样的引用。下面例子展示了一些合法的带有生命周期的函数:

// 一个拥有生命周期 `'a` 的输入引用,其中 `'a` 的存活时间
// 至少与函数的一样长。
fn print_one<'a>(x: &'a i32) {
    println!("`print_one`: x is {}", x);
}

// 可变引用同样也可能拥有生命周期。
fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

// 拥有不同生命周期的多个元素。对下面这种情形,两者即使拥有
// 相同的生命周期 `'a` 也没问题,但对一些更复杂的情形,可能
// 就需要不同的生命周期了。
fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
}

// 返回传递进来的引用也是可行的。
// 但必须返回正确的生命周期。
fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }

//fn invalid_output<'a>() -> &'a String { &String::from("foo") }
// 上面代码是无效的:`'a` 存活的时间必须比函数的长。
// 这里的 `&String::from("foo")` 将会创建一个 `String` 类型,然后对它取引用。
// 数据在离开作用域时删掉,返回一个指向无效数据的引用。

fn main() {
    let x = 7;
    let y = 9;
    
    print_one(&x);
    print_multi(&x, &y);
    
    let z = pass_x(&x, &y);
    print_one(z);

    let mut t = 3;
    add_one(&mut t);
    print_one(&t);
}

2、方法

方法的标注和函数类似:

struct Owner(i32);

impl Owner {
    // 标注生命周期,就像独立的函数一样。
    fn add_one<'a>(&'a mut self) { self.0 += 1; }
    fn print<'a>(&'a self) {
        println!("`print`: {}", self.0);
    }
}

fn main() {
    let mut owner  = Owner(18);

    owner.add_one();
    owner.print();
}

3、结构体

定义包含引用的结构体,需要为结构体定义中的每一个引用添加生命周期注解。在结构体中标注生命周期也和函数的类似:

// 一个 `Borrowed` 类型,含有一个指向 `i32` 类型的引用。
// 该引用必须比 `Borrowed` 寿命更长。
#[derive(Debug)]
struct Borrowed<'a>(&'a i32);

// 和前面类似,这里的两个引用都必须比这个结构体长寿。
#[derive(Debug)]
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

// 一个枚举类型,其取值不是 `i32` 类型就是一个指向 `i32` 的引用。
#[derive(Debug)]
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

fn main() {
    let x = 18;
    let y = 15;

    let single = Borrowed(&x);
    let double = NamedBorrowed { x: &x, y: &y };
    let reference = Either::Ref(&x);
    let number    = Either::Num(y);

    println!("x is borrowed in {:?}", single);
    println!("x and y are borrowed in {:?}", double);
    println!("x is borrowed in {:?}", reference);
    println!("y is *not* borrowed in {:?}", number);
}

4、trait

trait 方法中生命期的标注基本上与函数类似。注意,impl 也可能有生命周期的标注。

// 带有生命周期标注的结构体。
#[derive(Debug)]
 struct Borrowed<'a> {
     x: &'a i32,
 }

// 给 impl 标注生命周期。
impl<'a> Default for Borrowed<'a> {
    fn default() -> Self {
        Self {
            x: &10,
        }
    }
}

fn main() {
    let b: Borrowed = Default::default();
    println!("b is {:?}", b);
}

5、约束

就如泛型类型能够被约束一样,生命周期(它们本身就是泛型)也可以使用约束。: 字符 的意义在这里稍微有些不同,不过 + 是相同的。注意下面的说明:

  1. T: 'a:在 T 中的所有引用都必须比生命周期 'a 活得更长。
  2. T: Trait + 'aT 类型必须实现 Trait trait,并且在 T 中的所有引用 都必须比 'a 活得更长。

下面例子展示了上述语法的实际应用:

use std::fmt::Debug; // 用于约束的 trait。

#[derive(Debug)]
struct Ref<'a, T: 'a>(&'a T);
// `Ref` 包含一个指向泛型类型 `T` 的引用,其中 `T` 拥有一个未知的生命周期
// `'a`。`T` 拥有生命周期限制, `T` 中的任何*引用*都必须比 `'a` 活得更长。另外
// `Ref` 的生命周期也不能超出 `'a`。

// 一个泛型函数,使用 `Debug` trait 来打印内容。
fn print<T>(t: T) where
    T: Debug {
    println!("`print`: t is {:?}", t);
}

// 这里接受一个指向 `T` 的引用,其中 `T` 实现了 `Debug` trait,并且在 `T` 中的
// 所有*引用*都必须比 `'a'` 存活时间更长。另外,`'a` 也要比函数活得更长。
fn print_ref<'a, T>(t: &'a T) where
    T: Debug + 'a {
    println!("`print_ref`: t is {:?}", t);
}

fn main() {
    let x = 7;
    let ref_x = Ref(&x);

    print_ref(&ref_x);
    print(ref_x);
}

6、强制转换

一个较长的生命周期可以强制转成一个较短的生命周期,使它在一个通常情况下不能工作 的作用域内也能正常工作。强制转换可由编译器隐式地推导并执行,也可以通过声明不同 的生命周期的形式实现。

// 在这里,Rust 推导了一个尽可能短的生命周期。
// 然后这两个引用都被强制转成这个生命周期。
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// `<'a: 'b, 'b>` 读作生命周期 `'a` 至少和 `'b` 一样长。
// 在这里我们我们接受了一个 `&'a i32` 类型并返回一个 `&'b i32` 类型,这是
// 强制转换得到的结果。
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // 较长的生命周期
    
    {
        let second = 3; // 较短的生命周期
        
        println!("The product is {}", multiply(&first, &second));
        println!("{} is the first", choose_first(&first, &second));
    };
}

7、static

'static 生命周期是可能的生命周期中最长的,它会在整个程序运行的时期中 存在。'static 生命周期也可被强制转换成一个更短的生命周期。有两种方式使变量 拥有 'static 生命周期,它们都把数据保存在可执行文件的只读内存区:

  • 使用 static 声明来产生常量(constant)。
  • 产生一个拥有 &'static str 类型的 string 字面量。

看下面的例子,了解列举到的各个方法:

// 产生一个拥有 `'static` 生命周期的常量。
static NUM: i32 = 18;

// 返回一个指向 `NUM` 的引用,该引用不取 `NUM` 的 `'static` 生命周期,
// 而是被强制转换成和输入参数的一样。
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

fn main() {
    {
        // 产生一个 `string` 字面量并打印它:
        let static_string = "I'm in read-only memory";
        println!("static_string: {}", static_string);

        // 当 `static_string` 离开作用域时,该引用不能再使用,不过
        // 数据仍然存在于二进制文件里面。
    }
    
    {
        // 产生一个整型给 `coerce_static` 使用:
        let lifetime_num = 9;

        // 将对 `NUM` 的引用强制转换成 `lifetime_num` 的生命周期:
        let coerced_static = coerce_static(&lifetime_num);

        println!("coerced_static: {}", coerced_static);
    }
    
    println!("NUM: {} stays accessible!", NUM);
}

8、省略

有些生命周期的模式太常用了,所以借用检查器将会隐式地添加它们以减少程序输入量 和增强可读性。这种隐式添加生命周期的过程称为省略(elision)。在 Rust 使用省略 仅仅是因为这些模式太普遍了。

  • 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数: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), 那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。

下面代码展示了一些省略的例子。

// `elided_input` 和 `annotated_input` 事实上拥有相同的签名,
// `elided_input` 的生命周期会被编译器自动添加:
fn elided_input(x: &i32) {
    println!("`elided_input`: {}", x)
}

fn annotated_input<'a>(x: &'a i32) {
    println!("`annotated_input`: {}", x)
}

// 类似地,`elided_pass` 和 `annotated_pass` 也拥有相同的签名,
// 生命周期会被隐式地添加进 `elided_pass`:
fn elided_pass(x: &i32) -> &i32 { x }

fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x }

fn main() {
    let x = 3;
    
    elided_input(&x);
    annotated_input(&x);

    println!("`elided_pass`: {}", elided_pass(&x));
    println!("`annotated_pass`: {}", annotated_pass(&x));
}

十九、测试

1、单元测试

assert!(bool) :判断参数是否为 true ,是则通过,否则失败;

assert_eq!(T,T) : 判断两个参数是否相等,是则通过,否则失败;

assert_ne!(T,T) : 判断两个参数是否不等,是则通过,否则失败;


should_panic : 用与测试 panic!

例子:

执行 cargo new adder --lib 创建项目,并在 src/lib.rs 中写下如下代码:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 单元测试可以测试私有函数
fn a_eq_b(a: i32, b: i32) -> bool {
    a == b
}

pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不可以是 0!");
    } else if a < b {
        panic!("商为 0!")
    } else {
        a / b
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add01() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    #[ignore = "reason: 测试一下忽略功能"]
    fn test_add02() {
        assert_eq!(add(1, 3), 4);
    }

    #[test]
    fn test_a_eq_b01() {
        assert!(a_eq_b(2, 2));
    }

    #[test]
    fn test_div01() {
        assert_ne!(div(2, 2), 0);
    }

    #[test]
    #[should_panic]
    fn test_div02() {
        assert_eq!(div(2, 0), 1);
    }

    #[test]
    #[should_panic = "商为 0!"]
    fn test_div03() {
        assert_eq!(div(2, 3), 0);
    }
}

执行测试得:

$ cargo test
   Compiling calc v0.1.0 (/home/wlb/Documents/codes/rust/study/libs/calc)
    Finished test [unoptimized + debuginfo] target(s) in 0.30s
     Running unittests (target/debug/deps/calc-cf5d11b18b0c2df5)

running 6 tests
test tests::test_add02 ... ignored
test tests::test_a_eq_b01 ... ok
test tests::test_add01 ... ok
test tests::test_div01 ... ok
test tests::test_div02 - should panic ... ok
test tests::test_div03 - should panic ... ok

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

2、文档测试

为 Rust 工程编写文档的主要方式是在源代码中写注释。文档注释使用 markdown 语法 书写,支持代码块。Rust 很注重正确性,这些注释中的代码块也会被编译并且用作测试。

文档测试模板:

/// 功能描述:.........................
///
/// 详细说明:..........................
///
/// # 例子
/// 一些说明
/// ```rust
/// //代码
/// ```
///
/// # Panics
/// 一些说明
/// ```rust,should_panic
/// //代码
/// ```

3、集成测试

单元测试一次仅能单独测试一个模块,这种测试是小规模的,并且能测试私有 代码;集成测试是 crate 外部的测试,并且仅使用 crate 的公共接口,就像其他使用 该 crate 的程序那样。集成测试的目的是检验你的库的各部分是否能够正确地协同工作。

cargo 在与 src 同级别的 tests 目录寻找集成测试。所以需要在 src 同级目录新建 tests 目录,并在其下方新建一个 *.rs 文件。

例子:

extern crate calc; //用 extern 引入 ctate

#[test]
fn its_add01() {
    assert_eq!(calc::add(2, 3), 5);
}

#[test]
fn its_div01() {
    assert_eq!(calc::div(6, 3), 2);
}

// 与文档测试一样,集成测试只测公共接口,不需要也不能测私有接口
// #[test]
// fn its_add_private01() {
//     assert_eq!(calc::add_private(2, 3), 5);
// }

4、完整代码

// src/lib.rs

/// 函数功能描述:返回两个参数的和
///
/// 详细说明:调用时传入两个加数,再用一个变量接收函数的返回值
///
/// ```rust
/// let result = calc::add(2, 2);
/// assert_eq!(result, 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 文档测试只能测试公共接口
fn a_eq_b(a: i32, b: i32) -> bool {
    a == b
}

/// 函数功能描述:返回两个参数的商
///
/// 详细说明:调用时传入被除数和除数,再用一个变量接收函数的返回值
///
/// # 例子
/// ```rust
/// let result = calc::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
/// 如果第二个参数是 0,函数将会 panic。
/// ```rust,should_panic
/// calc::div(1, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不可以是 0!");
    } else if a < b {
        panic!("商为 0!")
    } else {
        a / b
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add01() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    #[ignore = "reason: 测试一下忽略功能"]
    fn test_add02() {
        assert_eq!(add(1, 3), 4);
    }

    #[test]
    fn test_a_eq_b01() {
        assert!(a_eq_b(2, 2));
    }

    #[test]
    fn test_div01() {
        assert_ne!(div(2, 2), 0);
    }

    #[test]
    #[should_panic]
    fn test_div02() {
        assert_eq!(div(2, 0), 1);
    }

    #[test]
    #[should_panic = "商为 0!"]
    fn test_div03() {
        assert_eq!(div(2, 3), 0);
    }
}
// tests/its.rs

extern crate calc;

#[test]
fn its_add01() {
    assert_eq!(calc::add(2, 3), 5);
}

#[test]
fn its_div01() {
    assert_eq!(calc::div(6, 3), 2);
}

// 与文档测试一样,集成测试只测公共接口,不需要也不能测私有接口
// #[test]
// fn its_add_private01() {
//     assert_eq!(calc::add_private(2, 3), 5);
// }

执行测试可得:

$ cargo test
   Compiling calc v0.1.0 (/home/wlb/Documents/codes/rust/study/libs/calc)
    Finished test [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests (target/debug/deps/calc-cf5d11b18b0c2df5)

running 6 tests
test tests::test_add02 ... ignored
test tests::test_a_eq_b01 ... ok
test tests::test_add01 ... ok
test tests::test_div01 ... ok
test tests::test_div02 - should panic ... ok
test tests::test_div03 - should panic ... ok

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

     Running tests/its.rs (target/debug/deps/its-3a32dab617016b02)

running 2 tests
test its_div01 ... ok
test its_add01 ... ok

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

   Doc-tests calc

running 3 tests
test src/lib.rs - div (line 32) ... ok
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 25) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

执行 cargo doc --open 可以打开浏览器查看你的文档:

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

持续更新 。。。。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值