共享行为:Trait入门

学习目标

学完本章后,你将能够:

  • 理解 trait 是什么:定义共享行为
  • 为自己定义的类型实现 trait
  • 使用 impl Trait 作为参数和返回值
  • 使用 #[derive] 自动获取常见 trait
  • 理解 trait 和接口的相似性

一、问题:不同类型,相同行为

你家里的各种东西都能”发出声音”,但声音不同:

  • 狗叫:汪汪
  • 猫叫:喵喵
  • 闹钟:叮叮叮

在代码里,我们希望能写一个统一的”让某物发出声音”的功能,而不针对每种动物写一份。这就是 trait 的用武之地。


二、最简单的 trait

// 定义一个 trait:描述"能说话"这个能力
trait Speak {
    fn speak(&self);
}
 
// 狗:实现 Speak
struct Dog;
impl Speak for Dog {
    fn speak(&self) {
        println!("汪汪!");
    }
}
 
// 猫:实现 Speak
struct Cat;
impl Speak for Cat {
    fn speak(&self) {
        println!("喵喵!");
    }
}
 
fn main() {
    let dog = Dog;
    let cat = Cat;
 
    dog.speak();
    cat.speak();
}

输出:

汪汪!
喵喵!

拆解:

trait Speak {           // 1. 定义一个 trait
    fn speak(&self);    // 2. 声明一个方法签名(没有函数体)
}
 
impl Speak for Dog {    // 3. 为 Dog 实现这个 trait
    fn speak(&self) {   // 4. 写出具体实现
        println!("汪汪!");
    }
}

trait 就像一个合同:你签了这个合同(impl Speak for Dog),就必须履行合同的条款(实现 speak 方法)。


三、trait 作为参数

有了 trait,就可以写操作”任何能说话的东西”的函数了:

trait Speak {
    fn speak(&self);
}
 
struct Dog;
impl Speak for Dog {
    fn speak(&self) { println!("汪汪!"); }
}
 
struct Cat;
impl Speak for Cat {
    fn speak(&self) { println!("喵喵!"); }
}
 
// 接受 "任何实现了 Speak 的类型"
fn make_speak(animal: &impl Speak) {
    animal.speak();
}
 
// 另一种写法:trait bound(更常用)
fn make_speak_twice<T: Speak>(animal: &T) {
    animal.speak();
    animal.speak();
}
 
fn main() {
    make_speak(&Dog);
    make_speak(&Cat);
    make_speak_twice(&Dog);
}

&impl Speak 的意思是:“一个实现了 Speak trait 的类型的引用”。

写法说明适用场景
fn foo(x: &impl Speak)语法糖写法简单参数
fn foo<T: Speak>(x: &T)trait bound 写法多个参数或复杂约束
fn foo<T>(x: &T) where T: Speakwhere 写法约束很长时更清晰

三种写法等效:

// 写法1
fn func(a: &impl Speak, b: &impl Speak)
 
// 写法2
fn func<T: Speak>(a: &T, b: &T)
 
// 写法3
fn func<T>(a: &T, b: &T) where T: Speak

四、trait 作为返回值

trait Animal {
    fn name(&self) -> &str;
}
 
struct Dog;
impl Animal for Dog {
    fn name(&self) -> "狗"
}
 
struct Cat;
impl Animal for Cat {
    fn name(&self) -> "猫"
}
 
fn pet_shop(choice: i32) -> Box<dyn Animal> {
    if choice == 1 {
        Box::new(Dog)
    } else {
        Box::new(Cat)
    }
}
 
fn main() {
    let pet = pet_shop(1);
    println!("你的宠物是:{}", pet.name());
}

注意:返回 trait 时要用 Box<dyn Trait>,因为不同实现的大小可能不同(Dog 和 Cat 对应的结构体大小可能不一样)。至于 Boxdyn 的具体细节,在深入篇中会详细介绍,现在先记住这种写法即可。


五、自动派生:#[derive]

很多常见的 trait 不需要自己实现,Rust 可以自动生成:

// 自动生成 Debug, Clone, PartialEq
#[derive(Debug, Clone, PartialEq)]
struct Person {
    name: String,
    age: u32,
}
 
