Rust语言入门教程(十) - Trait与泛型

什么是Trait

在Rust中,没有类继承,或者说没有结构体的继承。但是,大多数的面向对象的语言都是有继承特性的,那Rust还算是一门面向对象的语言吗?实际上没有一个普适的定义来判断一门语言究竟是不是面向对象的语言。就像争论php究竟是不是最好的语言一样,这近乎是个宗教争论,但Rust社区其实并不关心这一问题,因此究竟是不是面向对象,其实并不重要。

重要的是, Rust为什么没有结构体继承?因为Rust有一种比继承更好的方法来解决我们希望继承解决的问题: Trait。

Trait(特质)类似于Java或者Golang语言中的Interface(接口), Rust采用了Trait组合的方案,而不是继承。

Trait语法格式

还是以上一章中的RedFox结构体为例:

struct RedFox {
	enemy: bool,
	life: u32,
}

trait Noisy {
	fn get_noise(&self) - &str;
}

上面的代码中,我们创建了一个名为Noisy的Trait, 其中定义了该Trait所需的行为(函数/方法)。 换句话说,如果一个结构体想要拥有这个Trait, 它就必须实现这个Trait中定义的所有的行为(函数/方法)。

Noisy这个Trait告诉我们, 一个结构体想要拥有它的话,就不需要有一个名为get_noise,返回类型为&str的结构体方法,下面我们来看看怎么为结构体添加对Noisy这个Trait的实现:

struct RedFox {
	enemy: bool,
	life: u32,
}

trait Noisy {
	fn get_noise(&self) - &str;
}

impl Noisy for RedFox {
	fn get_noise(&self) -> &str { "Meow" }
}

由上面的代码,我们可以看到实现Trait的书写格式: impl for <结构体名称>

泛型函数

观察上面的代码,看起来好像很复杂,如果我们直接把get_noise作为结构体方法,不是很方便吗:

struct RedFox {
	enemy: bool,
	life: u32,
}

impl RedFox {
	fn get_noise(&self) -> &str { "Meow" }
}

这是因为如果我们使用Trait的话,我们就可以开始编写泛型函数了:

fn print_noise<T: Noisy>(item: T) {
	println!("{}", item.get_noise());
}

例如上面这个函数, 他的参数类型是T, 这是一个泛型,在<>内限定了这个类型T可以是任意一个实现了Noisy这个Trait的类型的值。 在该函数中, 可以使用item参数的任何行为。如上,我们就有了一个泛型函数,他可以接受任意类型的参数,只要这个类型实现了Noisy这个Trait。

为内置类型实现自定义Trait

根据上文的内容我们可以发现,在Rust中, Struct和Trait之间可以随意进行关联,我们甚至可以为Rust内置的类型实现自定义的Trait,例如:

fn print_noise<T: Noisy>(item: T) {
	println!("{}", item.get_noise());
}

impl Noisy for u8 {
	fn get_noise(&self) -> &str {"BYTE!"}
}

fn main() {
	print_noise(5_u8); // 输出 “BYTE!”
}

如上,我们为内置类型u8实现我们自定义的Trait, 在前面的章节中我们已经提到了, 在Rust中, Byteu8是等价的, 因此对u8类型实现get_noise这个方法,我们让他直接打印出“BYTE!”。

main函数中,我们调用print_noise(5_u8), 由于参数类型是u8, 因此会调用到Noisyu8类型的实现,直接打印出"BYTE!"。

特殊的Trait: Copy

在Rust中,有一个原生的Trait叫做Copy, 如果某个类型实现了Copy这个Trait, 那么它的值会被复制,而不会转移其所有权。这个Trait适用于一些占用内存较小,适合在栈内储存的数据类型,比如整型,浮点型,布尔型等基本数据类型。

如果一个类型使用了堆中的内存,那它就不适合实现Copy, Rust编译器也会禁止为这样的类型实现Copy。如果某个自定义类型中仅使用了其他一些实现了Copy的类型,那可以选择为这个自定义类型实现Copy这个Trait。

Trait用例举例

假设我们要设计一个游戏,这个游戏中包含三种动物:

  • 飞马

其中,鹅和飞马会飞, 马和飞马会跑, 鹅和马会爆炸, 如果想用继承来处理这三种动物的技能的话,就会发现非常的棘手了,他们之间有技能重合,但是又不存在完全的包含关系。但用Trait来实现,其实是一件很简单的事情。每种能力对应一个Trait:

  • 爆炸

因此,代码大概会是这样:

