Rust for cpp dev - Trait 的高级用法

在之前的章节中,我们的代码中出现了一些 trait 的用法,但是并没有展开来讲。本章我们将集中介绍 trait 的所有高级用法。

Placeholder Type

再来看看之前的 Iterator 定义:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

这里的 type Item 就是一个 placeholder type。它存在的意义是让这个 trait 的定义中的其他函数签名可以使用 Item 作为类型的 placeholder。而当实现 Iterator 这个 trait 时,用实际的类型去替换这个 placeholder。

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--

这听起来非常像泛型。那么为什么不写成下面的形式呢:

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

这主要是为了保证对于一个类型,只有一个 Iterator 的实现。如果用泛型,那么就可以实现多个,例如Iterator<u32>Iterator<String>。这样在使用的时候,也必须指明类型,对于 Iterator,我们显然只需要一种实现。

默认泛型参数和操作符重载

为泛型指定默认类型的语法是:

<PlaceholderType=ConcreteType>

一个经典用例是操作符重载。Rust 不允许任意创建或者重载操作符,只能通过重载 std::ops 中定义的 trait 来实现。例如,我们可以重载对 Point 的加操作:

use std::ops::Add;

// Debug for "{:?}", PartialEq for "=="
#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

上面代码中我们实现了 std::ops::Add trait:

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

这个 trait 是含有一个默认参数Rhs的,使用它可以允许不同类型相加,这也是为什么它需要一个 placeholder type Output,否则返回值不知道应该选哪个类型了。

下面的代码实现了不同类型间的 Add

use std::ops::Add;

struct Meters(u32);

#[derive(Debug, PartialEq)]
struct Millimeters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

fn main() {
    assert_eq!(Millimeters(500) + Meters(1), Millimeters(1500));
}

因此,默认参数的主要作用是:

  • 允许扩展一个类型,但是不破坏现有的代码
  • 允许用户自定义一些极少会用到的行为

调用不同 trait 的同名方法

Rust 允许不同的 trait 用同样的方法名。因此,对于同一个对象,如何区分调用哪个 trait 的方法呢?

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
    // specify trait name before method to disambiguate
    Pilot::fly(&person);
    Wizard::fly(&person);
}

可以通过明确指出 trait 名区分:

    person.fly();
    // specify trait name before method to disambiguate
    Pilot::fly(&person);
    Wizard::fly(&person);

使用 supertrait 表明 trait 的依赖关系

有时,实现某个 trait A 需要使用另一个 trait B。这时候,trait B 就称为 supertrait。我们可以用下面形式表明他们的关系:

trait A: B {
    // snip

这样,如果某个类只实现了 A 但是没有实现 B,编译时就会报错。

使用 newtype pattern 实现 trait

回忆一下之前讲过的“孤儿准则”:

如果 type 和 trait 都不是在当前 crate 定义的,那不允许为这个 type 实现这个 trait。

实际上,我们可以通过 newtype pattern 绕过这个检查,即,将这个 type 封装在一个 tuple struct 里。

例如我们希望自己给 Vec<T> 类型实现 Display trait,根据孤儿准则,由于 Vec<T> 类型和 Display trait 都是在其他 crate 定义的,因此无法直接实现。我们就将 Vec<T> 包装成另一个类型绕过检查:

struct Wrapper(Vec<String>);

代码为:

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

newtype pattern 还能实现更多的 trait,例如 Deref,来直接返回内部的数据 &self.0

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

推荐阅读更多精彩内容