5.1 Trait定义与实现:行为契约


5.1 Trait 定义与实现:行为契约

本节摘要:Trait 定义一组方法签名,类型通过 impl 履行契约;默认方法让契约自带通用实现。本节覆盖 trait 定义、实现规则(孤儿规则)、默认方法与标准库高频 Trait。读完你能为自己的类型接入整个标准库生态的协议体系。

立约与履约

trait Archivable { fn archive_id(&self) -> String; // 必须履行 fn summary(&self) -> String { // 默认条款 format!("[{}]", self.archive_id()) } } struct PaperCase { no: u32 } impl Archivable for PaperCase { fn archive_id(&self) -> String { format!("P-{}", self.no) } } let c = PaperCase { no: 173 }; println!("{}", c.summary()); // P-173,默认方法直接可用

两条司法限制:实现要么在类型本地、要么在 Trait 本地(孤儿规则),防止两个库给同一类型实现同一契约互相冲突;同类型对同 Trait 只能实现一次。

默认方法的价值

summary 只依赖契约内其他方法,实现方写一个 archive_id 就能白得一套流程。标准库的 Display、Iterator 大量用这招——Iterator 有七十多个默认方法,全部由你实现的那一个 next 推导出来。

标准库高频契约

Trait 契约内容 常见获得方式
Debug 调试打印 派生宏自动生成
Clone 与 Copy 显式复制、位复制 派生(Copy 需字段全 Copy)
PartialEq 相等比较 派生
Display 用户可读输出 手工实现(一个 fmt 方法)
From 从别的类型转换而来 手工实现,即获得 Into 反向能力

From 与 Into 是一对赠品判例:为 A 实现 From<B>,就自动得到 b.into() 得 A——标准库的 blanket impl 在起作用。

约束先行:泛型函数的入门

use std::fmt::Display; fn announce<T: Display>(item: T) { println!("现在宣布:{}", item); }

T: Display 是契约约束:只有会展示的类型能进来。约束机制在下一节全面展开。

图:契约履行的三方关系

图:契约履行的三方关系

要点回顾

  • Trait 是行为契约,impl 是履行行为,二者解耦;
  • 默认方法免费复用,Iterator 是最佳范例;
  • 孤儿规则保证实现不冲突,这是库生态的法律基础;
  • 实现 From 即获得 Into,blanket impl 的第一课。

默认方法与覆盖:trait 里的继承语义

trait 方法可以有默认实现,这给了"少量必须实现 + 多量可选覆盖"的接口设计模式,标准库的 Iterator 是最著名判例——只需实现 next,其余几十个方法全部免费。

trait Summarize { fn headline(&self) -> String; // 必须实现 fn preview(&self) -> String { // 默认实现 format!("[{}] {}", self.tag(), self.headline()) } fn tag(&self) -> String { // 默认实现可被覆盖,也可被其他默认方法调用 String::from("通告") } } struct Verdict { no: u32 } impl Summarize for Verdict { fn headline(&self) -> String { format!("第 {} 号判决", self.no) } fn tag(&self) -> String { String::from("判词") } // 覆盖默认 } fn main() { let v = Verdict { no: 173 }; println!("{}", v.preview()); // [判词] 第 173 号判决 }

注意默认方法之间可以互相调用,运行时分派到实现方的覆盖版——这是 trait 与早期接口最大的差异之一,也是 Iterator 组合子生态的根基。

关联类型:把"产出什么"写进合同

trait Archiver { type Item; // 关联类型:实现时定死 fn store(&mut self, item: Self::Item); fn count(&self) -> usize; } struct Shelf { slots: Vec<String> } impl Archiver for Shelf { type Item = String; // 每个 Shelf 只有一种物品 fn store(&mut self, item: String) { self.slots.push(item); } fn count(&self) -> usize { self.slots.len() } }

关联类型与泛型参数的选择判据:一个类型对同一 trait 只应有一种实现方式时用关联类型(读代码不用带参数);确实需要多种实现(如 From 多目标转换)时才用泛型参数。标准库里 Iterator 的 Item、Deref 的 Target 都是关联类型的判例。

孤儿规则与工程取舍

trait 或类型至少有一个定义在当前 crate,才能写 impl。这条"孤儿规则"防止两个包对同一对 (trait, type) 给出冲突实现。绕行手法是新类型包装:给外部类型套一层本地元组结构体,再为包装实现本地或外部 trait。代价是多一层 0 成本转换,换来的是全局 impl 的唯一性——这是 Rust 把"库组合不可预测冲突"这类悬案在编译期消灭的方式,第 7 章多 crate 工程会再遇到它。

derive 的清单与边界

可 derive 提供什么 何时手写
Debug / Clone / Copy 调试打印、复制语义 自定义格式或含非 Copy 字段却要 Copy 时不可
PartialEq / Eq 值相等 相等含业务规则(浮点比较)
Hash 哈希键资格 与 PartialEq 语义需一致
Default 零值构造 有意义的默认而非全零

derive 不是语法糖那么简单,它调用的是第 9 章的过程宏——编译期读入 token 流、生成 impl 块再交回编译器。理解这一点,derive 与手写 impl 完全等价的认知就成立了。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U