struct Goose {...}
struct Horse {...}
struct FlyHorse {...}

Trait Fly {
	fn fly(&self) {...}
}
Trait Run {
	fn run(&self) {...}
}
Trait Explode {
	fn explode(&self) {...}
}

// 为鹅实现飞和爆炸的特质
impl Fly for Goose {
	fn fly(&self) {...}
}
impl Explode for Goose {
	fn explode(&self) {...}
}

// 为飞马实现飞和跑的特质
impl Fly for FlyHorse {
	fn fly(&self) {...}
}
impl Run for FlyHorse {
	fn run(&self) {...}
}

// 为马实现跑和爆炸的特质
impl Run for Horse {
	fn run(&self) {...}
}
impl Explode for Horse {
	fn explode(&self) {...}
}

Trait的继承

Rust中没有结构体的继承,但是却有Trait的继承, 因此一个Trait可以从另一个Trait继承。
Rust语言入门教程(十) - Trait与泛型_第1张图片
上图展示了一个可能得Trait继承结构,Movement是一个基本的Trait, 其中可能定义了一些关于移动的基本函数,比如左移,右移等, Run从Movement继承而来,也拥有Movement中定义的所有函数,另外也有一些跟跑相关的函数,例如加速减速等, 同理, Ride和Fly从Run继承而来,拥有上级Trait中的所有函数,另外还拥有一些与自身相关的函数。
Rust语言入门教程(十) - Trait与泛型_第2张图片
如上,Horse这个结构体最终需要实现的是Ride和Explode这两个Trait, 那么Horse结构体也需要对其父Trait进行实现,听起来是不是有些麻烦?其实Trait中的函数是可以有默认行为的,因此只要设计得当, 多数情况下我们并不需要对父Trait中规定的函数进行实现。

定义Trait的默认行为

如果要直接在创建Trait时定义其中的一些默认行为,那么方法与之前会有一些不同,除了声明函数的名称,参数和返回类型外,还需要对其中的函数进行默认行为的定义:

trait Noisy {
	// 无默认行为的函数
	fn get_noise(&self) - &str;
}

trait Run {
	// 定义函数的默认行为
	fn run(&self) {
		println!("I'm running");
	}
}

如果你的结构体想使用某个Trait的默认行为,那么在实现这个Trait的时候,就不要再为这个函数重新定义行为:

trait Run {
	// 定义函数的默认行为
	fn run(&self) {
		println!("I'm running");
	}
}

struct Robot {}
impl Run for Robot {} // 此处不要重新定义run函数,就可以使用Run这个Trait中对run函数的默认定义

fn main() {
	let robot = Robot {};
	robot.run(); // 输出 “I'm running”
}

如果在实现Trait的时候,重新定义了其中具有默认行为的函数,那么默认的行为将被覆盖, 例如:

trait Run {
	// 定义函数的默认行为
	fn run(&self) {
		println!("I'm running");
	}
}

struct Robot {}
impl Run for Robot {
	fn run(&self) {
		println!("Robot is runniung");
	} // 此处定义run的函数覆盖了Trait中为run函数定义的默认行为
} 

fn main() {
	let robot = Robot {};
	robot.run(); // 输出 "Robot is running"
}

Trait没有字段

在Trait中只能定义函数,不能定义字段,只能通过自定义setter 和 getter函数来间接实现这一点, 这样做的好处是你可以为不同的类型提供统一的接口来获取和修改属性。下面是一个如何在 Rust 中定义包含 getter 和 setter 方法的 trait 的示例:

trait MyProperties {
    // Getter 方法返回一个引用
    fn get_property(&self) -> &String;

    // Setter 方法接收一个新的值
    fn set_property(&mut self, value: String);
}

struct MyStruct {
    property: String,
}

impl MyProperties for MyStruct {
    fn get_property(&self) -> &String {
        &self.property
    }

    fn set_property(&mut self, value: String) {
        self.property = value;
    }
}

fn main() {
    let mut my_struct = MyStruct {
        property: "Initial Value".to_string(),
    };

    // 使用 getter 获取值
    println!("Property before: {}", my_struct.get_property());

    // 使用 setter 修改值
    my_struct.set_property("New Value".to_string());

    // 再次使用 getter 获取新的值
    println!("Property after: {}", my_struct.get_property());
}

小结

本章介绍了Rust中强大的Trait,可以帮助我们实现泛型函数,和灵活的接口功能。下一章将介绍Rust中的一系列集合类型(Collections)。

你可能感兴趣的:(Rust教程,rust,开发语言,后端)