6. 泛型与Trait篇¶
对应章节:第10章(泛型、trait、生命周期)。
本篇掌握行为抽象(trait)、代码复用(泛型)与借用安全(生命周期)。
项目 13:形状库¶
目标:用 trait 抽象"能算面积的形状",泛型复用代码,生命周期处理引用。
创建项目¶
说明:这是库项目(--lib),生成
src/lib.rs。如需命令行演示,可自行添加src/main.rs(bin 与 lib 同名会自动互认)。
功能要求¶
- 定义
Shapetrait(area + name) - 实现
Circle、Rectangle、Triangle - 泛型函数接收任意形状
- 演示生命周期
longest
知识点¶
| 知识点 | 说明 |
|---|---|
trait 定义 |
行为抽象(接口) |
impl Trait for Type |
实现 trait |
泛型 <T: Shape> |
类型参数 + trait 约束 |
trait 对象 &dyn Shape |
动态分发 |
生命周期 'a |
引用有效范围 |
| 静态 vs 动态分发 | 编译期 vs 运行期 |
代码¶
src/lib.rs:
// trait 定义"行为":任何实现它的类型都能计算面积、给出名字
pub trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
// 三个形状结构体(字段设为 pub,方便在 main 中直接构造)
pub struct Circle { pub radius: f64 }
pub struct Rectangle { pub width: f64, pub height: f64 }
pub struct Triangle { pub base: f64, pub height: f64 }
// 为 Circle 实现 Shape
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> &str { "圆形" }
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> &str { "矩形" }
}
impl Shape for Triangle {
fn area(&self) -> f64 {
self.base * self.height / 2.0
}
fn name(&self) -> &str { "三角形" }
}
// 泛型函数:T 必须实现 Shape
// 编译期把 T 替换为具体类型 → 静态分发,性能好
pub fn describe<T: Shape>(shape: &T) -> String {
format!("{} 面积是 {:.2}", shape.name(), shape.area())
}
// trait 对象:&dyn Shape 表示"任意实现 Shape 的类型"
// 运行时查虚表(动态分发),可以装不同类型的形状混合使用
pub fn describe_all(shapes: &[&dyn Shape]) -> Vec<String> {
shapes.iter().map(|s| describe(*s)).collect()
}
// 生命周期:返回的引用与较短的输入引用同寿命
// 'a 是编译器用来检查"返回的引用不会悬空"的标注
pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
src/main.rs:
use geometry::{Circle, Rectangle, Shape, Triangle, describe, describe_all, longest};
fn main() {
let c = Circle { radius: 3.0 };
let r = Rectangle { width: 4.0, height: 5.0 };
let t = Triangle { base: 6.0, height: 2.0 };
// 泛型:编译期确定具体类型
println!("{}", describe(&c));
println!("{}", describe(&r));
println!("{}", describe(&t));
// trait 对象(动态分发):三种不同类型放进同一个数组
let shapes: [&dyn Shape; 3] = [&c, &r, &t];
for s in describe_all(&shapes) {
println!("{}", s);
}
// 生命周期:longest 返回的引用与传入参数绑定
let s1 = String::from("hello");
let s2 = String::from("world");
let result = longest(&s1, &s2); // result 借用 s1/s2,它们不能提前释放
println!("最长的是:{}", result);
}
运行¶
注意事项¶
- 泛型 vs trait 对象:
describe<T: Shape>编译期展开(静态分发,性能好);&dyn Shape运行时查虚表(动态分发,灵活,可混合类型)。按需选择。 - 生命周期
'a是约束:longest<'a>(a: &'a str, b: &'a str) -> &'a str表示"返回值和输入引用寿命相同"——编译器用这个约束检查借用不悬空。 &[&dyn Shape]:数组元素是引用,引用指向的是 trait 对象。- trait 需要
pub才能跨 crate 使用:describe/describe_all/longest在 main 中调用,必须pub。
进阶:纯库 + 单元测试¶
- trait 更小:
Area只有area方法——接口越小,实现越省 - Triangle 用三边:字段
a/b/c配海伦公式,而不是底×高÷2 - 固有
impl构造器:每个形状配new关联函数(注意没加pub,见要点3) total_area动态分发求和:&[&dyn Area]混装三种形状- 没有 main.rs:纯库项目,正确性全靠单元测试验证
src/lib.rs:
pub trait Area {
fn area(&self) -> f64;
}
pub struct Circle {
pub radius: f64,
}
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
pub struct Triangle {
pub a: f64,
pub b: f64,
pub c: f64,
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// 固有 impl:类型自己的方法,不属于任何 trait
impl Circle {
fn new(radius: f64) -> Self {
Circle { radius }
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Rectangle {
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
impl Triangle {
fn new(a: f64, b: f64, c: f64) -> Self {
Triangle { a, b, c }
}
}
impl Area for Triangle {
fn area(&self) -> f64 {
// 海伦公式:s = 半周长,面积 = √(s(s-a)(s-b)(s-c))
let s = (self.a + self.b + self.c) / 2.0;
(s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt()
}
}
// trait 对象切片:不同形状混在一起求总面积(动态分发)
pub fn total_area(shapes: &[&dyn Area]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn circle_area() {
let circle = Circle { radius: 5.0 };
assert_eq!(circle.area(), std::f64::consts::PI * 25.0);
}
#[test]
fn rectangle_area() {
let rectangle = Rectangle {
width: 5.0,
height: 10.0,
};
assert_eq!(rectangle.area(), 50.0);
}
#[test]
fn triangle_area() {
let triangle = Triangle {
a: 3.0,
b: 4.0,
c: 5.0,
};
assert_eq!(triangle.area(), 6.0); // 3-4-5 直角三角形
}
#[test]
fn total_area_of_shapes() {
let c1 = Circle::new(1.0);
let c2 = Circle::new(2.0);
let shapes: Vec<&dyn Area> = vec![&c1, &c2];
let total = total_area(&shapes);
// 浮点数不能直接比相等,用误差范围判断
assert!((total - 15.7079).abs() < 0.0001);
}
#[test]
fn total_area_mixed() {
let c = Circle::new(1.0);
let r = Rectangle::new(3.0, 4.0);
let t = Triangle::new(3.0, 4.0, 5.0);
let shapes: Vec<&dyn Area> = vec![&c, &r, &t];
let total = total_area(&shapes);
assert!((total - 21.1416).abs() < 0.001);
}
#[test]
fn longest_test() {
let s1 = String::from("short");
let s2 = String::from("longer");
assert_eq!(longest(&s1, &s2), &s2[..]);
}
}
运行¶
cargo test
# running 6 tests
# test tests::circle_area ... ok
# test tests::rectangle_area ... ok
# ...
# test result: ok. 6 passed
要点¶
- 接口设计取舍:
Area(单方法)vsShape(area+name)——不需要名字时,小 trait 让实现者少写代码;需要时再加。trait 可以之后扩展,但删除方法算破坏性变更。 - 固有 impl vs trait impl:
Circle::new写在普通impl Circle里(类型自带能力);fn area写在impl Area for Circle里(满足契约)。调用方式也不同:Circle::new(1.0)vsc.area()。 - 可见性实战:
new没加pub,库外部调不到它;单元测试在同一个 crate 内,所以能用——这就是pub边界的实际含义。想让外部用构造器就加pub。 - 浮点断言:
(total - expected).abs() < 0.0001而不是assert_eq!——二进制浮点有舍入误差,精确比较会翻车。