10.1 提取函数消除重复

对于如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let num_list = vec![34, 50, 25, 100, 65];
let mut largest = num_list[0];
for num in num_list {
if num > largest {
largest = num;
}
}
println!("The largest number is {}", largest);

let num_list = vec![123, 4523, 42, 992, 6000];
let mut largest = num_list[0];
for num in num_list {
if num > largest {
largest = num;
}
}
println!("The largest number is {}", largest);
}

我们可以定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}

fn main() {
let num_list = vec![34, 50, 25, 100, 65];
let mut result = largest(&num_list);
println!("The largest number is {}", result);

let num_list = vec![123, 4523, 42, 992, 6000];
let mut result = largest(&num_list);
println!("The largest number is {}", result);
}

来消除其中的重复部分

10.2 泛型

泛型是具体类型或其它属性的抽象代替,能够提高代码的复用能力,相当于C++里的模板

函数中的泛型

例如,如果我们希望上一节定义的函数还能用于更多类型的比较,可以定义

1
2
3
4
5
6
7
8
9
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
  • 其中T即为占位符,会在编译时被替换成具体的类型
  • Rust中的类型命名一般用CamelCase

事实上,上面的代码会编译不通过,因为不是所有的类型T都实现了std::cmp::PartialOrd这个trait,即不一定可以用于比较。我们需要额外指定这个泛型参数需要拥有的trait,这一点我们将在后面介绍

结构体中的泛型

类似地,我们还可以在结构体里使用泛型,例如

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T,
}

fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}

更进一步,如果希望xy是不同的类型,可以定义

1
2
3
4
struct Point<T1, T2> {
x: T1,
y: T2,
}

枚举中的泛型

同样,我们也可以在enum中使用泛型,例如

1
2
3
4
5
6
7
8
9
enum Option<T> {
Some(T),
None,
}

enum Result<T, E> {
Ok(T),
Err(E),
}
  • 其中Option<T>是我们之前接触过的枚举类型

方法中的泛型

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Point<T, U> {
x: T,
y: U,
}

impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y
}
}
}
  • 这里的<T, U><V, W>不必相同

泛型代码的性能

泛型代码的性能和使用具体类型的代码运行速度是相同的

  • 这是因为Rust使用了单态化(monomorphization),其将泛型替换成具体类型

10.3-4 Trait

Trait定义的是一些抽象的共享行为,它会告诉编译器,某些类型有哪些和其它类型共享的功能

  • 和其他语言的interface类似,但有点区别sd
  • 类似于C++的纯虚函数

定义一个trait

Trait把方法的签名都放在一起,来定义实现某种目的所必须的一组行为

  • 使用trait关键字
  • 其只有方法签名,没有具体实现
  • trait可以有多个方法,每个方法签名占一行,并以;结尾
  • 如果一个类型要实现这个trait,那么其必须提供具体的方法实现

以下便定义了一个trait:

1
2
3
4
pub trait Summary {
fn summarize(&self) -> String;
fn summarize_default(&self) -> String;
}

暂略(除了封装,QMC模拟应该用不到trait)

10.5 生命周期(一)

Rust的每个引用都有生命周期(保持有效的作用域)

  • 大多数情况下,生命周期是隐式的、可推断的
  • 当引用的生命周期可能以不同的方式互相关联时,手动标注生命周期
  • 其主要目标是避免悬垂引用

一个简单的例子为

1
2
3
4
5
6
7
8
9
10
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
  • 花括号对会标明一个作用域
  • 在内部的花括号结束时,x的lifetime就结束了,因此borrow是无效的

手动标记生命周期

定义下面的比较字符串长短的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
}
else {
y
}
}

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

则编译器会报错,因为返回值的地方缺少一个命名的lifetime标注

  • 函数的返回类型包含了借用的值,但没有声明是来自x还是y,而且我们也不知道xy的生命周期

为了解决这个问题,我们需要用泛型来在函数签名中指定对应的生命周期:

1
2
3
4
5
6
7
8
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
}
else {
y
}
}

10.6 生命周期(二)

生命周期的标注仅仅是在描述多个引用之间的生命周期的关系,不会影响生命周期本身

生命周期的标注

  • 参数名

    • '开头

    • 通常小写且非常短,习惯上会使用'a

  • 标注位置

    • 在引用的&后面
    • 使用空格将标注和引用类型分开

例如

  • &i32是一个引用
  • &'a i32是一个带有显式生命周期的引用
  • &'a mut i32是一个带有显式生命周期的可变引用

函数签名中的生命周期标注

泛型生命周期参数应当声明在函数名和参数列表之间的<>里面

对于上一节的函数

1
2
3
4
5
6
7
8
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
}
else {
y
}
}
  • 它标明xy的引用以及返回值的生命周期不可以短于'a
  • 当这个函数被调用的时候,实际的作用域将是xy的作用域所重叠的部分

在这种情况下,下面的代码也是正确的

1
2
3
4
5
6
7
8
fn main() {
let string1 = String::from("abcd");
{
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);let string2 = "xyz";
}
}
  • 因为这里的string2是字符串字面值,是静态生命周期,存活在整个程序运行期间

类似地逻辑,下面的代码将会报错

1
2
3
4
5
6
7
8
9
fn main() {
let string1 = String::from("abcd");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
  • 这里由于返回值可能是string2,但是string2的生命周期和result以及string1没办法交叠,因此会报错

10.7 生命周期(三)

我们指定生命周期的方式往往依赖于函数所做的事情,例如

1
2
3
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
  • 如果指定a作为返回值,那么就无需为y显式声明生命周期

从函数返回引用时,返回类型的生命周期必须要与其中一个参数的生命周期匹配

  • 如果返回的引用没有指向任何参数,那么其只能引用函数内创建的值,从而出现悬垂引用(对应值在函数结束时生命周期就结束了)

例如

1
2
3
4
fn get_string<'a>(x: &'a str) -> &str {
let mut s = String::from("abc");
s.as_str()
}

会造成悬垂引用

为了实现上面的功能,我们可以转交所有权,即

1
2
3
4
fn get_string<'a>(x: &'a str) -> String {
let mut s = String::from("abc");
s
}

Struct定义中的生命周期标注

之前考虑的结构体字段都是自持有类型,而结构体也可以包含引用类型,需要标注生命周期

例如

1
2
3
4
5
6
7
8
9
10
11
12
struct ImportantExcerpt<'a> {
part: &'a str,
}

fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");

let i = ImportantExcerpt { part: first_sentence };
}

是正确的。我们需要保证实例的生命周期大于等于内部引用的字段的生命周期

生命周期的省略

对于一些编译器能推断的场景,无需显式标明生命周期

不过,如果在编译器允许的规则下,不标注生命周期可能会带来模糊不清的问题,此时需要标注才能保证编译通过


生命周期省略的三个规则(对fnimpl都适用)

  • 对于输入生命周期:

    • 每个引用类型的参数都有自己的生命周期(即单参数默认一个生命周期,双参数默认两个)
  • 对于输出生命周期:

    • 如果只有一个输入生命周期参数,则其会被赋给所有输出生命周期参数

    • 在有多个输入生命周期时,若其中一个是&self或者&mut self,则self的生命周期会被赋给所有输出生命周期

10.8 生命周期(四)

在Rust中,有一个特殊的生命周期'static,其存活在整个程序的持续期间

例如,所有的字符串字面值都有着'static的生命周期:

1
let s: &'static str = "I have a static lifetime.";

在使用'static生命周期上,需要考虑

  • 该引用是否需要在整个程序期间存活