fn main() {
    let p1 = Person {
        name: String::from("小明"),
        age: 18,
    };
 
    let p2 = p1.clone();  // Clone 允许我们复制
 
    println!("{:?}", p1);          // Debug 允许打印
    println!("相等吗?{}", p1 == p2);  // PartialEq 允许比较
}
可派生 trait作用
Debug{:?} 打印调试信息
Clone.clone() 复制
Copy赋值时自动复制(浅层,只有小类型)
PartialEq==!= 比较
Eq完整的等价比较(配合 PartialEq)
PartialOrd<, > 等排序比较
Ord完整的排序
Hash用于 HashMap 的键
Default提供默认值 Default::default()

六、trait 的默认实现

trait 中的方法可以给出默认实现,这样实现方可以选择使用默认或覆盖:

trait Greet {
    // 带默认实现
    fn greet(&self) {
        println!("你好!");
    }
 
    // 无默认实现,必须自己写
    fn name(&self) -> &str;
}
 
struct Friend {
    name: String,
}
 
impl Greet for Friend {
    fn name(&self) -> &str {
        &self.name
    }
    // greet 使用默认实现,不需要写
}
 
struct Boss {
    name: String,
}
 
impl Greet for Boss {
    fn name(&self) -> &str {
        &self.name
    }
 
    // 覆盖默认实现
    fn greet(&self) {
        println!("{} 说:工作完成了吗?", self.name);
    }
}
 
fn main() {
    let f = Friend { name: String::from("小明") };
    let b = Boss { name: String::from("老板") };
 
    f.greet();  // "你好!"
    b.greet();  // "老板 说:工作完成了吗?"
}

七、trait 约束组合

use std::fmt::Display;
 
// 要求 T 同时实现 PartialOrd 和 Display
fn max_and_print<T: PartialOrd + Display>(a: T, b: T) {
    if a > b {
        println!("最大:{}", a);
    } else {
        println!("最大:{}", b);
    }
}
 
fn main() {
    max_and_print(10, 20);
    max_and_print(3.14, 2.71);
    // max_and_print("hello", "world"); // 也可以!
}

T: PartialOrd + Display = “T 必须同时实现 PartialOrd 和 Display”。


八、孤儿规则

你不能为不是你写的类型,不是你写的 trait 做实现。这就是”孤儿规则”。

// 假设你想让 i32 实现你自己定义的 Speak trait
trait Speak {
    fn speak(&self);
}
 
impl Speak for i32 {  // 合法!trait 是你写的
    fn speak(&self) {
        println!("数字:{}", self);
    }
}
 
// 但是:
// impl Display for Vec<i32> { ... }
// 非法!Display 和 Vec 都不是你定义的

这条规则防止不同库互相冲突。


九、综合示例:可排序的物品

use std::cmp::Ordering;
 
#[derive(Debug)]
struct Book {
    title: String,
    pages: u32,
}
 
// 实现 PartialEq:定义什么是"相等"
impl PartialEq for Book {
    fn eq(&self, other: &Self) -> bool {
        self.pages == other.pages
    }
}
 
impl Eq for Book {}
 
// 实现 PartialOrd:定义怎么比较
impl PartialOrd for Book {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.pages.partial_cmp(&other.pages)
    }
}
 
impl Ord for Book {
    fn cmp(&self, other: &Self) -> Ordering {
        self.pages.cmp(&other.pages)
    }
}
 
fn main() {
    let mut books = vec![
        Book { title: String::from("Rust入门"), pages: 300 },
        Book { title: String::from("Python入门"), pages: 200 },
        Book { title: String::from("C入门"), pages: 400 },
    ];
 
    books.sort();  // 按页数排序
 
    for book in &books {
        println!("{}({}页)", book.title, book.pages);
    }
}

本章小结

trait 让你定义”共享行为”:

  • trait Name { fn method(&self); } 定义行为规范
  • impl Trait for Type { ... } 为类型实现行为
  • &impl Trait 作为参数,接受任何实现了该 trait 的类型
  • #[derive(Trait)] 自动生成常见 trait 的实现
  • trait 可以有默认实现,类型可以选择覆盖
  • T: A + B 组合多个 trait 约束
  • 孤儿规则:trait 或类型必须有一个是你自己定义的

