Skip to main content

Rust 基础入门 - 第七章

泛型 Generic

有时候我们需要使用同一个函数来处理不同类型的数据,比如一个简单的加法函数,两个参数可能是整数,也可能是浮点数,甚至是字符串,这种时候我们不希望针对每一种类型都写一个函数,这时候就可以使用泛型。比如以下这个例子,如果不使用泛型,我们需要写两个函数,一个处理整数,一个处理浮点数。

fn add_i32 (a:i32, b:i32) -> i32 {
a + b
}
fn add_f64 (a:f64, b:f64) -> f64 {
a + b
}

如果使用泛型,就会简单很多:

fn add<T>(a:T, b:T) -> T {
a + b
}

上述代码中的 T 就是 泛型参数 ,实际上,你可以随意起名字,但是 T 代表 Type 相对来说,是一个完美的字母。这个函数的意思就是,接受两个参数,类型都是 T,返回值也是 T。这样,我们就可以使用 add 函数来处理整数、浮点数、字符串等等。

但是如果运行以上代码,依然会得到报错:

error [E0369]: cannot add `T` to `T`

给出的理由言简意赅,不是所有 T 类型都可以相加。因此,我们要对该函数的泛型参数进行约束,使得它只能接受可以相加的类型。这里我们使用 std::ops::Add 这个特征来约束,这个特征定义了 + 运算符的行为:

fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T {
a + b
}

结构体中的泛型

同样的,结构体中的字段也可以用泛型,这里定义了一个坐标点的结构,它的 x 和 y 坐标都是泛型:

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

但是需要注意的是,x 和 y 的类型必须是一致的,否则会报错,如果想要 x 和 y 的类型可以不一致,那么就要再声明一个泛型:

struct Point<T,U> {
x: T,
y: U,
}
fn main () {
let p = Point {x: 1, y :1.1};
}
note

如果当泛型参数过多的时候,会导致代码的可读性变差,这时候可以考虑拆分成多个结构体。

枚举中的泛型

在枚举中使用泛型很常见,比如 Option 枚举,它的 Some 可以是任意类型的值,也可以是 None

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

还有它的好兄弟 Result 枚举,它的 OkErr 也可以是任意类型的值:

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

特征 Trait

Rust 中特征 Trait 的概念和其他语言中的接口很相似,它定义了一些方法,如果一个类型实现了某个特征,那么它就可以调用这些方法。

既定义了 一组可以被共享的行为,只要实现了特征,就可以使用这些行为

定义特征

例如,我们有两个结构体,一个是新闻,一个是微博这两种内容载体,我们想对这两者都实现一个共享的 summarize 方法,用来返回一个摘要,那么我们可以定义一个特征 Summary,然后在两个结构体中实现这个特征:

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

这里我们使用 trait 关键字定义了一个名为 Summary 的特征,它有一个 summarize 方法,返回一个字符串。

定义这样一个特征,只是定义了一组行为,但是并没有具体实现,我们需要在结构体中定义实现这个特征的 具体方法

实现特征

还是上面这个例子,为两个结构体实现 Summary 特征:

pub trait Summary {
fn summarize(&self) -> String;
}
pub struct News {
pub title: String, // 标题
pub author: String, // 作者
pub content: String, // 内容
}

impl Summary for News {
fn summarize(&self) -> String {
format!(" 文章 {}, 作者是 {}", self.title, self.author)
}
}

pub struct Weibo {
pub username: String,
pub content: String
}

impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{} 发表了微博 {}", self.username, self.content)
}
}
fn mian() {
let news = News{title: "title".to_string(),author: "wjwei".to_string(), content: "Rust 棒极了!".to_string()};
let weibo = Weibo{username: "wjwei".to_string(),content: " 好像微博没 Tweet 好用 ".to_string()};

println!("{}",post.summarize());
println!("{}",weibo.summarize());
}

实现特征的方法和为结构体实现方法类似,只是在 impl 后面加上特征名,然后实现特征中定义的方法即可。

乍一看,似乎没什么用,和在结构体中定义方法有什么区别呢?

