陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

13|类型系统:如何使用trait来定义接口?

vtable
Trait Object
Trait Object 的安全性
Trait Object 实现机理
异步 trait
对象安全
孤儿规则
子类型多态
特设多态
动态分派
静态分派
泛型 trait
带关联类型的 trait
使用
定义
延伸阅读
注意事项
Trait 的应用
Trait 的分发方式
Trait 基础
Rust Trait 系统

该思维导图由 AI 生成,仅供参考

你好,我是陈天。
通过上一讲的学习,我们对 Rust 类型系统的本质有了认识。作为对类型进行定义、检查和处理的工具,类型系统保证了某个操作处理的数据类型是我们所希望的。
在 Rust 强大的泛型支持下,我们可以很方便地定义、使用泛型数据结构和泛型函数,并使用它们来处理参数多态,让输入输出参数的类型更灵活,增强代码的复用性。
今天我们继续讲多态中另外两种方式:特设多态和子类型多态,看看它们能用来解决什么问题、如何实现、如何使用。
如果你不太记得这两种多态的定义,我们简单回顾一下:特设多态包括运算符重载,是指同一种行为有很多不同的实现;而把子类型当成父类型使用,比如 Cat 当成 Animal 使用,属于子类型多态。
这两种多态的实现在 Rust 中都和 trait 有关,所以我们得先来了解一下 trait 是什么,再看怎么用 trait 来处理这两种多态。

什么是 trait?