trait 是 Rust 中实现”多态”的核心机制。接下来学习生命周期 — 帮助编译器理解引用的寿命。

13-引用有效期:生命周期入门


章节考查

总分100分:概念考查40分 + 判断正误20分 + 代码分析15分 + 编程大题15分 + 填空题5分 + 代码补全5分

一、概念考查(每题4分,共40分)

1. trait 在 Rust 中的作用是?

  • A. 存储数据
  • B. 定义共享的行为接口
  • C. 分配内存
  • D. 编译加速
点击查看答案

B。trait 定义了类型必须实现的行为(方法签名),是一种”能力声明”机制。

2. impl Trait for Type 是什么意思?

  • A. 创建一个新类型
  • B. 为 Type 实现 Trait 定义的方法
  • C. 导入一个 trait
  • D. 声明一个变量
点击查看答案

Bimpl Trait for Type 表示”为 Type 类型实现 Trait 所约定的功能”。

3. #[derive(Debug)] 做了什么?

  • A. 创建一个新变量
  • B. 自动生成 Debug trait 的实现
  • C. 删除 Debug trait
  • D. 打印调试信息
点击查看答案

Bderive 是编译器自动生成 trait 实现的机制,省去手动编写的重复代码。

4. fn foo(x: &impl Speak) 中的 &impl Speak 的含义是?

  • A. 只有 Speak 类型能传入
  • B. 任何实现 Speak trait 的类型都可以传入
  • C. x 是 Speak trait 本身
  • D. x 必须是一个引用
点击查看答案

B&impl Speak 表示接受”实现了 Speak trait 的任意类型的引用”。

5. 孤儿规则(Orphan Rule)规定?

  • A. trait 不能有默认实现
  • B. 实现 trait 时,trait 或类型必须至少有一个是本地定义的
  • C. trait 只能在 main 中实现
  • D. 每个 trait 只能被实现一次
点击查看答案

B。孤儿规则是为了确保 trait 实现的”一致性”:如果你不拥有 trait,也不拥有类型,就不能实现它。

6. trait 方法的默认实现是怎样的?

  • A. 不能被覆盖
  • B. 在 trait 中写函数体,实现方可以选择使用或覆盖
  • C. 所有方法必须有默认实现
  • D. 默认实现不存在
点击查看答案

B。trait 方法可以在 trait 定义中给出默认函数体,实现者可以选择覆盖或不覆盖。

7. T: Display + Clone 意味着?

  • A. T 要么实现 Display,要么实现 Clone
  • B. T 必须同时实现 Display 和 Clone
  • C. T 不能实现 Display 和 Clone
  • D. Display 比 Clone 更重要
点击查看答案

B+ 表示”并且”,T 必须同时满足两个约束。

8. 以下哪个不能 #[derive]

  • A. Debug
  • B. Clone
  • C. PartialEq
  • D. 任何自定义 trait
点击查看答案

D(当前理解层面)。derive 只能用于编译器内置支持的特殊 trait,自定义 trait 需要手动 implement。

9. PartialOrd trait 的作用是?

  • A. 比较是否相等
  • B. 排序比较(大于、小于)
  • C. 打印
  • D. 复制
点击查看答案

BPartialOrd 提供 <><=>= 比较功能。

10. Box<dyn Trait> 用于什么场景?

  • A. 加速代码
  • B. 在返回值中使用 trait 类型(动态分发)
  • C. 替换所有引用
  • D. 创建数组
点击查看答案

BBox<dyn Trait> 允许在运行时确定具体类型,用于存储、返回所需的不确定大小的 trait 对象。

二、判断正误(每题2分,共20分)

1. trait 可以有字段定义。

点击查看答案

错误。trait 只能定义方法(包括默认实现),不能定义字段。

2. 一个类型可以实现多个 trait。

点击查看答案

正确。一个类型可以 impl TraitA for Type { ... }impl TraitB for Type { ... }

3. impl Trait 语法和 T: Trait 语法在参数列表中完全等价。

点击查看答案

