The Rust programming language 读书笔记——泛型与 trait(特征)

所有的编程语言都会致力于高效地处理重复概念,Rust 中的泛型(generics)就是这样一种工具。泛型是具体类型或其他属性的抽象替代。比如 Option<T>Vec<T>Hash<K, V> 等。

将代码提取为函数以减少重复工作

下面的代码可以用来在数字列表中找到最大值:

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }
    println!("The largest number is {}", largest);
}

为了消除重复代码,可以通过定义函数来创建抽象,令该函数可以接收任意整数列表作为参数并进行求值。

fn largest(list: &[i32]) -> i32 {
    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);
}

假设我们拥有两个不同的函数:一个用于在 i32 切片中搜索最大值;另一个用于在 char 切片中搜索最大值。代码可能是下面这个样子:

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);
}
泛型数据类型

在函数定义中使用
当使用泛型来定义一个函数时,我们需要将泛型放置在函数签名中用于指定参数和返回值类型的地方。
以这种方式编写的代码更加灵活,可以在不引入重复代码的同时向函数调用者提供更多的功能。

上面代码中的 largest_i32largest_char 是两个只在名称和签名上有所区别的函数。largest_i32 作用于 i32 类型的切片,而 largest_char 作用于 char 类型的切片。
这两个函数拥有完全相同的代码,因此可以通过在一个函数中使用泛型来消除重复代码。

在函数签名中使用泛型合并不同的 largest 函数:

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

其中 largest<T: PartialOrd + Copy> 部分的 PartialOrdCopy 是为类型 T 指定的两个 trait 约束(后面会提到)。

在结构体定义中使用
同样地,也可以使用 <> 语法来定义在一个或多个字段中使用泛型的结构体。

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 1 };
    let float = Point { x: 1.0, y: 4.0 };
}

如上面的代码,在结构名后的一对尖括号中声明泛型参数后,就可以在结构体定义中用于指定具体数据类型的位置使用泛型了。

在定义 Point<T> 结构体时仅使用了一个泛型参数,表明该结构体对某个类型 T 是通用的。但无论 T 具体的类型是什么,字段 xy 都同时属于这个类型。即 xy 只能是同一类型。

为了使结构体 Point 中的 x 和 y 能够被实例化为不同的类型,可以使用多个泛型参数。

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 1 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

在方法定义中使用
方法也可以在自己的定义中使用泛型:

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

上面的代码为结构体 Point<T> 实现了名为 x 的方法,返回一个指向 x 字段中 T 类型值的引用。

紧跟着 impl 关键字声明 T 是必须的。通过在 impl 之后将 T 声明为泛型,Rust 能够识别出 Point<T> 中尖括号内的类型是泛型而不是具体的类型。

实际上,可以单独为 Point<f32> 实例而不是所有的 Point<T> 泛型实例来实现特定的方法。
当在 Point<32> 声明中使用了明确的类型 f32,也意味着无需在 impl 之后附带任何类型声明了。

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

上面的代码意味着,类型 Point<f32> 将会拥有一个名为 distance_from_origin 的方法,而其他的 Point<T> 实例则没有该方法的定义。

结构体定义中的泛型参数并不总是与方法签名中使用的类型参数一致。

struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    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);
    // => p3.x = 5, p3.y = c
}

trait:定义共享行为

trait 用来向 Rust 编译器描述某些特定类型拥有的且能够被其他类型共享的功能,使我们可以以一种抽象的方式来定义共享行为。

trait 与其他语言中的接口(interface)功能类似,但也不尽相同。
类型的行为由该类型本身可供调用的方法组成。当我们可以在不同的类型上调用相同的方法时,就称这些类型共享了相同的行为。
trait 提供了一种将特定方法组合起来的途径,定义了为达成某种目的所必须的方法(行为)集合

定义 trait

假如我们拥有多个结构体(struct),分别持有不同类型、不同数量的文本字段。其中 NewsArticle 结构体存放新闻故事,Tweet 结构体存放推文。
我们还想要方便地获取存储在 NewsArticle 和 Tweet 实例中的数据摘要。因此需要为每个结构体类型都实现摘要行为,从而可以在这些实例上统一地调用 summarize 方法来请求摘要内容。

可以定义如下形式的 Summary trait:

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

在大括号中声明了用于定义类型行为的方法签名,即 fn summarize(&self) -> String;
方法签名后省略了大括号及方法的具体实现。任何想要实现这个 trait 的类型都需要为上述方法提供自定义行为。编译器会确保每一个实现了 Summary trait 的类型都定义了与这个签名完全一致的 summarize 方法
一个 trait 可以包含多个方法,每个方法签名占据单独一行并以分号结尾。

为类型实现 trait

完整代码:

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

struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

struct Tweet {
    username: String,
    content: String,
    reply: bool,
    retweet: bool,
}

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

