写点什么

30 天拿下 Rust 之 Trait

作者:希望睿智
  • 2024-05-29
    安徽
  • 本文字数:2848 字

    阅读完需:约 9 分钟

30天拿下Rust之Trait

💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。

概述

在 Rust 中,Trait 是一个核心概念,它允许我们定义类型应该具有的行为。Trait 类似于其他语言中的接口,但 Rust 的 Trait 更为强大和灵活。它不仅定义了一组方法,还允许我们指定方法的默认实现、泛型约束和继承。通过 Trait,我们可以定义一组方法的签名和关联类型,使得不同的类型能够共享相同的行为接口,进而支持多态性。

定义 Trait

在 Rust 中,Trait(特征)用于定义一组方法签名,这些方法可以由任何实现了该 Trait 的类型来提供具体的实现。Trait 提供了一种抽象机制,允许我们编写与具体类型无关的通用代码。

在 Rust 中定义 Trait 的基本步骤如下。

1、声明 Trait:使用 trait 关键字来声明一个新的 Trait。

2、定义方法:在 Trait 体内,列出所有该 Trait 类型必须实现的方法,包括:方法名、参数列表和返回类型。

3、可选的默认实现:可以为 Trait 中的方法提供默认实现,这样实现该 Trait 的类型可以选择是否覆盖这些默认实现。

在下面的示例代码中,我们定义了一个名为 Shape 的 Trait,它有两个方法:area()和 perimeter()。area()方法没有默认实现,这意味着,任何实现 Shape Trait 的类型都必须提供这个方法的具体实现。perimeter()方法有一个默认实现,返回值为 0.0。实现这个 Trait 的类型可以选择提供自己的实现来覆盖这个默认值,当然,也可以不覆盖。