近似正确,但有细微差别(多个参数时约束是否要求同一类型等)。在入门层面可以认为它们表达相同的意思。

4. #[derive(Clone)] 对所有类型都有效。

点击查看答案

错误。如果结构体包含不支持 Clone 的字段,derive(Clone) 会失败。

5. trait 方法不能有返回值。

点击查看答案

错误。trait 方法可以定义任何返回类型,包括泛型返回类型。

6. &impl Trait 不能用作函数返回类型。

点击查看答案

实际上在新版 Rust 中 -> impl Trait 可以用作返回类型,但和 -> &impl Trait 有所不同。命题说”不能”是不准确的。在较新 Rust 版本中可以使用。

7. 所有实现了 PartialEq 的类型都可以用 == 比较。

点击查看答案

正确PartialEq trait 定义了 eqne 方法,==!= 是它们的语法糖。

8. trait 中带有默认实现的方法可以不用在 impl 块中重写。

点击查看答案

正确。默认提供了实现,实现者不必再写。除非想覆盖它。

9. 可以为自己写的 trait 给他人的类型做实现。

点击查看答案

正确。孤儿规则允许:只要 trait 或类型中有一个是你自己定义的就可以。

10. Box<dyn Animal> 在编译时就确定了具体类型。

点击查看答案

错误dyn 表示动态分发,具体类型在运行时确定。

三、代码分析(每题3分,共15分)

1. 下面代码的输出是什么?

trait Sound {
    fn make(&self) -> &str;
}
 
struct Bell;
impl Sound for Bell {
    fn make(&self) -> &str {
        "叮咚"
    }
}
 
struct Drum;
impl Sound for Drum {
    fn make(&self) -> &str {
        "咚咚"
    }
}
 
fn play(s: &impl Sound) {
    println!("{}", s.make());
}
 
fn main() {
    play(&Bell);
    play(&Drum);
}
  • A. 叮咚 咚咚
  • B. 编译错误
  • C. 咚咚 叮咚
  • D. 只有 叮咚
点击查看答案

Aplay(&Bell) 打印”叮咚”,play(&Drum) 打印”咚咚”。

2. 下面代码的输出是什么?

#[derive(Debug, Clone)]
struct Item {
    name: String,
    price: u32,
}
 
fn main() {
    let i1 = Item { name: String::from("笔"), price: 5 };
    let i2 = i1.clone();
    println!("{:?}", i1);
}
  • A. 编译错误(i1 被移动了)
  • B. Item { name: “笔”, price: 5 }
  • C. clone 失败
  • D. 打印 i2 的内容
点击查看答案

Bi1.clone() 创建了独立副本,i1 仍可使用。打印输出 i1 的 Debug 格式。

3. 下面代码能否编译通过?

trait Printable {
    fn print(&self);
}
 
impl Printable for i32 {
    fn print(&self) {
        println!("数字:{}", self);
    }
}
 
fn main() {
    42.print();
}
  • A. 能,输出”数字:42”
  • B. 不能,i32 不能有方法
  • C. 不能,孤儿规则不允许
  • D. 能,但输出”42”
点击查看答案

A。trait 是我们自己写的,i32 是标准库的 — 符合孤儿规则(至少一个是本地的)。

4. 下面代码的输出是什么?

trait Describable {
    fn describe(&self) -> String;
}
 
impl Describable for i32 {
    fn describe(&self) -> String {
        format!("整数:{}", self)
    }
}
 
fn print_info(x: &impl Describable, y: &impl Describable) {
    println!("{} 与 {}", x.describe(), y.describe());
}
 
fn main() {
    let a = 10;
    let b = 20;
    print_info(&a, &b);
}
  • A. 编译错误
  • B. 整数:10 与 整数:20
  • C. 10 与 20
  • D. 没有输出
点击查看答案

Bab 都是 i32,都实现了 Describable,输出对应的描述。

5. 下面代码有什么问题?

trait Action {
    fn act(&self);
}
 
struct Robot;
impl Action for Robot {
    fn act(&self) { println!("机械动作"); }
}
 
struct Human;
impl Action for Human {
    fn act(&self) { println!("人类动作"); }
}
 