trait 是 Rust 中的接口,它定义了类型使用这个接口的行为。你可以类比到自己熟悉的语言中理解,trait 对于 Rust 而言,相当于 interface 之于 Java、protocol 之于 Swift、type class 之于 Haskell。
在开发复杂系统的时候,我们常常会强调接口和实现要分离。因为这是一种良好的设计习惯,它把调用者和实现者隔离开,双方只要按照接口开发,彼此就可以不受对方内部改动的影响。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Rust中的Trait是一种接口定义方式,类似于其他编程语言中的interface、protocol或type class。通过trait,可以将数据结构中的行为抽取出来,在多个类型之间共享,也可以作为泛型编程中的约束。文章介绍了trait的基本定义和实现方式,以及如何使用trait来处理特设多态和子类型多态。通过示例代码展示了如何定义和实现trait,以及trait内部方法的调用方式。文章还介绍了如何为Parse trait添加关联类型Error,使得在出错时返回合理的错误。总体来说,文章深入浅出地介绍了Rust中的trait,对于想要了解Rust类型系统和多态特性的读者具有很好的参考价值。 文章主要介绍了Rust中的Trait的定义和实现方式,以及如何使用Trait来处理特设多态和子类型多态。通过示例代码展示了Trait的基本用法,以及如何为Trait添加关联类型。此外,文章还介绍了Trait的“继承”特性,以及泛型Trait的应用场景。通过深入浅出的讲解,读者可以快速了解Rust中Trait的特点和应用,对于想要深入了解Rust类型系统和多态特性的读者具有很好的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(31)

  • 最新
  • 精选
  • GengTeng
    1. 不可以。关联类型无法就只能impl一次了,我们需要为Complex实现多个Add<Rhs>。 2. 不能。返回类型中的 Self 需要是Sized,而 dyn Write 不是Sized。 3. #[derive(Debug, Copy, Clone)] 4. impl<'a> Iterator for SentenceIter<'a> { type Item = &'a str; fn next(&mut self) -> Option<Self::Item> { match self.s.find(self.delimiter) { None => None, Some(i) => { let s = &self.s[..i + self.delimiter.len_utf8()]; *self.s = &self.s[i + self.delimiter.len_utf8()..]; if let Some((start, _)) = s.as_bytes().iter().enumerate().find(|(_, b)| **b != b' ') { Some(&s[start..]) } else { None } } } } } 这个SentenceIter的功能定义不明确,分割出来的每个sentence如果都需要包括delimiter的话,那剩余部分没有delimiter的情况该返回None吗?或者返回一个不带delimiter的剩余部分?都很别扭。

    作者回复: 非常棒! 对于第 4 题,如果没有 delimiter 的,要返回剩余部分。可以看看我的参考答案: ```rust struct SentenceIter<'a> { s: &'a mut &'a str, delimiter: char, } impl<'a> SentenceIter<'a> { pub fn new(s: &'a mut &'a str, delimiter: char) -> Self { Self { s, delimiter } } } impl<'a> Iterator for SentenceIter<'a> { type Item = &'a str; fn next(&mut self) -> Option<Self::Item> { // 如果内部的字符串引用指向空,则提前结束 if self.s.is_empty() { return None; } match self.s.find(self.delimiter) { Some(pos) => { // 注意对于 utf8 char,取它的长度需要用 c.len_utf8() let len = self.delimiter.len_utf8(); let s = &self.s[..pos + len]; let suffix = &self.s[pos + len..]; // 更改内部字符串引用,指向剩余部分 *self.s = suffix; Some(s.trim()) } None => { // 没有找到句号时,有可能后续还有最后一句内容 let s = (*self.s).trim(); *self.s = ""; if s.len() == 0 { None } else { Some(s) } } } } } #[test] fn it_works() { let mut s = "This is the 1st sentence. This is the 2nd sentence."; let mut iter = SentenceIter::new(&mut s, '.'); assert_eq!(iter.next(), Some("This is the 1st sentence.")); assert_eq!(iter.next(), Some("This is the 2nd sentence.")); assert_eq!(iter.next(), None); } fn main() { let mut s = "a。 b。 c"; let sentences: Vec<_> = SentenceIter::new(&mut s, '。').collect(); println!("sentences: {:?}", sentences); } ``` playground:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4590211f7542553e7feef960e814ab0b

    2021-09-22
    5
    11
  • lisiur
    1. 不应该这么做。如果这么做的话,同一个类型对同一个 trait 只能有一个实现, Rhs 也之能有一种可能,这样就不能既实现 String + String 又实现 String + &str,没有扩展性。 2. 不能编译通过,因为 by_ref 返回值含有 Self,不能作为 trait object 的方法使用。 3. 对 Complex 实现 Copy 和 Clone ``` #[derive(Debug, Copy, Clone)] struct Complex { real: f64, imagine: f64, } ``` 4. ```rust impl<'a> Iterator for SentenceIter<'a> { type Item = &'a str; fn next(&mut self) -> Option<Self::Item> { self.s.find(self.delimiter).map(|index| { let next = &self.s[..index + self.delimiter.len_utf8()]; *self.s = &self.s[index + self.delimiter.len_utf8()..]; next.trim_start() }) } } ``` 有个小小的疑问想请教下老师,在 **Trait Object 的实现机理** 这一小节开始给的配图中, formatter 这个 trait object 的 ptr 为啥会指向参数 text 呢?不是指向 HtmlFormatter 这个实例数据吗?

    作者回复: 非常棒!1,2,3 答案正确!4 需要考虑最后的部分。对 main() 里的实例代码,这个只返回["a。", "b。"]。你可以看我之前在其他回答下的回复。 谢谢指正,对,trait object 那个图是有问题,ptr 应该指向 HtmlFormatter。有时候图 copy & paste 时,就回忘了把不用的部分删除并修改,lol。我回头让编辑更新!

    2021-09-22
    6
  • Marvichov
    ``` // 指向同一种类型的同一个 trait 的 vtable 地址相同 // 这里都是 String + Display assert_eq!(vtable1, vtable3); // 这里都是 String + Debug assert_eq!(vtable2, vtable4); ``` 我原本以为String有个method call table, Display也有一个单独的; 那么说, 我自己写了个trait给String type, 岂不是编译的时候, 我自己提供的method需要append到String之前已经有的vtable? 1. vtable和method call table有啥区别呢? 2. 那么call `fn fmt(&self, f: &mut Formatter<'_>) -> Result;`的时候, 编译器是知道这个call在各个不同的实现中vtable的offset? 而且, 这些offset都要一样? 因为我记得, 编译的时候, 因为类型被erase了, 只能通过addr + offset (ptr, *args) 来call metho 例如, vtable1 + offset 和 vtable3 + offset 的地址都是call table中 fmt method 对应地址?

    作者回复: 不是 append 的关系。每一个 trait 和某个数据结构对该 trait 的实现都会生成一个静态的 vtable。vtable 是彼此独立的。 1. method call 不需要 table,通过类型就可以得到类型固定的 method call 的地址(编译期就可以完成了)。vtable 是一个运行期的概念。在 trait object 生成的时候,才指向具体的 vtable。 2. 你可以再仔细看看关于 vtable 的那几个图。并不是通过 addr + offset 来调 trait 的 method 的。是通过 vtable。比如一个 trait object 指向了 String Display 的 vtable,那么重这个 vtable 里就能找到 fmt 方法的地址。

    2021-09-29
    3
    3
  • QY
    请问第四题为什么要用&mut &s 呢? 是性能更好吗? 感觉&s使用起来自由度更高。宣言s时不需要mut s。

    作者回复: 因为 strtok 会修改传入的参数,使其指向 &str 的 tokenize 后面的位置,所以需要 &mut &str。你可以仔细看我画的图去理解。

    2021-09-26
    3
    2
  • 夏洛克Moriaty
    今天内容很多,也很细,看来需要常来温故才行

    作者回复: 👍

    2021-09-22
    2
  • 王鹏飞
    内容言简意赅, 只讲核心, 含金量十足; 但是需要自己补充相对基础的东西,才能理解

    编辑回复: 恩是的,最好对编程语言相关基础概念有理解,这也是为什么“适合人群”中老师会标注掌握任意一门编程语言。另外,Rust基础语法,老师的想法是官方资料就很齐全了,大家可以配合学习

    2022-12-02归属地:北京
    1
  • 核桃
    老师,这里关于静态分发和动态分发,有个例子没搞懂 #[drive(Debug)] struct Foo; trait Bar{ fn baz(&self); } impl Bar for Foo{ fn baz(&self) { println!("{:?}",self)}; } fn static_dispatch<T>(t: &T) where T:Bar{ t.baz(); } //为什么这里是动态分发就无法确定类型大小 //这里t也是限制为&Bar对象呀. fn dynamic_dispatch(t: &Bar){ t.baz(); } 这里不能理解的是动态分发,参数t也是要求为&Bar的呀,那么这里到底和静态分发的核心区分是什么?凭什么就无法确定类型大小而需要用虚表呢?这个实在不太理解,多谢了。

    作者回复: 首先这里应该是 &dyn Bar,其次,Bar 是一个 trait,它的类型是不确定的,而 &T 是一个确定的类型,在编译时会根据使用编译成 Foo 或者其他类型。这是本质的区别。对于 trait,只能通过虚表来获得原始类型的能力和一些属性。

    2021-10-28
    2
    1
  • Marvichov
    > C++ / Java 指向 vtable 的指针,在编译时放在类结构里, 现在C++的vtable也是分开放的 (可能看编译器, clang是分开的); 不过对 cpp, 对interface的实现大家都和original class definition放在一起. 没有试过在分开的文件中impl一个cpp的class的interface (parent inheritance). 可能也行, 但是不符合cpp的convention. rust相对而言, 对某个类型的impl可以到处放. ``` vtable for Foo: .quad 0 .quad typeinfo for Foo // RTTI for foo .quad Foo::method1() // where vtable starts .quad Bar::method2() // for method2 in vtable .quad Foo::~Foo() [complete object destructor] .quad Foo::~Foo() [deleting destructor] ``` https://guihao-liang.github.io/2020/05/30/what-is-vtable-in-cpp

    作者回复: 对,我是说指向 vtable 的指针是放在类结构里的。文中最后 summary 的图里也是这个意思吧?

    2021-10-07
    1
  • hughieyu
    1. 不可以。 Complex不能实现Add多次,只能实现对一种类型的计算。 2. 不能。 对于trait object来说, Self信息已经被擦除了。 3. #[derive(Debug, Copy, Clone)] 或者 用自定义类型包装Rc实现Add 如RcComplex(Rc<Complex>) 4. impl<'a> Iterator for SentenceIter<'a> { type Item = &'a str; // 想想 Item 应该是什么类型? fn next(&mut self) -> Option<Self::Item> { match self.s.find(self.delimiter) { None => None, Some(idx) => { let slice = &self.s[..idx+self.delimiter.len_utf8()]; *self.s = &self.s[idx+self.delimiter.len_utf8()..]; match slice.as_bytes().iter().enumerate().find(|(_,b)|**b != b' '){ None => None, Some((start,_)) => Some(&slice[start..]), } } } } } 问题: trait泛型和关联类型的使用场景还是有些模糊

    作者回复: 1/2/3 正确。4 需要考虑 main() 中的场景,要处理不带 delimiter 的后续部分,可以看我之前的回答。 trait 泛型是对同一个数据结构你需要有多个不同的实现,trait 的关联类型是,在某个实现里,我需要设定和这个实现相关的类型。其实关联类型就和关联函数一样的。

    2021-09-22
    1
  • overheat
    陈天老师的这部分是目前看到的最生动的教程,the book对有些内部机制bi而不谈,其他书面教程感觉我自己的语文没学好。我现在是看了第20章再回头看的,终于明白了trait object这个之前差不多直接跳过的概念。

    作者回复: 谢谢!:)

    2021-12-13
收起评论
显示
设置
留言
31
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部