Rust快速入门(8) - 泛型、trait & 生命周期

泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。

trait 是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。

生命周期(*lifetimes*),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。

Rust 的泛型 与 具体类型代码 相比, 没有任何性能损失. Rust 通过在编译时进行泛型代码的 单态化(*monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

1. 泛型数据类型

泛型是一种抽象,使用类型参数而非具体类型,使得同一段代码可以处理不同类型的数据。

1.1 函数中的泛型

通过定义泛型函数,可以避免代码重复。考虑以下示例,两个函数分别找出整数列表和字符列表中的最大值:

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

通过泛型函数,可以将两个函数合并为一个:

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

泛型函数定义时,在函数名后的尖括号中声明类型参数,如<T>,然后使用该类型参数代替具体类型。

1.2 结构体中的泛型

结构体定义也可以使用泛型参数:

// 单一泛型参数
struct Point<T> {
    x: T,
    y: T,
}

// 多泛型参数
struct Point<T, U> {
    x: T,
    y: U,
}

使用单一泛型参数的结构体时,所有使用该参数的字段必须是同一类型:

let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
// 错误:类型不匹配
// let wont_work = Point { x: 5, y: 4.0 };

而多泛型参数的结构体允许不同字段有不同类型:

let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };

1.3 枚举中的泛型

枚举也可以使用泛型定义。标准库中的Option<T>Result<T, E>就是典型例子:

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

1.4 方法中的泛型

可以在结构体的impl块中定义泛型方法:

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()
    }
}

方法定义中也可以使用不同于结构体的泛型参数:

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

1.5 泛型代码的性能

使用泛型不会导致运行时性能损失。Rust通过单态化(monomorphization)在编译时将泛型代码转换为特定类型的代码。

单态化例子:

// 泛型代码
let integer = Some(5);
let float = Some(5.0);

// 编译后的代码(简化表示)
enum Option_i32 {
    Some(i32),
    None,
}
enum Option_f64 {
    Some(f64),
    None,
}
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);

2. Trait:定义共享行为

Trait(特性)定义了类型可以具有的功能,是Rust实现接口抽象的方式。

2.1 定义Trait

使用trait关键字定义特性:

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

这定义了一个名为Summary的特性,它有一个方法summarize。实现该特性的类型必须提供此方法的具体实现。

2.2 为类型实现Trait

使用impl Trait for Type语法为类型实现特性:

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)
    }
}

实现特性的限制:只能为本地类型实现本地特性。即无法为外部类型实现外部特性(这称为孤儿规则)。

2.3 默认实现

特性可以提供方法的默认实现:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

实现此特性的类型可以使用默认实现,也可以覆盖它:

impl Summary for NewsArticle {} // 使用默认实现

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content) // 覆盖默认实现
    }
}

默认实现可以调用特性中的其他方法,即使这些方法没有默认实现:

pub trait Summary {
    fn summarize_author(&self) -> String;  // 没有默认实现
    
    fn summarize(&self) -> String {  // 默认实现调用了没有默认实现的方法
        format!("(Read more from {}...)", self.summarize_author())
    }
}

2.4 特性作为参数

可以指定函数参数为实现了某特性的类型:

// 使用impl Trait语法
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// 等价的特性约束语法
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

当有多个参数时,特性约束语法更灵活:

// 允许不同类型,只要都实现了Summary
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

// 强制要求相同类型,且实现了Summary
pub fn notify<T: Summary>(item1: &T, item2: &T) {}

2.5 多特性约束

可以使用+语法指定参数必须实现多个特性:

pub fn notify(item: &(impl Summary + Display)) {}

// 或使用特性约束语法
pub fn notify<T: Summary + Display>(item: &T) {}

2.6 使用where从句简化特性约束

当特性约束变得复杂时,可以使用where从句简化:

// 不使用where从句
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
{}

2.7 返回实现特定特性的类型

可以使用impl Trait语法指定返回类型为实现了某特性的值:

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,
    }
}

这对于返回闭包或迭代器等复杂类型特别有用。

注意:使用此语法只能返回单一具体类型,不能在不同条件下返回不同类型。

2.8 使用特性约束有条件地实现方法

可以为满足特定特性约束的类型有条件地实现方法:

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);
        }
    }
}

也可以为实现了特定特性的所有类型实现某个特性,这称为覆盖实现(blanket implementations):

impl<T: Display> ToString for T {
    // 实现略
}

这就是为什么所有实现了Display特性的类型都可以调用to_string方法。

3. 验证引用的生命周期

生命周期是Rust另一种泛型形式,用于确保引用在需要它们有效的时候一直有效。

3.1 生命周期避免悬垂引用

生命周期的主要目的是防止悬垂引用:

// 以下代码无法编译
fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

编译器使用借用检查器(borrow checker)分析作用域,确保所有借用都是有效的。

3.2 函数中的泛型生命周期

当函数参数包含引用时,可能需要使用生命周期注解来明确引用间的关系:

// 无法编译,因为编译器不知道返回的引用与哪个参数有关
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 使用生命周期注解
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

生命周期注解不会改变引用的实际生命周期,只是描述了多个引用之间生命周期的关系。

3.3 生命周期注解语法

生命周期参数名以撇号(')开头,通常使用小写字母,常见的是'a。 注解位于引用符号(&)后面,如:

&i32        // 一个引用
&'a i32     // 一个具有显式生命周期的引用
&'a mut i32 // 一个具有显式生命周期的可变引用

3.4 函数签名中的生命周期注解

在函数签名中,生命周期注解与泛型类型参数一样放在尖括号内:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这告诉编译器:函数返回的引用的生命周期与参数xy的生命周期中较短的那个一样长。

3.5 结构体定义中的生命周期注解

含有引用的结构体必须在每个引用上添加生命周期注解:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

3.6 生命周期省略

Rust的某些常见模式允许省略生命周期注解,这称为生命周期省略规则:

  1. 每个引用参数都有自己的生命周期参数
  2. 如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数
  3. 如果有多个输入生命周期参数,但其中一个是&self&mut self,那么self的生命周期被赋给所有输出生命周期参数

因此,以下函数不需要显式添加生命周期注解:

fn first_word(s: &str) -> &str {
    // 实现略
}

编译器应用生命周期省略规则后,实际上是:

fn first_word<'a>(s: &'a str) -> &'a str {
    // 实现略
}

3.7 方法定义中的生命周期注解

方法中的生命周期注解类似于函数,但受益于生命周期省略规则,尤其是第三条规则:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
    
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

3.8 静态生命周期

特殊的生命周期'static表示引用在整个程序运行期间都有效:

let s: &'static str = "I have a static lifetime.";

所有字符串字面量都有'static生命周期,因为它们存储在可执行文件的数据段中。

4. 综合应用

可以在函数签名中结合使用泛型类型参数、特性约束和生命周期:

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
    }
}

总结

Rust的泛型、trait和生命周期是强大的抽象工具,它们共同提供了编写灵活、可重用且安全的代码的能力:

  • 泛型 - 允许我们编写适用于不同类型的代码
  • **Trait ** - 确保即使类型是泛型的,它们也会有我们需要的行为
  • 生命周期 - 确保引用在我们使用它们的整个过程中都是有效的

最重要的是,这些分析都在编译时进行,不会影响运行时性能。这使得Rust代码既灵活又高效。


好好学习,天天向上