fn main() {
    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());
    // => 1 new tweet: horse_ebooks: of course, as you probably already know, people
}

其中 impl Summary for NewsArticleimpl Summary for Tweet 部分负责为 NewsArticle 和 Tweet 两个结构体类型定义 Summary trait 中指定的 summarize 方法,并为该方法实现具体的行为。

默认实现

某些时候,为 trait 中的某些或所有方法都提供默认行为非常有用,使我们无需为每一个类型的 trait 实现都提供自定义行为。
当我们为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

如为 Summary trait 中的 summarize 方法指定一个默认的字符串返回值:

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

假如需要在 NewsArticle 的实例中使用上述默认实现,而不是自定义实现,可以指定一个空的 impl 代码块:
impl Summary for NewsArticle {}

此时虽然没有直接为 NewsArticle 定义 summarize 方法,依然可以在 NewsArticle 实例上调用 summarize 方法。

fn main() {
    let article = 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.",
        ),
    };

    println!("New article available! {}", article.summarize());
    // => New article available! (Read More...)
}

可以在默认实现中调用同一 trait 中的其他方法,哪怕这些被调用的方法没有默认实现。例如,可以为 Summary trait 定义一个需要被实现的方法 summarize_author(即 trait 中没有该方法的默认实现,需要在后续的类型中实现),再通过调用 summarize_authorsummarize 方法提供一个默认实现:

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

定义了 summarize_author 之后,就可以在 Tweet 实例上调用 summarize 了。summarize 的默认实现会进一步调用我们提供的 summarize_author 的定义。

fn main() {
    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());
    // => 1 new tweet: (Read more from @horse_ebooks...)
}
trait 作为参数

前面的代码中为 NewsArticle 和 Tweet 类型实现了 Summary trait,我们还可以定义一个 notify 函数来调用这些类型的 summarize 方法。语法如下:

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

上述代码没有为 item 参数指定具体的类型,而是使用了 impl 关键字及对应的 trait 名称。
这意味着 item 参数可以接收任何实现了指定 trait 的类型。在 notify 函数体内,则可以调用来自 Summary trait 的任何方法。
尝试使用其他类型(如 Stringi32)来调用 notify 函数则无法通过编译,因为这些类型没有实现 Summary trait。

上述代码其实只是 trait 约束的一种语法糖,完整形式如下:

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

通过 + 语法来指定多个 trait 约束
如果 notify 函数需要在调用 summarize 方法的同时显示格式化后的 item,则此处的 item 就必须实现两个不同的 trait:Summary 和 Display。
fn notify(item: impl Summary + Display) {

这一语法在泛型的 trait 约束中同样有效:
fn notify<T: Summary + Display>(item: T) {

where 从句简化 trait 约束
因为每个泛型都拥有自己的 trait 约束,定义多个类型参数的函数可能会有大量的 trait 约束信息需要被填写在函数名与参数列表之间。Rust 提供了一种替代语法。

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 的类型

同样可以在返回值中使用 impl Trait 语法,用于返回某种实现了特定 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,
    }
}

之前在介绍泛型时编写的 largest 函数就通过 trait 约束来限定泛型参数的具体类型。
largest 函数中,我们想要使用大于号运算符来比较两个 T 类型的值。这一运算符被定义为标准库 std::cmp::PartialOrd 的一个默认方法,因此需要在 T 的 trait 约束中指定 PartialOrd,才能够使 largest 函数用于任何可比较类型的切片上。

我们在编写 largest 函数的非泛型版本时,只尝试过搜索 i32char 类型的最大值。这两种都是拥有确定大小并存储在栈上的类型,实现了 Copy trait。
但当我们尝试将 largest 函数泛型化时,list 参数中的类型有可能是没有实现 Copy trait 的。为了确保这个函数只会被那些实现了 Copy trait 的类型所调用,还需要把 Copy 加入到 T 的 trait 约束中。

所以最终的 largest 函数采用如下声明:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {

使用 trait 约束有条件地实现方法

通过在带有泛型参数的 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);
        }
    }
}

fn main() {
    let pair = Pair::new(3, 4);
    pair.cmp_display()
}

上面的代码中,所有的 Pair<T> 类型都会实现 new 方法,但只有在内部类型 T 实现了 PartialOrd(用于比较)和 Display(用于打印)这两个 trait 的前提下,才会实现 cmd_display 方法。

总结

借助于 trait 和 trait 约束,我们可以在使用泛型参数消除重复代码的同时,向编译器指明自己希望泛型拥有的功能。而编译器则可以利用这些 trait 约束信息来确保代码中使用的具体类型提供了正确的行为
在动态语言中,尝试调用类型没有实现的方法会导致在运行时出现错误。Rust 将这些错误出现的时机转移到了编译期,我们无需编写那些用于在运行时检查类型的代码,这一机制在保留泛型灵活性的同时提升了代码性能。

参考资料

The Rust Programming Language

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容