Trait 抽象接口
Rust 强调将数据(结构体的字段)和行为(trait 定义的方法)分离。结构体负责存储数据,而 trait 负责定义操作这些数据的接口。这种设计使得代码更加清晰和模块化。
抽象接口是Trait的最基础的用法,它的特点如下:
- 使用
trait关键字定义接口 - 接口中可以定义方法,并支持默认实现
- 使用
impl关键字为类型实现接口方法 - 接口中不能实现另一个接口,但接口之间可以继承
- 同一个接口可以被多个类型实现,但不能被一个类型实现多次
1.定义与实现
Trait定义了一组可以被共享的行为,只要实现了Trait,该类型就能使用这组行为。
1.1 定义Trait
定义trait基本语法如下,trait的名称应使用大驼峰
trait TraitName {
fn func_1(args...) ->return_type ; // 关联函数
fn func_2(&self, args..) -> return_type ; // 方法
}使用trait关键字,可以定义一个Trait。在它的名字后面,使用{}定义这个Trait所具有的方法。
在定义时,往往只写出函数的签名,并且允许使用Self作为第一个参数,函数体用;代替,表示还未实现。
比如说我们可以定义一个植物接口,其中有一个攻击的方法
trait Plant {
fn attack(&self);
}1.2 实现Trait
impl TraitName for type {
fn func_1(args...) -> return_type {
// ...
}
fn func_2(&self, args..) -> return_type {
// ...
}
}使用impl关键字,可以为指定类型实现指定Trait,例如impl Add for i32就是给i32类型实现Add。
在{}中,需要给每一个之前定义的方法做出实现,必须实现所有未提供默认实现的方法。
上面已经定义了一个植物的接口,那么下面我们创建两个结构体,一个是豌豆射手,一个是大喷菇,他们各自有个一伤害的属性,每个植物的伤害不一样
// 豌豆射手
struct Peashooter {
damage: i32,
}
// 大喷菇
struct PuffShroom {
damage: i32,
}然后我们分别为这两个植物实现Plant接口
impl Plant for Peashooter {
fn attack(&self) {
println!("豌豆射手攻击,给对方造成{}点伤害", self.damage);
}
}
impl Plant for PuffShroom {
fn attack(&self) {
println!("大喷菇攻击,给对方造成{}点伤害", self.damage);
}
}最后我们创建两个示例调用自身的方法
fn main() {
let peashooter = Peashooter { damage: 100 };
let puffshroom = PuffShroom { damage: 150 };
peashooter.attack(); // 豌豆射手攻击,给对方造成100点伤害
puffshroom.attack(); // 大喷菇攻击,给对方造成150点伤害
}1.3 实现多个trait
一个类型也可以实现多个trait
trait Attack {
fn attack(&self) {
println!("攻击");
}
}
trait Defence {
fn defence(&self) {
println!("防御");
}
}
struct SpinyNut {}
impl Attack for SpinyNut {}
impl Defence for SpinyNut {}
fn main() {
let spinynut = SpinyNut {};
spinynut.attack(); // 攻击
spinynut.defence(); // 防御
}2.默认实现
Trait 可以提供默认实现,这样实现者可以直接使用,也可以选择覆盖。
上面的豌豆和大喷菇我们都各自实现了attack方法,但是他们的内容大部分都是一样的,所以可以直接在trait Plant给出attack方法的默认实现
trait Plant {
fn attack(&self) {
println!("脆弱的植物打出了无用的一击");
}
}
// 豌豆射手
struct Peashooter {
damage: i32,
}
// 大喷菇
struct PuffShroom {
damage: i32,
}
impl Plant for Peashooter {}
impl Plant for PuffShroom {
fn attack(&self) {
println!("大喷菇攻击,给对方造成{}点伤害", self.damage);
}
}
fn main() {
let peashooter = Peashooter { damage: 100 };
let puffshroom = PuffShroom { damage: 150 };
peashooter.attack(); // 脆弱的植物打出了无用的一击
puffshroom.attack(); // 大喷菇攻击,给对方造成150点伤害
}上面代码中,并没有给豌豆射手实现attack方法,但是Peashooter依然实现了Plant接口,所以豌豆射手的示例也可以调用attack方法,并且是调用了默认实现
3.泛型 Trait
在Trait中,支持使用泛型,语法如下:
trait TraitName<T, ...> {
}
impl<T, ...> TraitName<T, ...> for type {
}只需要在名称后面添加泛型声明列表,后续在实现中就可以使用。
与基本的泛型语法相同,在impl时需要先impl<>声明泛型,后续才能使用。
依然是植物接口,这次attack函数接受一个伤害值,并把这个值返回出去
trait Plant<T> {
fn attack(&self, damage: T) -> T;
}豌豆射手和大喷菇就不添加damage属性了,因为damage要用函数传递了
// 豌豆射手
struct Peashooter {}
// 大喷菇
struct PuffShroom {}
impl<T> Plant<T> for Peashooter {
fn attack(&self, damage: T) -> T {
damage
}
}
impl<T> Plant<T> for PuffShroom {
fn attack(&self, damage: T) -> T {
damage
}
}最后使用
fn main() {
let peashooter = Peashooter {};
let puffshroom = PuffShroom {};
let damage_from_peashooter = peashooter.attack(100);
let damage_from_puffshroom = puffshroom.attack(150);
println!("{}", damage_from_peashooter); // 100
println!("{}", damage_from_puffshroom); // 150
}4.关联类型
在为一个类型实现某接口时,我们可以用type定义一个关联类型
关联类型顾名思义是一个类型,它允许我们在接口实现时,给这个类型赋值为具体的类型
语法如下:
- 定义关联类型需要使用type关键字
- 使用关联类型需要使用
Self:: - 在trait实现中,需要给关联类型赋值一个类型
trait TraitName {
type AttackType;
fn attack(&self, damage: Self::AttackType);
}
impl TraitName for S{
type AttackType = u32;
}依然是豌豆射手和大喷菇,有时候植物工具不一定是整数,也有可能是浮点数,那么这时候我们可以在接口实现时,给这个关联类型一个具体的类型
trait Plant {
type AttackType;
fn attack(&self, damage: Self::AttackType);
}
// 豌豆射手
struct Peashooter {}
// 大喷菇
struct PuffShroom {}
impl Plant for Peashooter {
type AttackType = i32;
fn attack(&self, damage: Self::AttackType) {
println!("豌豆射手攻击,对方收到{}点伤害", damage);
}
}
impl Plant for PuffShroom {
type AttackType = f32;
fn attack(&self, damage: Self::AttackType) {
println!("大喷菇攻击,对方收到{}点伤害", damage);
}
}
fn main() {
let peashooter = Peashooter {};
let puffshroom = PuffShroom {};
peashooter.attack(100); // 豌豆射手攻击,对方收到100点伤害
puffshroom.attack(125.5); // 大喷菇攻击,对方收到125.5点伤害
}5.关联常量
除了关联类型,Trait 还可以定义关联常量,此处的关联常量和之前在impl时讲的关联常量是一样的。
- 与关联类型同样的是,关联常量在定义
Trait阶段定义,在实现阶段确定值。 - 使用const定义常量名,并标注类型
- 访问关联常量依然需要使用
Self::
trait Plant {
type HpType;
const HP: Self::HpType;
fn get_hp(&self) -> Self::HpType;
}
// 豌豆射手
struct Peashooter {}
// 大喷菇
struct PuffShroom {}
impl Plant for Peashooter {
type HpType = i32;
const HP: Self::HpType = 100;
fn get_hp(&self) -> Self::HpType {
Self::HP
}
}
impl Plant for PuffShroom {
type HpType = f32;
const HP: Self::HpType = 120.5;
fn get_hp(&self) -> Self::HpType {
Self::HP
}
}
fn main() {
let peashooter = Peashooter {};
let puffshroom = PuffShroom {};
let peashooter_hp = peashooter.get_hp();
let puffshroom_hp = puffshroom.get_hp();
println!("{}", peashooter_hp); // 100
println!("{}", puffshroom_hp); // 120.5
}5.1 默认值
关联常量可以给一个默认值,并且已经是一个稳定特性了,可以直接使用。
trait Plant {
const H: u32 = 100;
}6.Trait 继承
Rust不支持面向对象中的类型继承,但是在Trait之间允许继承。
6.1 基本继承
如果某个类型要实现该Trait,必须先实现这个Trait的所有父Trait。
trait继承语法:
trait ChildTrait: FatherTrait {}定义Trait时,在名称后面使用:指明要继承的其它Trait
trait Shape {
fn area(&self) -> f64;
}
// `Drawable` 继承自 `Shape`。一个类型要想能被绘制,它必须先是一个能计算面积的形状。
trait Drawable: Shape {
fn draw(&self);
}
struct Circle { radius: f64 }
// 必须首先实现父 Trait `Shape`
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
// 然后才能实现子 Trait `Drawable`
impl Drawable for Circle {
fn draw(&self) { println!("Drawing a circle with radius: {}", self.radius); }
}
fn main() {
let c = Circle { radius: 5.0 };
let area = c.area();
println!("{}", area); // 78.53981633974483
c.draw(); // Drawing a circle with radius: 5
}6.2 多继承
一个trait可以继承多个trait
如下示例,有三个刀类的trait,鬼彻、和刀一文字、阎魔,这里清奇的使用了中文变量名
然后有个三刀流trait继承了以上三个trait
定义一个剑客结构体,分别实现以上三个trait,还有最终的三刀流trait
然后我们定义一个索隆实例,调用这些方法
trait 鬼彻 {
fn luck(&self);
}
trait 和刀一文字 {
fn girlfriend(&self);
}
trait 阎魔 {
fn black(&self);
}
trait 三刀流: 鬼彻 + 和刀一文字 + 阎魔 {
fn ultimate(&self);
}struct 剑客 {
name: String,
}
impl 鬼彻 for 剑客 {
fn luck(&self) {
println!("拿我的运气和这把刀碰一碰")
}
}
impl 和刀一文字 for 剑客 {
fn girlfriend(&self) {
println!("女朋友送的刀得放嘴里")
}
}
impl 阎魔 for 剑客 {
fn black(&self) {
println!("练成黑刀肯定不错")
}
}
impl 三刀流 for 剑客 {
fn ultimate(&self) {
println!("三刀流奥义,阿修罗")
}
}fn main() {
let 索隆 = 剑客 {
name: String::from("索隆"),
};
索隆.luck();
索隆.girlfriend();
索隆.black();
索隆.ultimate();
}一个继承了别的trait的trait,也可以被继承
比如,下面示例,分别定义了12345档,每个档都继承前面的档
trait GearFirst {
fn rubber_pistol(&self) {
println!("橡胶手枪")
}
}
trait GearSecond: GearFirst {
fn jet_pistol(&self) {
println!("喷气手枪")
}
}
trait GearThird: GearSecond {
fn giant_pistol(&self) {
println!("巨人手枪")
}
}
trait GearFourth: GearThird {
fn kong_gun(&self) {
println!("猿王枪")
}
}
trait GearFifth: GearFourth {
fn drums_of_liberation(&self) {
println!("解放之鼓")
}
}struct Lufy {
name: String,
}
impl GearFirst for Lufy {
fn rubber_pistol(&self) {
println!("橡胶手枪")
}
}
impl GearSecond for Lufy {
fn jet_pistol(&self) {
println!("喷气手枪")
}
}
impl GearThird for Lufy {
fn giant_pistol(&self) {
println!("巨人手枪")
}
impl GearFourth for Lufy {
fn kong_gun(&self) {
println!("猿王枪")
}
}
impl GearFifth for Lufy {
fn drums_of_liberation(&self) {
println!("解放之鼓")
}
}fn main() {
let lufy = Lufy {
name: "王路飞".to_string(),
};
lufy.rubber_pistol();
lufy.jet_pistol();
lufy.giant_pistol();
lufy.kong_gun();
lufy.drums_of_liberation();
}7.完全限定语法
一个类型的多个Trait是可以分别自由实现方法的,那么也就允许多个Trait中出现同名方法。
而当多个 Trait 或者类型本身impl的方法名发生冲突时,编译器就会陷入困境。此时就需要 完全限定语法 (Fully Qualified Syntax)来避免歧义。
Rust 的方法调用有一个推导的过程:
- 编译器会先在当前类型的固有方法里查找,即
impl的内容 - 如果没找到,再去该类型实现的所有
Trait中查找 - 如果存在多个候选,就会报错,提示你需要显式指定
完全限定语法如下:
<Type as TraitName>::method(args...)Type 表示具体的类型,TraitName 表示方法来源的Trait,method 是要调用的方法名,args... 是额外的参数。
假设我们有两个 Trait,它们都定义了一个同名方法:
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Human {
fn fly(&self) {
println!("Human flapping arms... not very effective.");
}
}
impl Pilot for Human {
fn fly(&self) {
println!("Pilot flying the plane!");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Wizard flying with magic!");
}
}此时 Human 类型同时具备三种 fly 方法:
- 自身固有方法
Human::fly - 来自
Pilot的fly - 来自
Wizard的fly
如果直接调用:
let h = Human;
h.fly();编译器会优先选择固有方法,因此输出:
Human flapping arms... not very effective.但如果我们想调用 Pilot 或 Wizard 的版本,就必须使用完全限定语法:
Pilot::fly(&h); // 等价于 <Human as Pilot>::fly(&h)
Wizard::fly(&h); // 等价于 <Human as Wizard>::fly(&h)
<Human as Pilot>::fly(&h); // 更显式的写法此处由于第一个参数是self,所以要传入&h作为参数。通过完全限定语法就能明确告诉编译器要调用的是哪个 Trait 的实现。
8.孤儿规则
基于impl和trait两个特性,你可以很轻易的给一个类型添加各种方法,这就有可能导致一些不太安全的操作。
比如你的某位同事,已经为一个类型封装好了它的各类接口和Trait。但是你使用这个类型前,又对它的这个Trait进行了实现,导致篡改了某些该类型原本的行为,这就是一种破坏性的改写,可能导致难以预料的Bug,孤儿规则可以避免类似的情况。
如果要实现某个
Trait,那么该Trait和要实现这个Trait的类型,至少有一个要在当前Crate中定义
此处的Crate可以理解为一个库,比如std标准库算一个Crate
- 尝试给
Vec<i32>实现std::fmt::Display
Vec<i32>是标准库中的一个动态数组类型,而std::fmt::Display是一个标准库的Trait,实现后可以被print输出。
impl std::fmt::Display for Vec<i32> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "My custom Vec: {:?}", self)
}
}以上代码尝试给Vec<i32>实现std::fmt::Display,但是这个代码会报错,因为Vec<i32>这个类型不属于本地,而std::fmt::Display也不属于本地,这违背了孤儿规则,编译不通过。
- 给标准库类型实现本地
Trait
trait MyTrait {
fn my_method(&self);
}
impl MyTrait for Vec<i32> {
fn my_method(&self) {
println!("Vec length: {}", self.len());
}
}以上代码给Vec<i32>实现MyTrait,这个代码是合法的,因为MyTrait是本地的,符合孤儿规则。
- 给本地类型实现标准库
Trait
struct MyStruct;
impl std::fmt::Display for MyStruct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "This is MyStruct")
}
}
let my_instance = MyStruct;
println!("{}", my_instance);此处的 MyStruct 是自己定义的类型,std::fmt::Display是之前提到的标准库Trait。实现这个Trait后,直接就可以通过print输出结构体。以上过程也是正确的,因为类型是本地的,符合孤儿规则。
9.NewType 模式
在介绍孤儿规则时我们提到:你不能为外部类型实现外部 trait。这条规则保证了编译器在全局范围内的一致性,但在工程实践中也经常让人卡壳。比如:
- 你想为
String实现某个第三方库的trait - 或者你想为
Vec<T>增加一个外部 trait 的实现
那么该怎么办?Rust 社区的惯用解法就是 Newtype 模式。
所谓 Newtype,就是用一个新的元组结构体把原有类型“包裹”起来:
struct MyString(String);这样一来,MyString 是你自己定义的本地类型,本地类型自然可以实现任何外部 trait。
假设我们想为 String 实现一个外部库的 Display:
use std::fmt::{self, Display, Formatter};
struct MyString(String);
impl Display for MyString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "MyString says: {}", self.0)
}
}
fn main() {
let s = MyString("hello".to_string());
println!("{}", s);
}输出:
MyString says: hello这里的关键点在于:String 是外部类型,而Display 是外部 trait。直接 impl Display for String 会违反孤儿规则,但 MyString 是我们自己定义的本地类型,所以 impl Display for MyString 完全合法。
