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 将这些错误移动到编译时,甚至在代码能运之前就强迫修复错误

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

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

相关推荐
霍格沃兹测试开发学社测试人社区15 分钟前
软件测试学习笔记丨Flask操作数据库-数据库和表的管理
软件测试·笔记·测试开发·学习·flask
今天我又学废了31 分钟前
Scala学习记录,List
学习
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
Mephisto.java2 小时前
【大数据学习 | kafka高级部分】kafka中的选举机制
大数据·学习·kafka
南宫生2 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
武子康3 小时前
大数据-212 数据挖掘 机器学习理论 - 无监督学习算法 KMeans 基本原理 簇内误差平方和
大数据·人工智能·学习·算法·机器学习·数据挖掘
使者大牙3 小时前
【大语言模型学习笔记】第一篇:LLM大规模语言模型介绍
笔记·学习·语言模型
As977_4 小时前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
ajsbxi4 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
Rattenking4 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js