10.1 提取函数消除重复
对于如下的代码
1 | fn main() { |
我们可以定义函数
1 | fn largest(list: &[i32]) -> i32 { |
来消除其中的重复部分
10.2 泛型
泛型是具体类型或其它属性的抽象代替,能够提高代码的复用能力,相当于C++里的模板
函数中的泛型
例如,如果我们希望上一节定义的函数还能用于更多类型的比较,可以定义
1 | fn largest<T>(list: &[T]) -> T { |
- 其中
T
即为占位符,会在编译时被替换成具体的类型 - Rust中的类型命名一般用CamelCase
事实上,上面的代码会编译不通过,因为不是所有的类型T
都实现了std::cmp::PartialOrd
这个trait,即不一定可以用于比较。我们需要额外指定这个泛型参数需要拥有的trait,这一点我们将在后面介绍
结构体中的泛型
类似地,我们还可以在结构体里使用泛型,例如
1 | struct Point<T> { |
更进一步,如果希望x
和y
是不同的类型,可以定义
1 | struct Point<T1, T2> { |
枚举中的泛型
同样,我们也可以在enum中使用泛型,例如
1 | enum Option<T> { |
- 其中
Option<T>
是我们之前接触过的枚举类型
方法中的泛型
示例:
1 | struct Point<T, U> { |
- 这里的
<T, U>
和<V, W>
不必相同
泛型代码的性能
泛型代码的性能和使用具体类型的代码运行速度是相同的
- 这是因为Rust使用了单态化(monomorphization),其将泛型替换成具体类型
10.3-4 Trait
Trait定义的是一些抽象的共享行为,它会告诉编译器,某些类型有哪些和其它类型共享的功能
- 和其他语言的interface类似,但有点区别sd
- 类似于C++的纯虚函数
定义一个trait
Trait把方法的签名都放在一起,来定义实现某种目的所必须的一组行为
- 使用
trait
关键字 - 其只有方法签名,没有具体实现
- trait可以有多个方法,每个方法签名占一行,并以
;
结尾 - 如果一个类型要实现这个trait,那么其必须提供具体的方法实现
以下便定义了一个trait:
1 | pub trait Summary { |
暂略(除了封装,QMC模拟应该用不到trait)
10.5 生命周期(一)
Rust的每个引用都有生命周期(保持有效的作用域)
- 大多数情况下,生命周期是隐式的、可推断的
- 当引用的生命周期可能以不同的方式互相关联时,手动标注生命周期
- 其主要目标是避免悬垂引用
一个简单的例子为
1 | fn main() { |
- 花括号对会标明一个作用域
- 在内部的花括号结束时,
x
的lifetime就结束了,因此borrow是无效的
手动标记生命周期
定义下面的比较字符串长短的函数
1 | fn longest(x: &str, y: &str) -> &str { |
则编译器会报错,因为返回值的地方缺少一个命名的lifetime标注
- 函数的返回类型包含了借用的值,但没有声明是来自
x
还是y
,而且我们也不知道x
和y
的生命周期
为了解决这个问题,我们需要用泛型来在函数签名中指定对应的生命周期:
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
10.6 生命周期(二)
生命周期的标注仅仅是在描述多个引用之间的生命周期的关系,不会影响生命周期本身
生命周期的标注
参数名
以
'
开头通常小写且非常短,习惯上会使用
'a
标注位置
- 在引用的
&
后面 - 使用空格将标注和引用类型分开
- 在引用的
例如
&i32
是一个引用&'a i32
是一个带有显式生命周期的引用&'a mut i32
是一个带有显式生命周期的可变引用
函数签名中的生命周期标注
泛型生命周期参数应当声明在函数名和参数列表之间的<>
里面
对于上一节的函数
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
- 它标明
x
和y
的引用以及返回值的生命周期不可以短于'a
- 当这个函数被调用的时候,实际的作用域将是
x
和y
的作用域所重叠的部分
在这种情况下,下面的代码也是正确的
1 | fn main() { |
- 因为这里的
string2
是字符串字面值,是静态生命周期,存活在整个程序运行期间
类似地逻辑,下面的代码将会报错
1 | fn main() { |
- 这里由于返回值可能是
string2
,但是string2
的生命周期和result
以及string1
没办法交叠,因此会报错
10.7 生命周期(三)
我们指定生命周期的方式往往依赖于函数所做的事情,例如
1 | fn longest<'a>(x: &'a str, y: &str) -> &'a str { |
- 如果指定
a
作为返回值,那么就无需为y
显式声明生命周期
从函数返回引用时,返回类型的生命周期必须要与其中一个参数的生命周期匹配
- 如果返回的引用没有指向任何参数,那么其只能引用函数内创建的值,从而出现悬垂引用(对应值在函数结束时生命周期就结束了)
例如
1 | fn get_string<'a>(x: &'a str) -> &str { |
会造成悬垂引用
为了实现上面的功能,我们可以转交所有权,即
1 | fn get_string<'a>(x: &'a str) -> String { |
Struct定义中的生命周期标注
之前考虑的结构体字段都是自持有类型,而结构体也可以包含引用类型,需要标注生命周期
例如
1 | struct ImportantExcerpt<'a> { |
是正确的。我们需要保证实例的生命周期大于等于内部引用的字段的生命周期
生命周期的省略
对于一些编译器能推断的场景,无需显式标明生命周期
不过,如果在编译器允许的规则下,不标注生命周期可能会带来模糊不清的问题,此时需要标注才能保证编译通过
生命周期省略的三个规则(对fn
和impl
都适用)
对于输入生命周期:
- 每个引用类型的参数都有自己的生命周期(即单参数默认一个生命周期,双参数默认两个)
对于输出生命周期:
如果只有一个输入生命周期参数,则其会被赋给所有输出生命周期参数
在有多个输入生命周期时,若其中一个是
&self
或者&mut self
,则self
的生命周期会被赋给所有输出生命周期
10.8 生命周期(四)
在Rust中,有一个特殊的生命周期'static
,其存活在整个程序的持续期间
例如,所有的字符串字面值都有着'static
的生命周期:
1 | let s: &'static str = "I have a static lifetime."; |
在使用'static
生命周期上,需要考虑
- 该引用是否需要在整个程序期间存活