0x00 开篇
前面用两篇文章介绍了泛型,第二课也算是对结构体的一个补充了。结构体的知识尤为重要,今天这篇文章依然是围绕结构体来做介绍。相信有其它面向对象编程语言基础的小伙伴都了解类和接口的概念。但是 Rust 没有类和接口,那么它又是如何实现面向对象特征的呢?
0x01 trait定义
trait 是 Rust 中唯一的接口抽象方式,类似于 Java 和 C# 的 interface。trait 可以定义多个抽象的方法(行为)来用于多个结构体共享,使得不同的结构体可以拥有相同的方法(行为)。在某些博客或者书籍上会将 trait 翻译为 特型/特性/特征。在后面的文章中,我不会将其翻译成中文。
0x02 trait的实现
trait 是一种任何类型都可以选择支持或者不支持的特性,通常代表某一种行为或者能力。
示例:所有的动物基本上都会”说话“或者有一些其它类似的行为,但是它们的叫声又有区别,我们可以为把他们抽象出一个 trait 叫 Animal。
示例代码如下:
/// 动物 trait trait Animal { // 动物的叫声 fn make_sound(&self); }
上面的代码就是定义了一个动物的 trait,定义 trait 很简单,我们只需要给它命名并且列出 trait 方法的类型签名即可。那我们如何使用这个 trait 呢?实现这个 trait 的语法如下:
impl [TraitName] for [Type]
在 Rust 中,我们可以使用泛型为任何类型甚至是 str , i32, bool 等内置类型添加扩展方法。这里声明的 Animal 属于动物,我再定义几个动物类型的结构体。通常 trait 都是与 struct 一起使用。完整代码如下:
/// 动物 trait trait Animal { // 动物的叫声 fn make_sound(&self); } /// 狗 struct Dog { name: String, } /// 鱼 struct Cat { name: String, } /// 为 Dog 类型实现 Animal trait impl Animal for Dog { // 打印狗的叫声 fn make_sound(&self) { println!("汪汪~"); } } /// 为 Cat 类型实现 Animal trait impl Animal for Cat { // 打印猫的叫声 fn make_sound(&self) { println!("喵喵~"); } } fn main() { // 创建dog let dog = Dog { name: String::from("二哈") }; // 创建cat let cat = Cat { name: String::from("美短") }; // 运行方法 dog.make_sound(); cat.make_sound(); } // 运行结果: // 汪汪~ // 喵喵~
上面代码注释都很明确,我就不多做解释了。使用 trait 的方法时候有两个注意事项:
- 定义 trait 方法的时候不要忘记第一个参数必须是 &self。有没有 &self 的区别,上一篇文章我已经解释了。
- 使用 trait 方法的时候,trait 本身必须在当前作用域中,否则 trait 所有的方法都是隐藏的。这一点,CLion已经给了我们很好的提示。(有关示例代码我将在源码中给出,由于还没有讲到模块化,所以这里暂不多解释)
0x03 impl trait
trait 作为参数时一般使用 impl trait 的语法来表示参数类型。先上代码:
/// impl trait 作为参数 fn speak(animal: impl Animal) { animal.make_sound() } /// 为已有类型实现 trait (示例) impl Animal for i32 { fn make_sound(&self) { println!("i32"); } } fn main() { speak(dog); speak(cat); let a = 5; speak(a); // 运行结果: // 汪汪~ // 喵喵~ // i32 }
speak 函数表示可以接收实现了 Animal trait 的任何实例,包括已经存在的类型,上面代码以 i32 为例。如果你想让 speak 函数接收任意类型,那你可以让那个类型实现 Animal trait。
问题又来了,如果我想在某一个函数里接收同时实现了多个 trait 的参数应该如何操作呢?继续往下看:
/// 测试多trait trait Test { fn test(&self); } /// 为Dog实现Test impl Test for Dog { fn test(&self) { println!("这是一个Test方法"); } } /// 打印同时实现 Test 和 Animal Trait的方法 fn printMulti(p: impl Test + Animal) { p.make_sound(); p.test(); } fn main() { // 多 trait同时实现 printMulti(dog); // 下面的代码错误 // printMulti(cat); // 运行结果: // 汪汪~ // 这是一个Test方法 }
上面的代码又定义了一个 Test 的 trait,我们也为 Dog 实现了这个 Test。最后定义了一个 printMulti 函数打印内容。我们不能为这个函数传参 cat,因为它没有实现 Test。运行后,编译器会抛出下面的错误。
0x04 小结
本篇文章简单介绍了 trait 的概念和基础用法。如果你有 Java、 C# 等语言基础,那么这篇文章你将很容易理解。如果你有 C、C# 语言基础,你会发现 trait 的方法很像虚函数。有没有感觉 Rust 有将其它语言结合的特点呢。由于本篇文章所涉及的代码比较多,所以就先介绍这些吧。下一篇文章继续来介绍 trait 的其它内容。