Rust 并不把“面向对象”当作唯一范式,但通过 struct/enum 与 impl,我们依然可以把数据与行为贴合在一起,构造出清晰、稳定、可维护的抽象边界。

在 Rust 中,“方法”是与类型相关联的函数,定义在 impl TypeName { ... } 代码块中。没有接收者参数的叫“关联函数”,通常承担构造或工厂职责;带接收者参数的才是“方法”。接收者的选择反映所有权与可变性策略:
&self:只读借用,不修改可观察状态;&mut self:可变借用,允许在借用规则内修改状态;self:移动所有权,常用于“终结性构建”或把当前对象移入别处。没有接收者参数的是“跟类型走”的函数(Type::func(...)),叫关联函数;带接收者参数的是“跟实例走”的方法(value.method(...))。构造、工厂、解析、常量查询等不依赖实例状态的行为,
适合作为关联函数;读取、修改、消费实例的行为,适合作为方法。
&self&mut selfself&self + 内部可变性(Cell/RefCell,谨慎使用)在 Rust 社区中,我们遵循一套清晰的关联函数命名约定。最核心的是提供一个 new() 关联函数作为"默认构造器",它应该创建一个最小可用、最常见的实例。
当需要更多参数化选项时,我们不会创建诸如 new_with_capacity、new_with_options 这样冗长的函数名,而是采用更具语义表达力的命名方式。
比如,with_capacity(size) 比 new_with_capacity(size) 更简洁;from_bytes(data) 比 new_from_bytes(data) 更直观;empty() 比 new_empty() 更清晰。
这种命名方式让调用代码读起来更像自然语言,提高了代码的可读性和表达力。同时,new() 应该始终是最简单、最直接的构造方式,不需要复杂参数,让新手能够快速上手。
|#[derive(Debug, Clone, PartialEq, Eq)] struct Counter { current: i32, step: i32 } impl Counter { pub fn new() -> Self { Self { current: 0, step: 1 } } pub fn with_step(step: i32) -> Self { Self { current: 0, step } } } fn main() { println!("{:?}", Counter::new()); println!("{:?}", Counter::with_step(3)); }
|Counter { current: 0, step: 1 } Counter { current: 0, step: 3 }
当构造过程需要进行输入校验或可能遇到错误时,我们应该避免将 new 函数设计为"可能失败"的版本。这样做的原因是 new() 在 Rust 生态中被广泛认为是一个"总是成功"的构造函数,开发者期望它能够直接返回实例而不是 Result 类型。
|impl Counter { pub fn try_with_step(step: i32) -> Result<Self, String> { if step <= 0 { return Err("step must be positive".into()); } Ok(Self { current: 0, step }) } }
在 Rust 的方法设计中,&self 和 &mut self 这两种接收器类型体现了不同的契约承诺,它们直接影响着方法的行为语义和并发安全性。
&self 方法建立了一个重要的不变性契约:它承诺不会改变对象的任何可观察状态。这里的"可观察"是关键词——即使方法内部可能使用 Cell 或 RefCell 进行一些内部状态缓存,但从外部调用者的角度看,对象的逻辑状态保持不变。这种设计让多个线程可以同时安全地调用这些方法,因为没有数据竞争的风险。
相对地,&mut self 方法获得了修改权限,可以直接更新结构体的内部字段。这种独占性借用确保了在方法执行期间,没有其他代码能够同时访问该对象,从而在编译时就消除了数据竞争的可能性。
Rust 的借用检查器在编译阶段静态分析这些借用关系,确保别名规则得到严格遵守:要么存在多个不可变引用,要么存在一个可变引用,但两者不能同时存在。
|impl Counter { pub fn tick(&mut self) { self.current += self.step; } pub fn peek(&self) -> i32 { self.current } pub fn set_step(&mut self, step: i32) { self.step
|4 9
返回借用还是返回拷贝?
当调用方只是“看一眼”时,优先返回借用以避免分配(如 name(&self) -> &str)。当调用方需要“长期持有”时,再提供显式的拷贝版(如 to_name(&self) -> String)。两者并存可以兼顾性能与易用性。
self 夺走所有权当方法在语义上"终结"当前对象的生命周期,或需要将整个对象的所有权转移到其他地方时,使用 self 作为接收器是最自然的选择。这种模式在以下几种场景中特别常见:
资源转换场景:当我们需要将一个临时的构建器对象转换为最终的目标对象时,构建器已经完成了它的使命,应该被"消费"掉。比如 ConfigBuilder 调用 build() 后就不应该再被使用。
容器迁移场景:当对象需要从一个容器移动到另一个容器,或者需要将其内部资源提取出来时,消费掉原对象可以避免不必要的克隆操作。
状态机转换场景:在状态机设计中,从一个状态转换到另一个状态时,旧状态对象的使命已经结束,使用 self 可以确保编译器强制执行"一次性转换"的语义。
这种设计的核心优势在于,它通过类型系统在编译时就防止了对已经"失效"对象的误用,同时避免了不必要的内存分配和数据复制。
|#[derive(Debug)] struct Config { host: String, port: u16 } struct ConfigBuilder { host: String, port: u16 } impl ConfigBuilder { fn new() -> Self { Self { host: "127.0.0.1".into(), port: 8080 } } fn
|Config { host: "0.0.0.0", port: 3000 }
消费 self 的方法体现“终结语义”:如 into_inner(self)、build(self)、finalize(self)。当需要把内部资源转移给调用方或放入其他容器时,使用 self 能减少拷贝与别名问题。
通过让方法返回 Self 或新对象,我们可以把一系列配置写成顺畅的链式调用。链式调用应保持语义清晰:当链过长或包含复杂逻辑时,适当引入局部变量更易维护。
|#[derive(Debug, Default)] struct Request { url: String, timeout_ms: u64, retries: u32 } impl Request { fn new(url: impl Into<String>) -> Self { Self { url: url.into(), ..Default::default
当任一步骤可能失败时,构建器可以让 build(self) -> Result<T, E> 携带错误,并在内部进行逐步校验:
|#[derive(Default)] struct Builder { port: Option<u16> } impl Builder { fn port(mut self, p: u16) -> Self { self.port = Some(p); self } fn build(self) -> Result<Config
Rust 允许我们为同一类型编写多个 impl 块,这样可以将不同功能的方法分组管理,提高代码组织性。同时,通过孤儿规则(orphan rule),我们可以为外部类型添加新的方法实现,但前提是类型或 trait 至少有一方是我们自己定义的。
当 impl 块过于庞大时,按功能分割成多个块能让代码更清晰:
|struct Millis(u64); impl Millis { fn as_secs(&self) -> u64 { self.0 / 1000 } } trait Pretty { fn pretty(&self) -> String; } impl Pretty for Millis { fn pretty
|2300ms (~2s)
当我们想要为标准库或第三方crate中的类型添加新方法时,由于孤儿规则的限制,我们不能直接为这些外部类型实现trait。这时可以使用"新类型"模式(newtype pattern),通过创建一个包装结构体来绕过这个限制。 新类型本质上是一个只包含一个字段的元组结构体,它持有我们想要扩展的原始类型:
|struct AvgVec(Vec<i32>); impl AvgVec { fn avg(&self) -> Option<f64> { if self.0.is_empty() { None } else { Some(self.0.iter().sum::<i32>()
为枚举定义语义化的构造函数与方法,是 Rust 中一种强大的设计模式。这种方式可以将复杂的模式匹配逻辑从外部调用代码转移到枚举内部,不仅提高了代码的可读性和可维护性,还能确保枚举的使用方式更加一致和安全。
通过为枚举实现构造函数,我们可以提供更加直观的创建方式,避免直接使用变体构造语法。同时,将常见的操作封装为方法,可以隐藏内部的匹配细节,让外部代码更加简洁。这种封装还有助于在后续修改枚举结构时,减少对外部代码的影响。
|#[derive(Debug)] enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, } impl Shape { fn circle(r: f64) -> Self { Self::Circle { r } } fn rect(w: f64
|Circle { r: 2.0 } => 12.566370614359172 Rect { w: 3.0, h: 4.0 } => 12
把频繁出现的 match 封装进方法,既减少样板,又避免调用方遗漏分支:
|impl Shape { fn scale(&mut self, k: f64) { match self { Shape::Circle { r } => *r *= k, Shape::Rect { w, h } => { *w *= k; *h *= k; } } } }
Rust 的自动解引用机制是一个强大的语言特性,它让我们能够更自然地使用智能指针。当一个类型 T 实现了 Deref<Target = U> trait 时,编译器在方法调用的上下文中会自动执行解引用转换,将 &T 类型的引用视为 &U 类型。
这种机制的核心价值在于让智能指针的使用变得透明化。我们不需要显式地调用解引用操作符,就能够直接在智能指针上调用其包装类型的方法。
例如,Box<String> 可以直接调用 String 的方法,Rc<Vec<i32>> 可以直接调用 Vec<i32> 的方法。
编译器会按照以下顺序查找方法:
T 自身查找方法Deref 转换到 U 类型继续查找这种设计让智能指针真正做到了"像值一样使用",大大提升了代码的可读性和易用性。
|use std::ops::Deref; struct Wrapper(String); impl Deref for Wrapper { type Target = String; fn deref(&self) -> &Self::Target { &self.0 } } impl
|HELLO
当存在多层智能指针时(如 Box<Box<String>>),方法解析会沿着 Deref 链一路找到底层类型的方法:
|fn main() { let s = Box::new(Box::new(String::from("hi"))); println!("{}", s.len()); // 等价于 ((&**s).len()) }
在复杂的 Rust 项目中,我们经常会遇到这样的情况:一个类型同时实现了多个 trait,而这些 trait 中恰好定义了同名的方法。这种情况下,如果我们直接调用方法,编译器就无法确定我们想要调用的是哪个 trait 的实现,从而产生歧义错误。
为了解决这个问题,Rust 提供了完全限定语法(Fully Qualified Syntax,也称为 UFCS - Universal Function Call Syntax)。这种语法允许我们明确指定要调用哪个 trait 的方法实现,从而消除歧义。
完全限定语法的基本形式是:<Type as Trait>::method()。通过这种方式,我们可以精确地告诉编译器我们想要调用的是哪个特定 trait 的方法实现,而不是依赖编译器的自动推断。
|trait A { fn name(&self) -> &'static str; } trait B { fn name(&self) -> &'static str; } struct T; impl A for T { fn name(&self)
|A B
Rust 的方法解析遵循严格的优先级顺序,理解这个顺序对于编写清晰、无歧义的代码至关重要。
方法解析的优先级顺序:
use 引入 traitDisplay、Debug 等预导入的 trait当编译器遇到方法调用时,它会按照这个顺序进行查找。如果在某个级别找到了匹配的方法,就会停止查找,不会继续到下一个级别。
9. 关联函数和构造器练习
为Point结构体实现关联函数:
new(x, y):使用x和y坐标创建点origin():创建原点(0.0, 0.0)from_polar(r, theta):从极坐标创建点(r是半径,theta是角度,单位:弧度)|#[derive(Debug, PartialEq)] struct Point { x: f64, y: f64 } impl Point { // 使用坐标创建点 fn new(x: f64, y: f64) -> Self { Self { x, y } } // 创建原点 fn origin() ->
10. 借用返回vs拷贝返回练习
为User结构体实现两个方法,体会借用和拷贝的区别:
name(&self) -> &str:返回字符串的借用,不分配新内存to_name(&self) -> String:返回字符串的拷贝,获得所有权|struct User { name: String } impl User { // 返回借用:不分配内存,适合临时使用 fn name(&self) -> &str { &self.name } // 返回拷贝:分配新内存,适合需要长期持有 fn to_name(&self) -> String { self.name
11. 可变方法和不变式练习
实现Counter结构体的方法:
tick(&mut self):将当前值增加步长set_step(&mut self, step: i32) -> bool:设置步长,但必须保证step > 0step <= 0,不修改步长并返回falsestep > 0,修改步长并返回trueget_current(&self)方法获取当前值|struct Counter { cur: i32, step: i32 } impl Counter { // 创建计数器 fn new(initial: i32, step: i32) -> Self { Self { cur: initial, step } } // 增加一步 fn tick(
12. 消费self的方法练习
为Config结构体实现into_parts方法:
into_parts(self) -> (String, u16)|#[derive(Debug)] struct Config { host: String, port: u16 } impl Config { // 创建配置 fn new(host: String, port: u16) -> Self { Self { host, port } } // 消费self,返回所有字段 fn into_parts
13. 构建器模式和链式调用练习
实现ServerBuilder构建器:
addr(addr: String):设置地址,返回Self以支持链式调用port(port: u16):设置端口,返回Self以支持链式调用build(self) -> Result<Server, String>:构建Server,验证必填字段addr或port为None,返回错误信息Ok(Server)|#[derive(Debug)] struct Server { addr: String, port: u16 } #[derive(Default)] struct ServerBuilder { addr: Option<String>, port: Option<u16> } impl ServerBuilder { // 设置地址(链式调用)
14. 枚举方法封装匹配练习
为Shape枚举实现方法,封装内部的模式匹配:
perimeter(&self) -> f64:计算周长
scale(&mut self, k: f64):按比例缩放
|use std::f64::consts::PI; enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 } } impl Shape { // 计算周长 fn perimeter(&self) -> f64 { match
15. Deref自动解引用练习
为Wrapper结构体实现Deref trait,演示自动解引用:
Deref<Target = String>main中对Box<Wrapper>直接调用String的方法|use std::ops::Deref; struct Wrapper(String); impl Wrapper { fn new(s: impl Into<String>) -> Self { Self(s.into()) } } impl Deref for Wrapper {
|点1: Point { x: 3.0, y: 4.0 } 原点: Point { x: 0.0, y: 0.0 } 极坐标点: Point { x: 3.5355339059327378, y: 3.5355339059327378 } 验证: p3 ≈ Point { x: 3.5355339059327378, y: 3.5355339059327378 }
说明:
new()是最常见的构造器,接受参数创建对象origin()是工厂方法,创建特殊值from_polar()是转换构造器,从另一种表示创建对象Type::function()调用,不需要实例|借用: 张三 可以继续使用user: "张三" 拷贝: 张三 可以继续使用user: "张三" 长期持有: 李四
说明:
name()返回&str,是借用,不分配内存,但受生命周期限制to_name()返回String,是拷贝,分配内存,但获得独立所有权|初始值: 0, 步长: 2 tick两次后: 4 设置步长为5: true, 新步长: 5 tick后: 9 设置步长为-1: false, 步长保持: 5 tick后(步长未变): 14
说明:
&mut self允许修改结构体字段set_step方法维护不变式:步长必须大于0|配置: Config { host: "localhost", port: 8080 } 解构后 - 主机: localhost, 端口: 8080 config已被消费,无法再使用
说明:
self作为接收者会移动所有权into_parts消费对象,返回其内部字段build()方法|成功: Ok(Server { addr: "0.0.0.0", port: 3000 }) 失败: Err("端口未设置") 成功: Ok(Server { addr: "localhost", port: 8080 })
说明:
Self实现build()方法消费构建器,进行最终验证Result处理构建失败的情况|圆形 - 半径: 5.0 周长: 31.42 面积: 78.54 缩放2倍后: 周长: 62.83 面积: 314.16 矩形 - 宽: 3.0, 高: 4.0 周长: 14.00 面积: 12.00 缩放1.5倍后: 周长: 21.00 面积: 27.00
说明:
perimeter使用&self,只读访问scale使用&mut self,需要修改枚举变体的字段match处理不同的变体|长度: 10 大写: HELLO RUST 包含'rust': true 多层指针: 长度: 12 大写: 多层指针 手动解引用(等价): 长度: 10 长度: 10
说明:
Deref trait使包装类型可以自动解引用到目标类型Deref实现,进行解引用转换Box<Box<Wrapper>>)