fn factory(kind: bool) -> impl Action {
    if kind {
        Robot
    } else {
        Human
    }
}
  • A. 没有错误
  • B. 不能用 impl Action 作为返回类型
  • C. impl Action 返回时必须所有分支返回相同类型
  • D. act 方法不能有默认实现
点击查看答案

Cimpl Trait 作为返回类型时,所有代码路径必须返回同一具体类型。这里 Robot 和 Human 是不同的类型。应该用 Box<dyn Action>

四、编程大题(15分)

题目: 设计一个”形状面积计算”系统:

  1. 定义 trait Area:方法 fn area(&self) -> f64
  2. 定义 trait Perimeter:方法 fn perimeter(&self) -> f64
  3. 实现结构体 Circle(半径)和 Rectangle(宽高)
  4. 两者都实现 Area 和 Perimeter
  5. 实现一个函数 print_info(shape: &(impl Area + Perimeter)) 打印面积和周长
  6. 在 main 中测试
点击查看答案
use std::f64::consts::PI;
 
trait Area {
    fn area(&self) -> f64;
}
 
trait Perimeter {
    fn perimeter(&self) -> f64;
}
 
struct Circle {
    radius: f64,
}
 
impl Area for Circle {
    fn area(&self) -> f64 {
        PI * self.radius * self.radius
    }
}
 
impl Perimeter for Circle {
    fn perimeter(&self) -> f64 {
        2.0 * PI * self.radius
    }
}
 
struct Rectangle {
    width: f64,
    height: f64,
}
 
impl Area for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}
 
impl Perimeter for Rectangle {
    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
}
 
fn print_info(shape: &(impl Area + Perimeter)) {
    println!("面积:{:.2},周长:{:.2}", shape.area(), shape.perimeter());
}
 
fn main() {
    let c = Circle { radius: 5.0 };
    let r = Rectangle { width: 4.0, height: 6.0 };
 
    println!("圆形:");
    print_info(&c);
 
    println!("矩形:");
    print_info(&r);
}

评分标准

  • Area trait(2分)
  • Perimeter trait(2分)
  • Circle 和 impl(3分)
  • Rectangle 和 impl(3分)
  • print_info 函数(3分)
  • main 测试(2分)

五、填空题(每题1分,共5分)

1. 定义 trait 用关键字 ______

点击查看答案

traittrait Name { fn method(&self); }

2. 为类型实现 trait:______ TraitName ______ TypeName { ... }

点击查看答案

implforimpl TraitName for TypeName { ... }

3. #[derive(______)] 可以让结构体支持 {:?} 打印。

点击查看答案

Debugderive(Debug) 生成 fmt::Debug 的实现。

4. fn foo(x: &______ Trait) 接受任何实现了 Trait 的类型的引用。

点击查看答案

impl&impl Trait 是 trait bound 的语法糖。

5. ______ 规则:实现 trait 时,trait 或类型至少一个必须是本地定义的。

点击查看答案

孤儿(Orphan)。孤儿规则保证了 trait 实现的一致性(coherence)。

六、代码补全(共5分)

1. 补全 trait 定义(2分)

______ Summary {
    fn summarize(&self) -> String;
}
点击查看答案
trait Summary {

2. 补全 trait 实现(2分)

struct Article {
    title: String,
}
 
______ Summary ______ Article {
    fn summarize(&self) -> String {
        format!("文章:{}", self.title)
    }
}
点击查看答案
impl Summary for Article {

3. 补全 derive 用法(1分)

#[______(Debug, Clone)]
struct Point {
    x: i32,
    y: i32,
}
点击查看答案
#[derive(Debug, Clone)]

计分:概念40 + 判断20 + 代码分析15 + 编程15 + 填空5 + 补全5 = 总分100分


练习

练习

题号题目链接知识点
P1001A+B Problemhttps://www.luogu.com.cn/problem/P1001输入输出
P1008全排列https://www.luogu.com.cn/problem/P1008递归、枚举
P1010幂次方https://www.luogu.com.cn/problem/P1010函数、递归
P1012拼数https://www.luogu.com.cn/problem/P1012字符串、排序