【Rust 中级教程】 03 trait (1)

259 阅读4分钟

0x00 开篇

前面用两篇文章介绍了泛型,第二课也算是对结构体的一个补充了。结构体的知识尤为重要,今天这篇文章依然是围绕结构体来做介绍。相信有其它面向对象编程语言基础的小伙伴都了解类和接口的概念。但是 Rust 没有类和接口,那么它又是如何实现面向对象特征的呢?

0x01 trait定义

trait 是 Rust 中唯一的接口抽象方式,类似于 Java 和 C# 的 interfacetrait 可以定义多个抽象的方法(行为)来用于多个结构体共享,使得不同的结构体可以拥有相同的方法(行为) 。在某些博客或者书籍上会将 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#  等语言基础,那么这篇文章你将很容易理解。如果你有 CC#  语言基础,你会发现 trait 的方法很像虚函数。有没有感觉 Rust 有将其它语言结合的特点呢。由于本篇文章所涉及的代码比较多,所以就先介绍这些吧。下一篇文章继续来介绍 trait 的其它内容。