消除重复代码
重复代码
消除重复的步骤
- 识别重复代码
- 提取重复代码到函数体中,并在函数签名中指定函数的输入和返回值
- 将重复的代码使用函数调用进行替代
1 | fn main() { |
对于上述的重复代码,可以将一些操作抽到函数中,具体代码如下所示
1 | fn largest(list: &[i32]) -> i32 { |
泛型
- 泛型:提高代码复用能力
- 处理重复代码的问题
- 泛型是具体类型或其它属性的抽象代替
- 你编写的代码不是最终的代码,而是一种模版,里面有一些“占位符”
- 编译器在编译时将“占位符”替换为具体的类型
- 例如:
fn largest<T>(list: &[T]) -> T {...} - 泛型类型参数
- 很短,通常一个字母;通常使用 T 作为泛型参数名称
- CamelCase(驼峰命名)
- T:Type 的缩写
函数定义中的泛型
- 泛型参数
- 参数类型
- 返回类型
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T { |
Struct 定义中的泛型
1 | struct Point<T> { |
可以使用多个泛型的类型参数
- 太多类型参数:你的代码需要重组为多个更小的单元
1
2
3
4
5
6
7
8
9struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let interger = Point { x: 5, y: 1.0 };
let float = Point { x : 1.0, y: 40 };
}Enum定义中的泛型可以让枚举的变体持有泛型数据类型
- 例如:
Option<T>、Result<T, E>
1
2
3
4
5
6
7
8
9
10
11enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E)
}
fn main() {}方法定义中的泛型
- 例如:
为 struct 或
enum实现方法的时候,可在定义中使用泛型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point {x: 5, y: 10};
println!("p.x = {}", p.x());
}注意:
- 把 T 放在
impl关键字后,表示在类型 T 上实现方法- 例如:
impl<T> Point<T>
- 例如:
- 只针对具体类型实现方法(其余类型没实现方法)
- 例如:
impl Point<i32>
- 例如:
- 把 T 放在
struct 里的泛型参数可以和方法的泛型参数不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct 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
}
}
}
fn main() {
let p1 = Point { x: 5, y: 4 };
let p2 = Point { x: "hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
泛型代码的性能
- 使用泛型的代码和使用具体类型的代码运行速度是一样的
- 单态化(
monomorphization)- 在编译时将泛型替换为具体类型的过程
Trait
- Trait 告诉 Rust 编译器:
- 某种类型具有那些并且可以与其他类型共享的功能
- Trait:抽象的定义共享行为
- Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型
- Trait 与其它语言的接口(interface)类似,但有些区别
定义一个 Trait
- Trait 定义:把方法签名放在一起,来定义实现某种目的所必需的一组行为
- 关键字:trait
- 只有方法签名,没有具体实现
- trait 可以有多个方法:每个方法签名占一行,以
;结尾 - 实现该 trait 的类型必须提供具体的方法实现
1 | pub trait Summary { |
在类型上实现 Trait
- 与为类型实现方法类似
- 不同之处
impl Xxxx for Tweet {...}- 在
impl的块里,需要对 Trait 里方法签名进行具体实现
1 | pub trait Summary { |
1 | use demo::Summary; |
实现 Trait 的约束
- 可以在某个类型上实现某个 trait 的前提条件是
- 这个类型或这个 trait 是在本地 crate 里定义的
- 无法为外部类型来实现外部的 trait
- 这个限制是程序属性的一部分(也就是一致性)
- 更具体地说是孤儿规则:之所以这样命名是因为父类型不存在
- 此归杂确保其他人的代码不能破坏你的代码,反之亦然
- 如果没有这个规则,两个 crate 可以为同一个类型实现同一个 trait,Rust 就不知道应该使用那个实现了
默认实现
- 默认实现的方法可以调用 trait 中其他的方法,即使这些方法没有默认实现
- 注意:无法从方法的重写实现里面调用默认的实现
1 | pub trait Summary { |
Trait 作为参数
impl Trait 语法:适用于简单情况
1
2
3
4// 使用 impl Summary 来声明 trait 作为参数
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}Trait bound 语法:可用于复杂情况
- impl Trait 语法是 Trait bound 的语法糖
1
2
3
4// 使用泛型来声明 trait 作为参数
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}使用 + 指定多个 Trait bound:代码不太美观
1
2
3
4// 要求 item 实现了 Summary 与 Display Trait
pub fn notify(item: impl Summary + Display) {
println!("Breaking news! {}", item.summarize());
}1
2
3pub fn notify<T: Summary + Display>(item: T) {
println!("Breaking news! {}", item.summarize());
}Trait bound 使用 where 子句
- 在方法签名后指定 where 子句
1
2
3pub fn notify<T: Summary + Display, U: Clone + Debug>(a: T, b: U) -> String {
format!("Breaking news! {}", a.summarize());
}在 notify 函数签名中有一串很长的 trait 签名约束,这就会导致看起来很复杂并且也不美观,这时候我们可以使用 where 子句来简化 trait 约束
1
2
3
4
5
6
7pub fn notify<T, U>(a: T, b: U) -> String
where
T: Summary + Display,
U: Clone + Debug
{
format!("Breaking news! {}", a.summarize());
}Trait 作为返回类型
impl Trait 语法
- 注意:impl Trait 只能返回确定的同一种类型,返回可能不同类型的代码会报错
1 | pub fn notify(s: &str) -> impl Summary { |
使用 Trait Bound 有条件的实现方法
在使用泛型参数的 impl 块上使用 Trait bound,我们可以有条件的为实现特定 Trait 的类型来实现方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> self {
self( x, y )
}
}
// Pair 泛型中是 Display 与 PartialOrd Trait,才可以使用 cmp_display 方法
impl <T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}也可以为实现了其它 Trait 的任意类型有条件的实现某个 Trait
为满足 Trait Bound 的所有类型上实现 Trait 叫做覆盖实现(blanket implementations)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl<T: fmt::Display + ?Sized> ToString for T {
// A common guideline is to not inline generic functions. However,
// removing `#[inline]` from this method causes non-negligible regressions.
// See <https://github.com/rust-lang/rust/pull/74852>, the last attempt
// to try to remove it.
default fn to_string(&self) -> String {
use fmt::Write;
let mut buf = String::new();
buf.write_fmt(format_args!("{}", self))
.expect("a Display implementation returned an error unexpectedly");
buf
}
}
生命周期
- Rust 的每个引用都有自己的生命周期
- 生命周期:引用保持有效的作用域
- 大多数情况:生命周期是隐式的、可被推断的
- 当引用的生命周期可能以不同的方式互相关联时:需要手动标注生命周期
避免悬垂引用(dangling reference)
- 生命周期的主要目标:避免悬垂引用(dangling reference)
在 main 函数中有两个作用域,在第三行我们声明了一个r并未赋值(rust必须给变量赋值,不然运行不了),之后我们在进入了一个作用域,这里面声明了 x,然后将 x 引用赋值给了 r,出了作用域之后这次 x 就被回收了,这里还会出现一个错误就是我们借用了被销毁的变量,编译器会提示我们不能这样做。
1 | fn main() { |
借用检查器
- Rust 编译器的借用检查器:比较作用域来判断所有借用是否合法
1 | fn main() { |
函数中的泛型生命周期
以下代码会报错,这个函数的返回类型包含一个借用的值,但是函数的签名没有说明这个借用的值是来自x还是来自y,可以考虑引入一个命名的生命周期参数
–> src/main.rs:31:33
|
31 | fn longest(x: &str, y: &str) -> &str {
| —- —- ^ expected named lifetime parameter
|
= help: this function’s return type contains a borrowed value, but the signature does not say whether it is borrowed fromxory
help: consider introducing a named lifetime parameter
|
31 | fn longest<’a>(x: &’a str, y: &’a str) -> &’a str {
| ^^^^ ^^^^^^^ ^^^^^^^ ^^^
1 | fn main() { |
之后我们根据提示,来修改一个函数的签名,此时会出现一个问题就是编译器无法确定 longest 函数返回的是x还是y,如果我们将 ‘a 修饰某个函数参数,那么代码依旧会报原先的错误(无论这里返回的是x还是y),此时需要使用 ‘a 来修饰两个变量参数
1 | fn main() { |
生命周期标注-语法
- 生命周期的标注不会改变引用的生命周期长度
- 当指定了泛型生命周期参数,函数可以接收带有任何生命周期的引用
- 生命周期的标注:描述了多个引用的生命周期间的关系,但不影响生命周期
- 生命周期参数名
- 以 ‘ 开头
- 通常全小写且非常短
- 很多人使用 ‘a
- 生命周期标注的位置
- 在引用的 & 符号后
- 使用空格将标注和引用类型分开
生命周期标注-例子
&i32:一个引用&'a i32:带有显式生命周期的引用&'a mut i32:带有显式生命周期的可变引用- 单个生命周期标注本身没有意义
函数签名中的生命周期标注
- 泛型生命周期参数声明在:函数名和参数列表之间的 <> 里
- 生命周期 ‘a 的实际生命周期是:x和y两个生命周期中较小的那个
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
深入理解生命周期
指定生命周期参数的方式依赖于函数所做的事情
下面例子,通过 longest 函数我们只返回 x,所以就不需要 y 的生命周期了
1
2
3
4
5
6
7
8
9
10
11fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值
- 这就是悬垂引用:该值在函数结束就走出了作用域
下面例子返回了一个悬垂引用,编译时会报错,不能返回一个悬垂引用
1
2
3
4
5
6
7
8
9
10
11
12
13fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
let result = String::from("abc");
result.as_str()
}如果想要在函数内部创建值,那么只需要返回对象的本身就行了,也就是说我们不需要返回引用,只需要返回对象的所有权,返回之后这对象内存也就由函数的调用者来进行清理了
1
2
3
4fn longest<'a>(x: &'a str, y: &str) -> String {
let result = String::from("abc");
result
}Struct 定义中的生命周期标注
Struct 里可包括
- 自持有的类型
- 引用:需要在每个引用上添加生命周期标注
ImportExcerpt 需要比 part 字段生命周期长,这样 part 才可以持有更长时间
1 | struct ImportExcerpt<'a> { |
生命周期的省略
- 每个引用都有生命周期
- 需要为使用生命周期的函数或 struct 指定生命周期参数
这段代码在早期的 rust 的编译器中是无法编译通过的,因为我们返回的是一个引用,我们需要显式的在函数签名上标注生命周期,但是 rust 开发团队发现很多开发者在开发中,大量重复去标注生命周期,这使得 rust 开发团队就将一些标注的规则直接写入到编译器中,面对有的场景,开发者就不需要标注生命周期,在编译期间编译器就帮你做了
1 | fn first_word(s: &str) -> &str { |
1 | fn first_word<'a>(s: &'a str) -> &'a str { |
生命周期省略规则
- 在 Rust 引用分析中所编入的模式称为生命周期省略规则
- 这些规则无需开发者来遵守
- 它们是一些特殊情况,由编译器来考虑
- 如果你的代码符合这些情况,那么就无需显式标注生命周期
- 生命周期省略规则不会提供完整的推断
- 如果应用规则后,引用的生命周期仍然模糊不清则会编译错误
- 解决办法:添加生命周期标注,表明引用间的相互关系
输入、输出生命周期
- 函数/方法的参数:输入生命周期
- 函数/方法的返回值:输出生命周期
生命周期省略的三个规则
- 编译器使用 3 个规则在没有显式标注生命周期的情况下,来确定的引用的生命周期
- 规则1应用于输入生命周期
- 规则2、3应用于输出生命周期
- 如果编译器应用完3个规则之后,仍然有无法确定生命周期的引用则会报错
- 这些规则适用于 fn 定义和 impl 块
- 规则1:每个引用类型的参数都有自己的生命周期
- 规则2:如果只有1个输入生命周期参数,那么该生命周期被赋给所有输出生命周期参数
- 规则3:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self(是方法),那么 self 的生命周期会被赋给所有的输出生命参数
生命周期省略的三个规则-例子
假设我们是编译器,编译器在编译期间会对生命周期做什么
fn first_word(s: &str) -> &str {fn first_word<'a>(s: &'a str) -> &str {:规则1fn first_word<'a>(s: &‘a str) -> &’a str {:规则2
fn longest(x: &str, y: &str) -> &str {fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {:规则1(每个函数参数都有自己的生命周期)
在应用规则1之后,编译器为x与y添加了生命周期,因为函数参数有两个生命周期所有第2条与第3条生命周期也不适用了,那么此时编译器就会报错,面对这种情况我们就需要显示的去生命周期了。
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {,需要在函数返回值签名上声明返回的生命周期
方法定义中的生命周期标注
- 在 struct 上使用生命周期实现方法,语法和泛型参数的语法一样
- 在那声明和使用生命周期参数,依赖于:
- 生命周期参数是否和字段、方法的参数或返回值有关
- struct 字段的生命周期名
- 在 impl 后声明
- 在 struct 名后使用
- 这些生命周期是 struct 类型的一部分
- impl 块内的方法签名中:
- 引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以
- 生命周期省略规则经常使得方法中的生命周期标注不是必须的
1 | struct ImportExcerpt<'a> { |
静态生命周期
- ‘static 是一个特殊的生命周期:整个程序的持续时间
- 例如:所有的字符串字面值都拥有 ‘static 生命周期
let s: &'static str = "I have a static lifetime";
- 例如:所有的字符串字面值都拥有 ‘static 生命周期
- 为引用指定 ‘static 生命周期前要三思
- 是否需要引用在程序整个生命周期内都存活
泛型参数类型、Trait Bound、生命周期
1 | use std::fmt::Display; |