09.泛型、trait与生命周期
一、删减重复代码
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}
在上述代码中存在了一部分重复代码,我们通过提取重复代码并创建其为largest
函数,从而达到消减重读代码的效果。
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {}", result);
}
二、泛型数据类型
在Rust中,泛型(generics)致力于高效地处理重复概念。
1、在函数定义中
当使用泛型来定义一个函数时,我们需要将泛型放置在函数签名中,通常用于指定参数和返回值类型的地方。
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}
这里的largest_i32
函数正是我们归纳出来用于寻找i32切片中最大值的函数。而largest_char
函数则是largest函数作用于char切片的版本。因为这两个函数拥有完全相同的代码,所以我们可以通过在一个函数中使用泛型来消除重复代码。
为了参数化这个新函数所使用的类型,首先需要给类型参数命名,我们可以使用合法的标识符作为类型参数名称。在Rust中,我们出于惯例选择了T
作为简短的泛型参数名称,通常仅仅是一个字母。另外,Rust采用了驼峰命名法作为类型的命名规范。
当我们在函数体中使用参数时,我们必须要在签名中声明对应的参数名称;当我们需要在函数签名中使用类型参数时,也需要在使用前声明这个类型参数的名称。
为了定义泛型版本的largest
函数,类型名称的声明必须被放置在函数名与参数列别之间的一对尖括号<>
中:
fn largest<T>(list: &[T]) -> {}
这段代码含义是:函数largest
拥有泛型参数T
,它接收一个名为list
的T
值切片作为参数,并返回一个同样拥有类型T的值作为结果。
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
但是这段代码无法通过编译,我们将会修复他。
![[Pasted image 20221129162102.png]]
这个错误表明largest
函数中的代码不能适用于T所有可能的类型。因为函数体中的相关语句需要比较类型T的值,这个操作只能被用于可排序的值类型。
2、在结构体定义中
同样地,我们也可以使用<>
语法来定义在一个或多个字段中使用泛型的结构体。
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};
}
在结构体名后的一对尖括号中声明泛型参数后,我们就可以在结构体定义中那些通常用于指定具体类型的位置使用泛型了。
注意,我们在定义Point<T>
时仅使用了一个泛型,这个定义表明Point<T>
结构体对某个类型T是通用的。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point{ x: 5, y: 4.0};
}
字段x和y必须是相同的类型,因为它们拥有相同的泛型T
。在这个例子中,当我们将整数5赋值给x时,编译器就会将这个Point<T>
实例中的泛型T识别为整数类型,但是我们接着为y制定了浮点数4.0,而且这个变量被定义为与x拥有相同的类型,因此这段代码会触发类型不匹配错误。
![[Pasted image 20221130101507.png]]
为了在保持泛型状态的前提下,让Point结构体中的x和y能够被实例化为不同的类型,我们可以使用多个泛型参数。
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point{ x: 5, y: 10};
let both_float = Point{ x: 1.0, y: 4.0};
let wont_work = Point{ x: 5, y: 4.0};
}
3、在枚举定义中
枚举定义在它们的便体重存放泛型数据。
enum Option<T> {
Some(T),
None,
}
Option<T>
是一个拥有泛型T的枚举。它拥有两个变体:持有T类型值得Some变体,以及一个不持有任何值得None变体。
枚举也可以使用多个泛型参数。
enum Result<T, E> {
Ok(T),
Err(E),
}
Result枚举拥有两个泛型:T和E,持有T类型值得OK,以及一个持有E类型的Err。
4、在方法定义中
struct 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());
}
为结构体Point<T>
实现名为x的方法,它会返回一个指向x字段中T类型值得引用。
注意,我们必须紧跟着impl
关键字声明T,以便能够在实现方法时指定类型Point<T>
。通过Impl之后将T声明为泛型,Rust能够识别出Point见括号内的类型是泛型而不是具体类型。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
这里的impl
代码块作用于具体类型替换了泛型参数T的结构,这就意味着我们无需在impl
之后声明任何类型了。
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,
}
}
}
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);
}
在main中,我们定义了一个Point,它的x拥有类型为i32的值5,而y则拥有类型为f64的值10.4。接下来p2变量同样是一个Point结构体,其中x的类型为字符串切片(值为”Hello“),而y的类型则是char(值为c
)。在p1上调用mixup并传入p2作为参数,返回值为p3。p3会拥有类型为i32的字段x,因为x来自p1;它还会拥有类型为char的字段y,因为y来自p2。最后调用prlintln!
宏输出结果。
![[Pasted image 20221130171601.png]]
5、泛型代码的性能问题
Rust实现泛型的方式决定了使用泛型的代码与使用具体类型的代码相比会有任何速度上的差异。
二、Trait:定义共享行为
trait(特征)被用来向Rust编译器描述某些特定类型拥有的且能被其他类型共享的功能,它使我们可以一种抽象的方式来定义共享行为。
注意:
trait与其他语言中常被称为接口(interface)的功能类似,但也不尽相同。
1、定义trait
trait提供了一种将特定签名组合起来的途径,它定义了为达成某种目的所必须的行为集合。如下示例10-12
pub trait Summary {
fn summarize(&self) -> String;
}
使用trait
关键字来声明trait,紧跟随关键的是该trait的名字,这里是Summary
。在其后的花括号中,我们声明了用于定义类型行为的方法签名,也就是fn summarize(&self) -> String;
。在方法签名后,我们省略了花括号及具体的实现,直接使用分号终结了当前语句。任何想要实现这个trait的类型都需要为上述方法提供自定义行为。
一个trait可以包含多个方法:每个方法签名占据单独一行并以分号结尾。
2、为类型实现trait
我们基于Summary trait定义了所期望的行为,现在就可以在多媒体聚合中依次为每个类型实现这个trait了。在src/lib.rs 10-13
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
为类型实现trait与实现普通方法的步骤十分类似。它们的区别在于我们必须在impl
关键字后提供我们想要实现的trait名,并紧接for
关键字及当前的类型名。在impl代码块中,我们同样需要填入trait中的方法签名。但在每个签名的结尾不再使用分号,而是使用花括号并在其中编写函数体来为这个特定类型实现该trait的方法所应具有的行为。src/main.rs
use aggregator::{Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
注意,示例10-13
将Summary trait以及NewsArticle和Tweet结构体定义在了同一个lib.rs
文件中,所以它们处于统统的作用域中。假设这个lib.rs
属于某个名为aggregator
的库,当第三方开发者想要为他们自定义的结构体实现Summary trait并使用相关功能是,就必须将这个traIt引入自己的作用域中,使用use aggtrgator::Summary;
语句完成引入操作,进而调用相关方法或为自定义类型实现Summary。同时,trait使用了pub关键字作为前缀,这是因为我们必须要到将Summary trait声明为公共的才能被其他库用于具体实现。
注意:实现trait有一个限制:只有当trait或类型定义于我们的库中时,我们才能为该类型实现对应的trait。
3、默认实现
当我们为某个特定类型实现trait时,可以选在保留或重载每个方法的默认行为。
如下10-14
展示了如何为Summary trait中锋summarize方法指定一个默认的字符串返回值,而不是是如同示例src/lib.rs 10-12
一样仅仅定义方法签名本身。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
假如我们决定在NewArticle的实例中使用这种默认实现而不是自定义实现,那么我们可以指定一个空的impl代码块:impl Summary for NewArticle {}
。
此时没有直接为NewArticle
定义summarize
方法,我们也可以提供一个默认实现,并指定NewsArticle实现Summary trait。于是,我们依然可以在NewsArticle的实例上调用summarize方法,示例如下:
use chapter10::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
为summarize
提供一个默认实现并不会影响示例10-13
中为Tweet
实现Summary时所编写的代码。这是因为重载默认实现与实现trait方法的语法一致。
注意,我们是无法在重载方法实现的过程中调用该方法的默认实现。
4、使用trait作为参数
在示例10-13
中,我们为NewArticle与Tweet类型实现了Summary trait。我们可以定义notify函数来调用其item参数summarize方法,这里的参数item可以是任何实现了Summary trait的类型。可以像下面一样使用impl Trait
语法:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
/*如下示例*/
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
我们没有为item参数指定具体的类型,而是使用了Impl关键字及对应的trait名称,这一参数可以接收任何实现了指定trait的类型。
1.trait约束
这里的impl trait常被用在一些较短的示例中,但它其实只是trait约束的一种语法糖。它的完整形式如下:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
这种较长的形式完全等同于之前的示例,只是后面的写法会稍显臃肿。我们将泛型参数与trait约束同时放置在尖括号中,并使用冒号分隔。impl Trait
更适用于短小的示例,而trait约束则更适用于复杂情形。
例如,假设我们需要接收两个都实现了Summary的参数,那么使用impl Trait
的写法如下所示:
pub fn notitfy(item1: &impl Summary, item2: &impl Summary) {}
只要item1和item2可以使用不同的类型,这段代码就没有问题。如果你想强制两个参数使用相同的类型,那么如下代码:
pub fn notify<T: Summary>(item1: T, item2: T) {}
泛型T
制定了参数item1和item2的类型,它同时也决定了函数为item1与item2接收的参数值必须拥有相同的类型。
2.通过+
语法来指定多个trait约束
假如notify函数需要在调用summarize方法的同时显示格式化后的item,那么item就必须实现两个不同的trait:Summary和Display。我们使用+
语法实现:
pub fn notify(item: impl Summary + Display) {}
这一语法在泛型的trait约束中同样有效:
pub fn notify<T: Summary + Display>(item: T) {}
通过指定两个trait约束,可以在notify函数体中调用summarize,并使用{}来格式化item。
3.使用where从句来简化trait约束
因为每个泛型都拥有自己的trait约束,定义有多个泛型参数的函数可能会有大量的trait约束信息需要被填写在函数名与参数列表之间,这会导致函数签名变得难以理解。为解决这一问题,Rust提供了一个替代语法,我们可以在函数签名之后使用where从句来指定trait约束。如下代码可以更代为:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}
更改为where从句为:
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
5、返回实现了trait的类型
我们同样可以在返回值中使用impl Trait
语法,用于返回某种实现了trait的类型:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
/*示例如下*/
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
通过在返回类型中使用impl Summary
,我们指定returns_summarizable()
函数返回一个Summary trait的类型作为结果,而无须显示地声明具体的类型名称。
三、使用生命周期保证引用的有效性
Rust的每个引用都有自己的生命周期(lifetime),它对应着引用保持有效性的作用域。在大多数时候,生命周期都是隐式且可以被推导出来的。当出现了多个可能的类型时,我们就必须手动声明类型。类似地,当引用的声明周期可能以不同的方式相互关联时,我们就必须手动标注声明周期。
1、使用声明周期来避免悬垂引用
声明周期最主要的目标在于避免悬垂引用,进而避免程序引用到非预期的数据。
2、借用检查器
Rust编译器拥有一个借用检查器,它被用于比较不同的作用域并确定所有借用的合法性。
3、生命周期标注语法
生命周期的标注并不会改变任何引用的生命周期长度。在不影响生命周期的前提下,标准本身会被用于描述多个引用生命周期之间的关系。
生命周期的标注使用了一种明显不同的语法:它们的参数名称必须以('
)开头,且通常使用全小写字符。它们比较简短,大部分开发者选择以'a
作为默认使用名称。
&i32 //引用
&'a i32 //拥有显式生命周期的引用
&'a mut i32 //拥有显示生命周期的可变引用
单个生命周期的标注本身并没有太多意义,标注之所以存在是为了向Rust描述多个泛型生命周期参数之间的关系。
4、函数签名中的生命周期标注
在函数签名中我们需要表达的意思是:参数与返回值中的所有引用都必须拥有相同的生命周期。
fn 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: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这段代码的函数签名向Rust表明,函数所获取的两个字符串切片参数的存活时间,必须不短于给定的生命周期'a
。这个函数签名同时也意味着,从这个函数返回的字符串切片也可以获得不短于'a
的生命周期。记住,当我们在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,我们只是向借用检查器指出了一些可以用于检查非法调用的约束。
当我们将具体的引用传入longest
时,被用于代替'a
的具体生命周期就是作用域x与作用域y重叠的那一部分。换句话说,泛型生命周期'a
会被具体化为x与y两者生命周期较短的那一个。
5、深入理解生命周期
指定生命周期的方式往往取决于函数的具体功能。
fn main() {
let string1 = String::from("abcd");
let string2 = "efghijklmnopqrstuvwxyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
在这个例子中,我们为参数x与返回类型制定了相同的生命周期参数'a
,却有意忽略了参数y,这是因为y的生命周期与x和返回值的生命周期没有任何关系。
![[Pasted image 20221216162351.png]]
当函数返回一个引用时,返回类型的生命周期参数必须要与其中一个参数的生命周期参数相匹配。当返回的引用没有指向任何参数时,那么它只可能是指向了一个创建于函数内部的值,由于这个值会因为函数的结束而离开作用域,所以返回的内容也就变成了悬垂引用。
下面将演示一个无法通过编译的longest
函数:
fn 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: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
即便我们在上面的代码中为返回类型指定了生命周期参数'a
,这个实现也依然无法通过编译,因为返回值的生命周期参数没有于任何参数的生命周期产生关联。
![[Pasted image 20221216164818.png]]
这个问题在于result
在longest
函数结束时就离开了作用域,并被清理。因此造成了无法编译的问题。
从根本上说,生命周期语法就是用来关联一个函数中不同参数及返回值的生命周期的。一旦它们形成了某种练习,Rust就获得了足够的信息来支持保障内存安全的操作,并阻止那些可能会导致悬垂指针或其他违反内存安全的行为。
6、结构体定义中的生命周期标注
如下示例,定义了一个存放字符串切片的ImportantExcerpt
结构体。
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,
};
}
这个结构体仅有一个字段part,用于存储一个字符串切片。为了在结构体定义中使用生命周期参数,我们需要在结构体名称后的尖括号内声明泛型生命周期的名称(ImportantExcerpt
)。这个标注意味着ImportantExcerpt
实例存货时间不能超过存储在part字段中的引用的存活时间。
在main函数中。首先创建了一个String实例novel,接着又创建了一个ImportantExcerpt
结构体的实例,它存放了变量novel中第一个句子的引用。在ImportantExcerpt
实例创建之前,novel中的数据就已经生成了,而novel会在ImportantExcerpt
离开作用域后才离开作用域,所以ImportantExcerpt
实例中的引用总是有效的。
7、生命周期省略
在Rust的早期版本(pre-1.0)中,每个引用都必须有一个显式的生命周期。Rust团队发现,在某些特定情况Rust程序员总是重复地编写同样的生命周期标注,并且这样的场景是可预测的。于是Rust团队将这些模式直接写入编译器代码中,使用借用检查器在这些情况下可以自动对生命周期进行推导而无需显式标注。
这就随之产生了生命周期省略规则,但是省略规则并不能提供完整的推断。那么就需要你通过添加生命周期标注,显式地注明引用之间的关系。来解决错误。
函数参数或方法参数中的生命周期被称为输入生命周期(input lifetime),而返回值的生命周期被称为输出生命周期(output lifetime)。
在没有显式标注的情况下,编译器目前使用了三种规则来计算引用的生命周期。第一条规则作用于输入生命周期,第二条和第三条规则作用于输出生命周期。当编译器检查完这3条规则仍然有无法计算出生命周期的引用时,编译器就会停止运行并抛出错误。
- 第一条规则:每一个引用参数都会拥有自己的生命周期参数;
- 第二条规则:当只存在一个输入生命周期参数时,这个生命周期会被赋予给所有输出生命参数;
- 第三条规则:当拥有多个输入生命周期参数,而其中一个是
&self
或&mut self
时,self的生命周期会被赋予给所有的输出生命周期参数;
8、方法定义中的生命周期标注
结构体字段中的生命周期名字总是需要被生命在impl关键字之后,并被用于结构体名称之后,因为这些生命周期是结构体类型的一部分。在impl代码块的方法签名中,引用可能是独立的,也可能与结构体字段中的引用的生命周期相关联。另外,生命周期省略规则在大部分情况下可以帮我们免去方法签名中的生命周期标注。
9、静态生命周期
Rust中存在一种特殊的生命周期'static
,它表示整个程序的执行期。所有的字符串字面量都拥有'static
生命周期,如下标注:
fn main() {
let s: &'static str = "I have a static lifetime.";
}
字符串的文本被直接存储在二进制程序中,并总是可用的。因此,所有的字符串字面量的生命周期都是'static