Skip to content

Trait 抽象接口

Rust 强调将数据(结构体的字段)和行为(trait 定义的方法)分离。结构体负责存储数据,而 trait 负责定义操作这些数据的接口。这种设计使得代码更加清晰和模块化。

抽象接口是Trait的最基础的用法,它的特点如下:

  1. 使用trait关键字定义接口
  2. 接口中可以定义方法,并支持默认实现
  3. 使用impl关键字为类型实现接口方法
  4. 接口中不能实现另一个接口,但接口之间可以继承
  5. 同一个接口可以被多个类型实现,但不能被一个类型实现多次

1.定义与实现

Trait定义了一组可以被共享的行为,只要实现了Trait,该类型就能使用这组行为。

1.1 定义Trait

定义trait基本语法如下,trait的名称应使用大驼峰

rust
trait TraitName {
	fn func_1(args...) ->return_type ;     // 关联函数

	fn func_2(&self, args..) -> return_type ;		// 方法
}

使用trait关键字,可以定义一个Trait。在它的名字后面,使用{}定义这个Trait所具有的方法。

在定义时,往往只写出函数的签名,并且允许使用Self作为第一个参数,函数体用;代替,表示还未实现。

比如说我们可以定义一个植物接口,其中有一个攻击的方法

rust
trait Plant {
    fn attack(&self);
}

1.2 实现Trait

rust
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

{}中,需要给每一个之前定义的方法做出实现,必须实现所有未提供默认实现的方法。

上面已经定义了一个植物的接口,那么下面我们创建两个结构体,一个是豌豆射手,一个是大喷菇,他们各自有个一伤害的属性,每个植物的伤害不一样

rust
// 豌豆射手
struct Peashooter {
    damage: i32,
}

// 大喷菇
struct PuffShroom {
    damage: i32,
}

然后我们分别为这两个植物实现Plant接口

rust
impl Plant for Peashooter {
    fn attack(&self) {
        println!("豌豆射手攻击,给对方造成{}点伤害", self.damage);
    }
}

impl Plant for PuffShroom {
    fn attack(&self) {
        println!("大喷菇攻击,给对方造成{}点伤害", self.damage);
    }
}

最后我们创建两个示例调用自身的方法

rust
fn main() {
    let peashooter = Peashooter { damage: 100 };
    let puffshroom = PuffShroom { damage: 150 };
    peashooter.attack();		// 豌豆射手攻击,给对方造成100点伤害
    puffshroom.attack();		// 大喷菇攻击,给对方造成150点伤害
}

1.3 实现多个trait

一个类型也可以实现多个trait

rust
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方法的默认实现

rust
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中,支持使用泛型,语法如下:

rust
trait TraitName<T, ...> {
}

impl<T, ...> TraitName<T, ...> for type {
}

只需要在名称后面添加泛型声明列表,后续在实现中就可以使用。

与基本的泛型语法相同,在impl时需要先impl<>声明泛型,后续才能使用。

依然是植物接口,这次attack函数接受一个伤害值,并把这个值返回出去

rust
trait Plant<T> {
    fn attack(&self, damage: T) -> T;
}

豌豆射手和大喷菇就不添加damage属性了,因为damage要用函数传递了

rust
// 豌豆射手
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
    }
}

最后使用

rust
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实现中,需要给关联类型赋值一个类型
rust
trait TraitName {
    type AttackType;
    fn attack(&self, damage: Self::AttackType);
}

impl TraitName for S{
    type AttackType = u32;
}

依然是豌豆射手和大喷菇,有时候植物工具不一定是整数,也有可能是浮点数,那么这时候我们可以在接口实现时,给这个关联类型一个具体的类型

rust
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::
rust
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 默认值

关联常量可以给一个默认值,并且已经是一个稳定特性了,可以直接使用。

rust
trait Plant {
    const H: u32 = 100;
}

6.Trait 继承

Rust不支持面向对象中的类型继承,但是在Trait之间允许继承。

6.1 基本继承

如果某个类型要实现该Trait,必须先实现这个Trait的所有父Trait

trait继承语法:

rust
trait ChildTrait: FatherTrait  {}

定义Trait时,在名称后面使用:指明要继承的其它Trait

rust
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

然后我们定义一个索隆实例,调用这些方法

rust
trait 鬼彻 {
    fn luck(&self);
}
trait 和刀一文字 {
    fn girlfriend(&self);
}

trait 阎魔 {
    fn black(&self);
}

trait 三刀流: 鬼彻 + 和刀一文字 + 阎魔 {
    fn ultimate(&self);
}
rust
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!("三刀流奥义,阿修罗")
    }
}
rust
fn main() {
    let 索隆 = 剑客 {
        name: String::from("索隆"),
    };


    索隆.luck();
    索隆.girlfriend();
    索隆.black();
    索隆.ultimate();
}

一个继承了别的trait的trait,也可以被继承

比如,下面示例,分别定义了12345档,每个档都继承前面的档

rust
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!("解放之鼓")
    }
}
rust
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!("解放之鼓")
    }
}
rust
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 中查找
  • 如果存在多个候选,就会报错,提示你需要显式指定

完全限定语法如下:

rust
<Type as TraitName>::method(args...)

Type 表示具体的类型,TraitName 表示方法来源的Traitmethod 是要调用的方法名,args... 是额外的参数。

假设我们有两个 Trait,它们都定义了一个同名方法:

rust
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
  • 来自 Pilotfly
  • 来自 Wizardfly

如果直接调用:

rust
let h = Human;
h.fly();

编译器会优先选择固有方法,因此输出:

rust
Human flapping arms... not very effective.

但如果我们想调用 PilotWizard 的版本,就必须使用完全限定语法:

rust
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.孤儿规则

基于impltrait两个特性,你可以很轻易的给一个类型添加各种方法,这就有可能导致一些不太安全的操作。

比如你的某位同事,已经为一个类型封装好了它的各类接口和Trait。但是你使用这个类型前,又对它的这个Trait进行了实现,导致篡改了某些该类型原本的行为,这就是一种破坏性的改写,可能导致难以预料的Bug,孤儿规则可以避免类似的情况。

如果要实现某个Trait,那么该Trait和要实现这个Trait的类型,至少有一个要在当前Crate中定义

此处的Crate可以理解为一个库,比如std标准库算一个Crate

  • 尝试给Vec<i32>实现std::fmt::Display

Vec<i32>是标准库中的一个动态数组类型,而std::fmt::Display是一个标准库的Trait,实现后可以被print输出。

rust
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
rust
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
rust
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,就是用一个新的元组结构体把原有类型“包裹”起来:

rust
struct MyString(String);

这样一来,MyString 是你自己定义的本地类型,本地类型自然可以实现任何外部 trait

假设我们想为 String 实现一个外部库的 Display

rust
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);
}

输出:

rust
MyString says: hello

这里的关键点在于:String 是外部类型,而Display 是外部 trait。直接 impl Display for String 会违反孤儿规则,但 MyString 是我们自己定义的本地类型,所以 impl Display for MyString 完全合法。

MIT Licensed