rust学习-泛型和trait

泛型

Option,Vec,HashMap<K, V>,Result<T, E>等,取函数以减少代码重复的机制

背景

两个函数,不同点只是名称和签名类型

复制代码
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);
}

重写如下

复制代码
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);
}

// 编译失败,提示受限的类型,因为有的类型没法用
// help: consider restricting type parameter `T`
// fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T
// binary operation `>` cannot be applied to type `T`
// 具体的解法见下文

结构体的泛型

复制代码
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 integer_and_float = Point { x: 5, y: 4.0 };
}

枚举中的泛型

复制代码
// Some,它存放了一个类型 T 的值、不存在任何值的 None
enum Option<T> {
    Some(T),
    None,
}

// Result 枚举有两个泛型类型,T 和 E
// Result 有两个成员:Ok,它存放一个类型 T 的值,而 Err 则存放一个类型 E 的值
// 如当成功打开文件的时候,T 对应的是 std::fs::File 类型
// 比如当打开文件出现问题时,E 的值则是 std::io::Error 类型
enum Result<T, E> {
    Ok(T),
    Err(E),
}

结构体的泛型

为所有类型的结构体提供方法

复制代码
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());
}

只为f32提供方法

复制代码
struct Point<T> {
    x: T,
    y: T,
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    // 打开如下两行,编译报错
    // method `distance_from_origin` not found for this struct
    // let p = Point { x: 5, y: 10 };
    // println!("result={}", p.distance_from_origin());

    // 如下两行则正常编译
	 let p = Point { x: 5.1, y: 10.2 };
	 println!("result={}", p.distance_from_origin());
}

方法使用了与结构体定义中不同类型的泛型

复制代码
struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    // 泛型参数 V 和 W 声明于 fn mixup 后,因为他们只是相对于方法本身
    // 删除之后编译失败:cannot find type `V、W` in this scope
    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);
}

泛型是否会影响运行时性能

Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失

编译时进行泛型代码的单态化:通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程

复制代码
enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    // 将泛型定义 Option<T> 展开为 Option_i32 和 Option_f64
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

trait

trait,定义泛型行为的方法

可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型

类似于其他语言的接口

示例

为NewsArticle和Tweet实现相同的摘要接口

复制代码
// trait
pub trait Summary {
    fn summarize(&self) -> String;
}

// 在相同的 lib.rs 里定义了 Summary trait 和 NewsArticle 与 Tweet 类型
// 他们位于同一作用域
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

// 为不同的类型实现不同的summarize
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,
}

// 为不同的类型实现不同的summarize
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
};

println!("new tweet: {}", tweet.summarize());

trait复用

别人想要用该crate的功能为其自己的库作用域中的结构体实现 Summary trait:

(1)需要将 trait 引入作用域:use aggregator::Summary;

(2)Summary 必须是公有 trait, 其他 crate 才可以实现它,将 pub 置于 trait 之前

只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait:

(1)为当前crate 的自定义类型 Tweet 实现如标准库中的 Display trait,Tweet 类型位于本地的作用域

(2)在 当前crate 中为 Vec 实现 Summary,Summary trait 位于 该crate 本地作用域

不能为外部类型实现外部 trait:

(1)不能在 aggregator crate 中为 Vec 实现 Display trait,Display 和 Vec 都定义于标准库中,不在当前作用域

专属词汇:这个限制是被称为 相干性(coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),忽略即可

trait默认实现

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

如果想要对 NewsArticle 实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {} 指定一个空的 impl 块。

为 summarize 创建默认实现并不要求对Tweet 上的 Summary 实现做任何改变。其原因是(重载一个默认实现的语法)与(实现没有默认实现的 trait 方法)的语法一样

trait提供很多接口但只实现部分

复制代码
pub trait Summary {
    fn summarize_author(&self) -> String;

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

// 为了使用这个版本的 Summary
// 只需在实现 trait 时定义 summarize_author
impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

// 具体实现
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());

trait作参数-impl trait

使用 trait 来接受多种不同类型的参数

复制代码
// 对于 item 参数,指定 impl 关键字和 trait 名称,而不是具体的类型
// 该参数支持任何实现了指定 trait 的类型
pub fn notify(item: impl Summary) {
    // 在 notify 函数体中,可以调用任何来自 Summary trait 的方法
    // 可以传递任何 NewsArticle 或 Tweet 的实例来调用 notify
    println!("Breaking news! {}", item.summarize());
}

