泛型类型, Traits, 生命期
https://doc.rust-lang.org/book/ch10-00-generics.html
任何编程语言都有有效处理代码重复的手段. 在 Rust 中提供的类似方式是泛型.
Rust 支持泛型类和泛型函数, 比如 Vec<T>
. 此外, Trait
也可以定义为泛型的, Rust 中的 Trait 就是其它语言中的 interface 概念.
在 Rust 中还有一个核心概念是 lifetime. 下面分别来看.
泛型数据类型
泛型函数语法如下:
/// 找列表中最大值
fn largest<T>(list: &[T]) -> &T where T: std::cmp::PartialOrd{
let mut largest = &list[0];
for item in list {
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);
}
泛型类型语法如下:
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 MyValues<T, U> {
v1: T,
v2: U,
}
// enum 中的泛型参数
enum MyResult<T, E> {
Some(T),
Err(E),
}
// 在方法实现中使用泛型
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 某个特定泛型参数类型的实现
impl Point<f32> {
fn sqrt(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
泛型性能
Rust 的一个特点就是零开销抽象, Rust 在实现泛型时, 是通过编译的时候对泛型展开来实现的. 在展开过程中, 编译器会查找所有使用到对应泛型的地方, 并按照实际类型将泛型展开为具体类型代码.
比如下面的代码:
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
当 Rust 编译这段代码时, 编译器读取代码中所有使用 Option<T>
的地方, 这里会发现有 Option<i32>
和 Option<f64>
两处, 因此展开后的实际代码中, 会包含 Option
类型的两个定义:
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
可以注意到展开后的类型命名, 且正因为 Rust 编译器会将泛型自动展开为具体类型, 因此我们在使用泛型抽象时, 泛型抽象本身是零开销的.
Trait
: 定义同有行为
Rust 中 Trait 即其它语言中的 interface 概念, 但和 interface 有细微差别.
通过 Trait
可以提供一种对共有行为的抽象.
简单的 Trait 定义:
pub trait Summary {
fn summarize(&self) -> String;
}
某个类型对 Trait 实现:
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 类型 {..}
.
在实现 Trait 的时候有一个限制: 无法对本 crate 外部类型实现本 crate 外部的 Trait. 比如我们可以在定义 trait 的 crate 中在 Vec
上实现该协议, 也可以在本地的 MyPoint
类型上实现外部的 Trait, 但无法在引入的类型上实现引入的 Trait.
Rust 引入这个限制的原因是: 确保其它的用户或开发者自己不 会通过实现 Trait 的方式把对方的代码搞坏, 因为上面的限制保证了只能对自己的类型或自己的 Trait 才能提供 Trait 实现. (比如无法对 Vec
再提供 Display
的实现, 因为这两个都不是我们自己的)
可以提供 Trait 的默认实现:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
如果某个类型想实现有默认实现的 Trait, 则:
// 如果想使用默认实现
impl Summary for MyType { }
// 如果想覆盖默认实 现
impl Summary for MyType {
fn summarize(&self) -> String {
String::from("(More ...)")
}
}
Trait 作为函数参数使用
Trait 作为参数的语法如下, 使用 impl
关键字:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 使用 + 号指定多 Trait 的组合
pub fn notify(item: &(impl Summary + Display)) {
// ...
}
这样我们可以传入任意实现了 Trait 的类型到这个函数, 并使用 Trait 中提供的方法或函数.
Trait 作为泛型约束使用
Trait 作为泛型约束的语法如下:
// 使用普通方式指定约束
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// 多个泛型约束可以使用 + 号
pub fn notify<T: Summary + Display>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// 使用 where 指定约束(更清晰的语法)
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
Trait 作为返回值使用
Trait 作为返回值时, 也可以使用 impl
关键字指定:
// 返回值被 move 出去, 因为返回的不是 Copy 的类型
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 Trait
返回类型的函数或方法中返回多个不同的具体类型, 这个限制是由于编译器本身实现导致的. 如果要突破这个限制, 需要用到 Trait Object
, 后续会讲到.
使用 Trait 约束来有条件地实现泛型
可以在实现泛型时指定针对实现了特定 Trait 的类型才提供实现:
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
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);
}
}
}
在上述泛型实现中, 我们仅针对实现了 Display
和 PartialOrd
的具体类型才提供 cmp_display
实现. 而 new
函数则是任意具体类型都实现了的.
同时, 我们也可以为任意实现了某个 Trait 的类型提供额外的 Trait 实现, 比如标准库中:
impl<T: Display> ToString for T {
// empty
}
上述表示为任意实现了 Display
的类型提供 ToString
Trait 的实现, 由于 ToString
Trait 提供了 to_string
的默认实现, 因此上述代码中实现块是空的. 正因为这样, 我们才可以使用比如下面的方式将 i32
转为 String
:
let s = 3.to_string();
由于 Rust 的零开销抽象泛型展开处理方式, 因此我们可以在 Rust 中完整避免类似其它动态语言泛型在运行时才发现错误的情况, 因为如果泛型有问题, Rust 编译就不会通过.
核心概念: 使用生命期检验引用有效性
Rust 编译器拥有 borrow checker
, 用于比较 borrow 的引用是否有效.
下面是 Rust 中关于引用生命期的典型例子:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
这个例子如果直接编译会报错, 因为不知道 x 和 y 两个引用的生命期是如何的.
标记某个引用生命期的语法如下:
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
// 在函数中标记参数和返回值的生命期:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
正因为标记了生命期, 因此下面的代码可以通过编译:
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}
下面的例子无法通过编译:
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
// result 在超过 string1 和 string2 中短生命期的那个 scope 后使用. 无法通过编译
println!("The longest string is {}", result);
}
如果是下面的情况, 则不需要标记生命期, rust 编译器就可以自己发现:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
实际上, 函数中的生命期标记是为了将参数和返回值关联起来.
struct 中如果使用引用成员, 则也需要标记生命期:
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,
};
}
Lifetime 推断
Rust 编译器会自动对函数或方法参数及返回值推断生命期, Rust 编译器通过三条规则对生命期进行推断, 如果无法推断且没有标记生命期, 则编译不通过.
我们将函数的参数生命期称为 input lifetime
, 返回值生命期称为 output lifetime
. 编译器推断生命期的三条规则中, 第一条应用到 input, 第二条和第三条应用到 output.
这三条规则是在长久的实践中发现的规律, 因此内置到编译器了, 编译器在编译过程中会自动按如下规则处理后, 再进行生命期检查(borrow checker), 通过检查则继续编译, 否则编译出错:
- 规则1: 编译器会自动给引用参数赋予生命期标记. 比如单个参数
fn foo<'a>(x: &'a i32)
, 两个参数则fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
, 依次类推. - 规则2: 经过规则 1 处理后, 如果只有一个 lifetime 参数, 则将该 lifetime 也标记到返回值
fn foo<'a>(x: &'a i32) -> &'a i32
- 规则3: 如果 input 包含
&self
或&mut self
, 则返回值直接被赋予和 self 参数相同的生命期标记.
有了这三条规则, 就可以发现:
fn first_word(s: &str) -> &str
实际是由编译器直接给的生命期标记, 我们不写也可以通过编译.fn longest(x: &str, y: &str) -> &str
无法推断返回值的生命期标记, 因此才会无法编译.
实际在开发过程中需要记住这几条推断规则, 如果需要手动给生命期标记的时候必须要给. 也需要明确只有使用 borrow(引用) 的时候才会牵扯到生命期.
static 生命期
'static
是一个特殊的生命期标记, 它表示该引用是和程序的生命期一致的.
总结
下面是一个结合 Trait, 泛型, 泛型约束, 生命期的例子:
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}