Skip to main content

泛型类型, 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);
}
}
}

在上述泛型实现中, 我们仅针对实现了 DisplayPartialOrd 的具体类型才提供 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. 规则1: 编译器会自动给引用参数赋予生命期标记. 比如单个参数 fn foo<'a>(x: &'a i32), 两个参数则 fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依次类推.
  2. 规则2: 经过规则 1 处理后, 如果只有一个 lifetime 参数, 则将该 lifetime 也标记到返回值 fn foo<'a>(x: &'a i32) -> &'a i32
  3. 规则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
}
}