trait作参数-trait Bound

impl Trait 很方便,适用于短小的例子

trait bound 则适用于更复杂的场景

复制代码
// trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面
pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

获取两个实现了 Summary 的参数

复制代码
// 适用于 item1 和 item2 允许是不同类型的情况(只要它们都实现了 Summary)
pub fn notify(item1: impl Summary, item2: impl Summary) {

// 强制它们都是相同类型
// 泛型 T 被指定为 item1 和 item2 的参数限制
// 如此传递给参数 item1 和 item2 值的具体类型必须一致。
pub fn notify<T: Summary>(item1: T, item2: T) {

指定多个 trait bound

既要显示 item 的格式化形式,也要使用 summarize 方法

复制代码
pub fn notify(item: impl Summary + Display) {
pub fn notify<T: Summary + Display>(item: T)

简化trait bound

每个泛型有其自己的 trait bound

复制代码
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

返回值是trait类型

和go的interface太像了,但又像阉割版的

调用方并不知情返回的类型,只适用于返回单一类型的情况

复制代码
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,
    }
}

糟糕的例子

复制代码
fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        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."),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from("of course, as you probably already know, people"),
            reply: false,
            retweet: false,
        }
    }
}

最开始例子的修复

使用泛型参数 trait bound 来指定所需的行为

在 largest 函数体中想要使用大于运算符(>)比较两个 T 类型的值。这个运算符被定义为标准库中 trait std::cmp::PartialOrd 的一个默认方法。

修改函数签名

复制代码
fn largest<T: PartialOrd>(list: &[T]) -> T {

// 报错如下
// cannot move out of type `[T]`, a non-copy slice
// i32 和 char 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 Copy trait
// 将 largest 函数改成使用泛型后,现在 list 参数的类型就有可能是没有实现 Copy trait
// 意味着可能不能将 list[0] 的值移动到 largest 变量中

解决方法1

为了只对实现了 Copy 的类型调用这些代码

可以在 T 的 trait bounds 中增加 Copy

复制代码
fn largest<T: PartialOrd + Copy>(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);
}

解决方法2

不希望限制 largest 函数只能用于实现了 Copy trait 的类型

可以在 T 的 trait bounds 中指定 Clone 而不是 Copy

克隆 slice 的每一个值使得 largest 函数拥有其所有权

使用 clone 函数意味着对于类似 String 这样拥有堆上数据的类型

会潜在的分配更多堆上空间

而堆分配在涉及大量数据时可能会相当缓慢

解决方式3

返回在 slice 中 T 值的引用

如果将函数返回值从 T 改为 &T 并改变函数体使其能够返回一个引用

将不需要任何 Clone 或 Copy 的 trait bounds 而且也不会有任何的堆分配

trait Bound 有条件地实现方法

复制代码
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,
        }
    }
}

// 只有那些为 T 类型实现了 PartialOrd trait (来允许比较) 和 Display trait (来启用打印)的 Pair<T> 
// 才会实现 cmp_display
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);
        }
    }
}

对任何实现了特定 trait 的类型有条件地实现 trait

blanket implementations,广泛用于 Rust 标准库

标准库为任何实现了 Display trait 的类型实现了 ToString trait

对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法

// 某个行为依赖于另一个行为

复制代码
impl<T: Display> ToString for T {
    // --snip--
}

使用场景

复制代码
let s = 3.to_string();

总结

动态类型语言中如果尝试调用一个类型并没有实现方法,运行时报错

Rust 将这些错误移动到编译时,甚至在代码能运之前就强迫修复错误

无需编写运行时检查行为的代码,因为在编译时就已经检查过了

相比其他不愿放弃泛型灵活性的语言有更好的性能

相关推荐
DongLi012 天前
rustlings 学习笔记 -- exercises/05_vecs
rust
西岸行者2 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意2 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
番茄灭世神2 天前
Rust学习笔记第2篇
rust·编程语言
别催小唐敲代码2 天前
嵌入式学习路线
学习
毛小茛3 天前
计算机系统概论——校验码
学习
babe小鑫3 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms3 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下3 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。3 天前
2026.2.25监控学习
学习