Rust 中的数据 vs 行为
Source: Dev.to
Rust 中的结构体:建模数据
在 Rust 中,struct 仅用于表示数据。
struct Rect {
width: f32,
height: f32,
}
结构体本身并不定义行为。这使得数据表示保持明确且简单。
使用 impl 添加行为
Rust 通过 impl 块将行为附加到数据上。
impl Rect {
fn area(&self) -> f32 {
self.width * self.height
}
}
这种方式在行为属于单一具体类型时非常理想。方法使用 &self 借用结构体,确保在不转移所有权的情况下安全访问。
Rust 中的 Trait:定义行为契约
Trait 定义跨类型的共享行为。
trait Shape {
fn area(&self) -> f32;
}
Trait 指明 类型能做什么,而不是 如何做。
impl Shape for Rect {
fn area(&self) -> f32 {
self.width * self.height
}
}
这使得多个类型可以实现相同的行为,同时保持各自内部逻辑的独立。
Trait Bound 与可扩展设计
当与泛型函数一起使用时,Trait 的威力尤为强大。
fn get_area(shape: impl Shape) -> f32 {
shape.area()
}
该函数可以接受任何实现了 Shape Trait 的类型。添加新形状无需修改已有代码,这也是为什么 Trait 被视为 Rust 可扩展性的核心工具。
Clone 与 Copy:所有权感知的复制
Rust 使用 Trait 来控制值的复制方式。
#[derive(Clone)]
struct Person {
name: String,
age: u32,
}
因为 String 拥有堆内存,克隆必须是显式的:
let p2 = p1.clone();
对于仅在栈上的数据,Rust 允许隐式复制:
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
这种显式的区分可以防止意外的内存错误。
Debug 与 Display:Rust 中的格式化输出
Rust 将面向开发者的输出与面向用户的输出分离。
Debug 格式化
#[derive(Debug)]
struct Rect {
width: u32,
height: u32,
}
println!("{:?}", rect);
Display 格式化
use std::fmt;
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.width, self.height)
}
}
println!("{}", rect);
这种分离鼓励干净的日志记录并提供更好的用户体验。
为什么这种设计有效
通过将 数据 (struct) 与 行为 (trait) 分离,Rust 避免了继承相关的问题,促进了组合。这样可以得到更易于扩展、重构和推理的代码。