Rust 基础-泛型、特征和生命周期(第五章)

1. 泛型

泛型就是一种多态,泛型参数必需在使用前进行声明。

1
2
3
4
5
6
7
8
9
fn add<T>(a:T, b:T) -> T {
a + b
}

fn main() {
println!("add i8: {}", add(2i8, 3i8));
println!("add i32: {}", add(20, 30));
println!("add f64: {}", add(1.23, 1.23));
}

1. 结构体中使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 };
}

// 适用不同可加类型参数
struct Point<T,U> {
x: T,
y: U,
}

2. 枚举中使用泛型

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

3. 方法中使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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,
}
}
}

// 为具体的泛型类型实现方法
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c'};

let p3 = p1.mixup(p2);

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

4. 泛型的性能

Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。编译器会填充编译时使用的具体类型,寻找所有泛型代码被调用的位置并针对具体类型生成代码

2. 特征(Trait)

特征(Trait)类似于其他语言中的接口。使用 trait 关键字来声明一个特征,后面跟着特征名,在大括号中定义了该特征的所有方法,方法签名结尾是分号。

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

1. 孤儿规则

如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的。

2. 默认实现

可以在特征中定义具有默认实现的方法,其它类型也可以重载该方法。并且默认实现允许调用该特征中的其他方法,即使它们没有默认实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}

// 在默认方法中调用其他默认方法
pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}

3. 特征约束(trait bound)

可以使用特征作为函数参数,它是一个语法糖,叫做特征约束,其还可以指定多个约束条件。

1
2
3
4
5
6
7
8
// 使用任何实现了 Summary 特征的类型作为该函数的参数
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 特征约束
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}

除了单个约束条件,还可以指定多个约束条件,并且使用 where 关键字简化。

1
2
3
4
5
6
7
// 多重特征约束
pub fn notify(item: &(impl Summary + Display)) {}
// where
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}

4. 函数返回中的 impl Trait

可以通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征。

1
2
3
4
5
6
7
8
9
fn returns_summarizable() -> impl Summary {
// 因为 Weibo 实现了 Summary,因此可以用它来作为返回值
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}

注意:

  • 对于调用者而言,只知道返回了一个实现了 Summary 特征的对象,但不知道具体类型
  • 该功能,在返回的真实类型非常复杂时,很有用
  • 该功能,只能有返回一个具体的类型,如果想要实现返回不同的类型,需要使用特征对象

5. 派生特征

形如 #[derive(Debug)] 的代码,是一种特征派生语法,被标记的对象会自动实现对应的默认特征代码,继承相应的功能。

3. 生命周期

生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据。为了保证所有权和借用的正确性,Rust 使用了一个借用检查器(Borrow checker),来检查我们程序的借用正确性。

1
2
3
4
5
6
7
8
9
10
{
let r;
{
let x = 5;
r = &x;
}
// r 引用了内部大括号中的 x 变量,而 x 被提前释放,此时就是一个悬垂指针
// 报错:`x` does not live long enough
println!("r: {}", r);
}

生命周期简而言之就是引用的有效作用域,一般情况下,编译器会自动推导。在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

// 返回值 &str 处编译报错,expected named lifetime parameter
// 主要是编译器无法知道该函数的返回值到底引用 x 还是 y
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

1. 语法

生命周期标注并不会改变任何引用的实际作用域。

  • 语法以单引号 ' 开头,名称往往是一个单独的小写字母
  • 'a: 'b 是生命周期约束语法,表示 'a 必须比 'b 活得久,可以直接声明或者使用 where 'a: 'b
  • 特殊的生命周期 'static,拥有该生命周期的引用可以和整个程序活得一样久,例如字符串字面量和特征对象
1
2
3
4
&i32        // 一个引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用
&'static str

2. 函数中的生命周期

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
  • 和泛型一样,使用生命周期参数,需要先声明 <'a>
  • 表示 x、y、返回值 至少活得和 'a 一样久

3. 结构体中的生命周期

在结构体中使用引用,需要为结构体中的每一个引用标注上生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ImportantExcerpt<'a> {
// 意为结构体所引用的字符串 str 必须比该结构体活得更久
part: &'a str,
}

fn main() {
let i;
{
let novel = String::from("Once upon a time");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}
// 报错,因为此时结构体比它引用的字符串活得更久
println!("{:?}",i);
}

4. 方法中的生命周期

类似泛型参数语法,根据生命周期消除规则,方法签名中,往往不需要标注生命周期。

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

impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

5. 生命周期消除

编译器为了简化用户的使用,有时会进行生命周期消除。函数参数和返回值的生命周期被称为输入生命周期和输出生命周期。

有三条消除规则来确定哪些场景不需要显式地去标注生命周期,当不适用时,就会报错来提示手动标注。

1. 每一个引用参数都会获得独自的生命周期

例如一个引用参数的函数就有一个生命周期标注:fn foo<'a>(x: &'a i32),两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此类推。

2. 若只有一个输入生命周期,即函数参数中只有一个引用类型,那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期

例如函数 fn foo(x: &i32) -> &i32x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32

3. 若存在多个输入生命周期,且其中一个是 &self&mut self,则 &self 的生命周期被赋给所有的输出生命周期

拥有 &self 形式的参数,说明该函数是一个方法,该规则让方法的使用便利度大幅提升。