Skip to main content
6 泛型和特征

6 泛型和特征

•1 min•166 words

泛型

前言

泛型,目的是让一套代码能处理各种类型。有的是通过兼容不同类型例如Java,有的是通过快速生成另一套定义例如C++。那么Rust的泛型是如何设计的呢?

Rust的泛型:编译后生成新代码(单态化)

说起快速生成新的代码,想起元组,数组,能快速定义新类型。但是泛型能作用与函数,结构体,枚举…

和C++的策略一样,泛型通过编译器,在编译后生成全新的代码。但是这会导致代码膨胀,过多的代码会导致CPU缓存命中数据的命中率低。

除此之外,rust还特别注重安全,并非每一种类型都能适配这套代码,所以rust决定让我们显式指定泛型代码的适用类型(特征trait),如果有意外类型,编译器能进行错误提示。

Faq

泛型代码只能被用于几种语法中
1 函数定义 2 方法定义 3 结构体定义 4 枚举定义

泛型代码的定义方法

定义泛型代码的一个特制是,在代码名的后面用<>包裹你需要传入的 类型参数。然后在代码的参数列表类型,返回值类型,字段类型等 使用传入的类型参数来声明。

类型参数的传入与使用

Faq
Summary
  1. 显式传入类型参数:在调用时用 ::<>(涡轮鱼)或类型标注明确告诉编译器。
    函数调用时:foo::<i32>(x)
    变量声明时:let v: Vec<String> =

  2. 隐式传入,编译器会根据传入的实参(值)自动推导出泛型参数。
    也就是在参数类型用类型参数替代,这个时候就会从参数类型传入。

Summary
  1. 泛型结构体名,泛型函数名(泛型是唯一表示的一部分)
  2. impl作用域声明,重新“声明”一个同名的占位符避免编译器报错
  3. 参数、返回值可以使用泛型表示类型。
泛型代码的使用方法

分为泛型类型标注,与泛型方法、函数调用两种。“类型只管套尖括号,调用必须加双冒号。”
.
对于类型来说,直接可以直接通过类型名<具体泛型>来唯一确定一个类型。
.
而对于方法与函数来说,不能直接接<具体泛型>,因为可能会有同名字段,此时<可以被理解为比较操作符,此时有二义性。所以,rust要求必须使用::<具体类型>的语法。

能共用泛型说明有相同的特征

既然这些类型都能使用一套泛型代码,为什么其他类型不行?其实泛型将这些类型划分成了另外的一类,这类类型拥有共同的特征,这种特征就像是类中的类。

特征trait

trait 就像 “HashMap”,key是结构体id,value是具体的实现

我们只需要把trait导入,编译器会自动匹配到对应trait中对应结构体的实现,然后将其地址替换到具体的调用。

“特征”不改变结构体id,只是让其具有新个能力

rust中结构体的数据与方法解耦。无论方法如何增加(只能增不能减),结构体还是那个结构体。所以我们可以随意拓展外部类。添加的结构体方法,会在内存中对应结构体的声明块与实现块中以指针数据的方式拓展。

Todo
Warning
  • 方法调用优先级:当你在作用域内引入了多个同名 trait(或与原生方法同名)时,Rust 编译器会优先选择原生方法(Inherent Method)。只有当前类型没有原生匹配时,编译器才会去搜索作用域内可见的 trait 方法。
  • 编译期绑定:原生方法本质上是该类型的固有实现(Inherent Implementation),它和类型是强绑定的,不需要引入任何 trait 作用域就能直接调用。而拓展方法必须满足 Trait in Scope(引入 trait 到当前作用域)才能调用。
Summary
pub trait Summary {
   fn summarize(&self) -> String;
}
实现trait

结构体可声明自己实现了某trait:impl 特制名 for 结构体名 { 代码块 }

pub trait Summary { 
	fn summarize(&self) -> String; 
}
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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

trait 必须和类型一起引入作用域以便使用额外的 trait 方法。

非侵入式trait

为一个结构体实现一个trait根本不需要更改结构体和trait中的任何东西,结构体、trait、和trait的实现这三块完全独立。
这使得我们可以为外部类型实现内部trait,内部类型实现外部trait,内部类型实现内部trait。但是不能为外部类型实现外部trait,这样容易造成trait实现的冲突。
(也就是说要有一个是内部的,确保了唯一性(与本crate相关所以其他crate无法防制))

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

这个限制是被称为相干性(coherence)的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你的代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

trait的默认实现

某些trait中的方法提供了模式的实现。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
impl Summary for NewsArticle {} // 所以这里可以使用空实现

请注意,无法从一个方法的重写实现中调用与其同名的默认实现。

trait参数

我们可以使用impl <trait名字>来表示一个实现了某特质类型的参数。

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
Summary
pub fn notify<T: Summary>(item: &T) {
  println!("Breaking news! {}", item.summarize());
}

rust会在编译期将trait参数按照传入的参数类型生成对应的单态化代码。在运行时没有任何检查,直接用设定好的结构体内存模型调用对应的方法地址。

多trait约束

如果一个参数的约束是实现了多个trait,可以使用+语法
或者使用where语法,将泛型的约束后置

pub fn notify(item: &(impl Summary + Display)) 
// 或者
pub fn notify<T: Summary + Display>(item: &T)
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{ 代码 }

trait返回值

和trait参数不同的一点是trait返回值必须确定类型

因为trait参数编译器可以根据调用时传参确定这个函数需要用到什么具体类型的传参版本,从而单态化。而返回值,如果函数只返回一种类型编译器当然允许使用trait返回值,但是当函数中返回不同trait实现类型的时候,trait无法确定调用处究竟要调用哪个返回值类型的函数,所以rust不允许这样的语法。

第十八章的“使用 trait object 来抽象出共享行为”部分会介绍如何编写一个允许返回不确定类型的函数。

对特定单态泛型实现方法

在impl块中对泛型添加约束从而只对特定类型impl

我们知道泛型最终会单态化成各种各样的具体类型,我们可以以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);
        }
    }
}

也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait 约束的类型实现 trait 被称为 blanket implementations,它们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display trait 的类型实现了 ToString trait。这个 impl 块看起来像这样:

impl<T: Display> ToString for T {     
	// --snip--
 }

上层trait

上层trait依赖其他trait才能存在

要求我们必须先实现其依赖的trait,才能实现本trait
语法就是+

例如官方的Ord,这个是用于sort的trait。

pub trait Ord: Eq + PartialOrd {
    // Required method
    fn cmp(&self, other: &Self) -> Ordering;

    // Provided methods
    fn max(self, other: Self) -> Self
       where Self: Sized { ... }
    fn min(self, other: Self) -> Self
       where Self: Sized { ... }
    fn clamp(self, min: Self, max: Self) -> Self
       where Self: Sized { ... }
}

我们必须让结构体先实现Eq trait和PartialOrd trait