特征定义和实现的位置(孤儿规则)

关于特征定义和实现的位置,一个很重要的原则:

如果为类型 A 实现特征 T ,那么 AT 至少一个需要在当前作用域中定义

也就是说:

  • 如果要实现外部的特征,要先将特征引入到当前作用域中
  • 不能对外部类型实现外部特征
  • 可以对外部类型实现自己定义的特征
  • 可以对自己定义的类型实现外部特征
tip

外部是指当前作用域之外,比如标准库、第三方库等

这条规则被称为 孤儿规则 (Orphan Rule),可以保证代码之间不会相互破坏。

默认实现

特征中的方法可以有默认实现,这样在实现特征的时候,就不需要再实现这个方法了,比如我们为 Summary 特征添加一个默认实现:

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

再来一个例子:

impl Summary for News {}

impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{} 发表了微博 {}", self.username, self.content)
}
}

这样,News 结构体就会使用默认的 summarize 方法,而 Weibo 结构体会重载自己的 summarize 方法。

特征作为函数参数

特征可以作为函数参数,这样可以接受任意实现了特征的类型,比如:

pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}

这里的 item 参数可以是任意实现了 Summary 特征的类型,比如 News 或者 Weibo。这个用法相当实用!

特征约束 Trait Bound

上面这种写法, 是一种语法糖🍬,真正完整的书写形式如下:

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

结合学过的泛型知识,形如 <T: Summary> 的写法被称为 特征约束 (Trait Bound),它表示泛型 T 必须实现 Summary 特征。

在部分简单场景下,可以使用 impl Trait 的简写形式,但是在一些复杂场景下,特征约束可以提供更多的灵活性和语法表现能力,比如:

pub fn notify_1(item1: &impl Summary, item2: &impl Summary) {}
pub fn notify_2<T: Summary>(item1: T, item2: T) {}

这两个函数的参数都是实现了 Summary 特征的类型,但是第一个函数的两个参数可以是不同的类型,而第二个函数的两个参数必须是相同的类型,并且这个类型必须实现了 Summary 特征。

多重约束

可以指定多个约束,比如:

pub fn notify_1(item: impl Summary + Display) {}
pub fn notify_2<T: Summary + Display>(item: T) {}

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

这样是把约束放到了函数签名之外,更加清晰。

使用特征约束有条件地实现方法和特征

特征约束可以让我们有条件地实现方法和特征,比如:

fn main() {
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);
}
}
}
}

最后一段代码,对 Pair 结构体实现了 cmp_display 方法,但是这个方法只有在 T 实现了 DisplayPartialOrd 两个特征的时候才会被实现。

这样的代码可读性更好,泛型、特征约束、返回值类型、方法体都在一起,更加清晰。

函数返回值类型中使用特征约束

函数返回值类型中也可以使用特征约束,比如:

fn returns_summarizable() -> impl Summary {
News {
headline: String::from("headline"),
location: String::from("location"),
}
}

这里的返回值类型是 impl Summary,表示返回值类型必须实现 Summary 特征。对于第三方调用者而言,无需知道具体的返回值类型,只需要知道它实现了 Summary 特征即可。

在某些场景中,返回类型相当复杂,可以使用特征约束来简化函数签名。

但是这种形式也有局限性,只能有一个具体的类型,比如:

fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
News {
title: String::from(
"Lakers 0:3 Nuggets",
),
author: String::from("NBA"),
content: String::from(
"The Lakers canot stop Jokic",
),
}
} else {
Weibo {
username: String::from("wjwei"),
content: String::from(
"lalala",
),
}
}
}

上面这段代码不能通过编译,因为返回值类型有两个。

通过 drive 宏派生特征

Rust 提供了 derive 宏,可以自动实现一些特征,比如 DebugClone 等,被标记的对象会自动实现这些特征,继承相应的功能。

比如 Debug 特征,标记后,可以使用 {:?} 打印对象。

具体所有的特征可以参考 Rust Reference

进阶内容

详见后续进阶章节。

参考资料