trait Shape {    // 定义一个没有默认实现的方法    fn area(&self) -> f64;
// 定义一个带有默认实现的方法 fn perimeter(&self) -> f64 { // 这里是默认实现,但可以被实现该Trait的类型覆盖 0.0 }}
复制代码

实现 Trait

一旦我们定义了某个 Trait,就可以为具体的类型实现它。这通常通过 impl 关键字来完成,后面跟着 Trait 名称和类型名称。

在下面的示例代码中,我们定义了一个 Circle 结构体,并为它实现了 Shape Trait。我们提供了 area()和 perimeter()方法的具体实现,其中,perimeter()方法覆盖了 Shape Trait 中定义的默认实现。现在,任何接受 Shape Trait 作为参数或返回值的函数都可以使用 Circle 类型的实例,因为 Circle 实现了 Shape Trait。正是这种灵活性,使得 Trait 成为 Rust 中实现代码复用和抽象的重要工具。

impl Shape for Circle {    // 提供area方法的具体实现    fn area(&self) -> f64 {        std::f64::consts::PI * self.radius * self.radius    }
// 覆盖Shape Trait中perimeter方法的默认实现 fn perimeter(&self) -> f64 { 2.0 * std::f64::consts::PI * self.radius }}
复制代码

在 Rust 中,一个类型还可以实现多个 Trait。

trait Fly {    fn fly(&self);}
trait Swim { fn swim(&self);}
struct Duck { name: String,}
impl Fly for Duck { fn fly(&self) { println!("{} is flying", self.name); }}
impl Swim for Duck { fn swim(&self) { println!("{} is swimming", self.name); }}
fn main() { let duck = Duck { name: "Donald".to_string() }; duck.fly(); duck.swim();}
复制代码

在上面的示例代码中,Duck 结构体实现了 Fly 和 Swim 这两个 Trait,因此它既可以飞,也可以游泳。这允许我们在使用 Duck 实例时,根据需要调用相应的接口方法。

泛型约束

泛型函数和泛型结构体通常需要对其类型参数施加一些约束,以确保它们支持某些操作。此时,我们可以使用 Trait 作为泛型约束。

在下面的示例代码中,我们首先定义了一个名为 Displayable 的 Trait。然后,我们为 Fruit 结构体实现了 Displayable Trait,并编写了 display()方法。接下来,我们编写了一个泛型函数 print_all,它接受一个实现了 Displayable Trait 的类型的切片。最后,我们调用 print_all()方法,输出了所有水果的信息。

trait Displayable {    fn display(&self);}
struct Fruit { name: String,}
impl Displayable for Fruit { fn display(&self) { println!("Fruit is {}", self.name); }}
fn print_all<T: Displayable>(items: &[T]) { for item in items { item.display(); }}
fn main() { let fruits = [ Fruit { name: String::from("Lemon") }, Fruit { name: String::from("Apple") }, Fruit { name: String::from("Peach") }, ]; print_all(&fruits);}
复制代码

另外,如果一个函数接受一个参数,并且要求这个参数必须同时满足多个 Trait,可以用+符号来表示。对于一些复杂的实现关系,我们可以使用 where 关键字简化。

fn do_both_actions<T: Fly + Swim>(animal: T) {    animal.fly();    animal.swim();}
fn do_both_actions2<T>(animal: T)where T: Fly + Swim{ animal.fly(); animal.swim();}
fn main() { let duck = Duck { name: "Donald".to_string() }; do_both_actions(duck); do_both_actions2(duck);}
复制代码

Trait 对象

要使用 Trait 对象,我们需要先定义 Trait。

在下面的示例代码中,我们首先定义了一个名为 Animal 的 Trait。然后,我们为 Dog 结构体和 Cat 结构体实现了 Animal Trait,并编写了 speak()方法。

trait Animal {    fn speak(&self);}
struct Dog;impl Animal for Dog { fn speak(&self) { println!("Dog"); }}
struct Cat;impl Animal for Cat { fn speak(&self) { println!("Cat"); }}
复制代码

到这里,我们可以创建 Trait 对象了。在 Rust 中,Trait 对象是通过使用 &dyn Trait 语法来表示的,其中 Trait 是一个 trait 的名字。这种表示法允许我们在运行时进行动态分派,即可以在不知道具体类型的情况下调用 trait 中定义的方法。为了创建一个 Trait 对象,可以将实现了该 trait 的具体类型的引用转换为 &dyn Trait。

在下面的示例代码中,我们声明了 Dog 和 Cat 的实例,分别为 dog 和 cat。接着,我们将 &dog 和 &cat 赋值给 Trait 对象 dog_ref 和 cat_ref。由于 dog_ref 和 cat_ref 现在都是 Animal Trait 对象,故可以安全地调用它们的 speak 方法,而无需知道它们实际是 Dog 还是 Cat。

fn animal_speak(animal: &dyn Animal) {    animal.speak();}
fn main() { let dog = Dog; let cat = Cat; // 创建Animal Trait对象 let dog_ref: &dyn Animal = &dog; let cat_ref: &dyn Animal = &cat;
// 输出:Dog animal_speak(dog_ref); // 输出:Cat animal_speak(cat_ref);}
复制代码

另外,我们还可以将 Trait 对象作为集合的一部分进行存储,并遍历集合调用 Trait 对象的方法。

fn main() {    let dog = Dog;    let cat = Cat;        // 将Animal Trait对象存储到向量中    let animals: Vec<&dyn Animal> = vec![&dog, &cat];    for animal in animals {        animal.speak();    }}
复制代码

注意:使用 Trait 对象会带来一些运行时开销,因为需要在堆上分配一个额外的结构体来存储类型信息,并且调用方法时需要进行间接调用。因此,在性能敏感的场景中,应该谨慎使用 Trait 对象。

发布于: 刚刚阅读数: 3
用户头像

希望睿智

关注

一起学习,一起成长,一起进步! 2024-05-21 加入

中国科学技术大学毕业,在客户端、运营级平台、Web开发、嵌入式开发、深度学习、人工智能、音视频编解码、图像处理、流媒体等多个领域具备实战开发经验和技术积累,共发表发明专利十余项,软件著作权几十项。

评论

发布
暂无评论
30天拿下Rust之Trait_Trait_希望睿智_InfoQ写作社区