主题
Rust struct 与 impl 完全理解|关联函数、方法与“构造函数”
本文基于一段完整可运行代码,系统梳理 Rust 中
struct、impl、关联函数、方法以及“构造函数”的核心概念。
目标:一次搞清楚,之后只看这一篇就能想明白。
一、示例代码(全文)
rust
struct Product {
id: u32,
name: String,
price: f32,
}
impl Product {
fn new(name: String, price: f32) -> Product {
Product { id: 1, name, price }
}
fn get_default_sales_tax() -> f32 {
0.1
}
fn calculate_price(&self) -> f32 {
self.price * Product::get_default_sales_tax()
}
fn set_price(&mut self, price: f32) {
self.price = price;
}
fn get_price(&self) -> f32 {
self.price
}
fn buy(self) -> u32 {
let name = self.name;
println!("buy {}", name);
123
}
}
fn main() {
let mut book = Product {
id: 1,
name: String::from("Book"),
price: 10.0,
};
println!("id {}", book.id);
println!("name {}", book.name);
let price = book.price;
book.price = 15.0;
println!("price {}", price);
let price_sales = calculate_price(&book);
println!("price sales {}", price_sales);
let price_sales1 = book.calculate_price();
println!("price sales1 {}", price_sales1);
book.set_price(20.0);
let price = book.get_price();
println!("price {}", price);
let order_id = book.buy();
println!("order id {}", order_id);
let sales_tax = Product::get_default_sales_tax();
println!("sales tax {}", sales_tax);
let book1 = Product::new(String::from("Book1"), 10.0);
println!("book1-id {}", book1.id);
println!("book1-name {}", book1.name);
println!("book1-price {}", book1.price);
}
fn calculate_price(product: &Product) -> f32 {
product.price * 0.1
}二、struct:先搞清楚“数据长什么样”
rust
struct Product {
id: u32,
name: String,
price: f32,
}可以把 struct Product 理解为一块数据的形状定义:
Product是一个自定义类型- 内部有三个字段:
id: u32name: Stringprice: f32
和其他语言对比:
- 类似 C 里的
struct - 类似 Java/C#/C++ 里“没有方法、只有字段的类”
但有一个关键点:
- 在 Rust 里,struct 只负责存数据
- 行为(方法)是通过后面的
impl单独“贴”上去的
三、impl:给类型“装上行为”
rust
impl Product {
fn new(name: String, price: f32) -> Product {
Product { id: 1, name, price }
}
fn get_default_sales_tax() -> f32 {
0.1
}
fn calculate_price(&self) -> f32 {
self.price * Product::get_default_sales_tax()
}
fn set_price(&mut self, price: f32) {
self.price = price;
}
fn get_price(&self) -> f32 {
self.price
}
fn buy(self) -> u32 {
let name = self.name;
println!("buy {}", name);
123
}
}impl Product { ... } 的语义是:
为类型 Product 实现一组函数。
这些函数分成两类:
- 带
self/&self/&mut self的叫方法(method) - 不带
self的叫关联函数(associated function)
调用方式:
- 关联函数:
Product::new(...)、Product::get_default_sales_tax() - 方法:
book.calculate_price()、book.set_price(20.0)
四、关联函数 vs 方法:有没有 self 是分水岭
在这段代码里:
rust
impl Product {
fn new(name: String, price: f32) -> Product {
Product { id: 1, name, price }
}
fn get_default_sales_tax() -> f32 {
0.1
}
fn calculate_price(&self) -> f32 {
self.price * Product::get_default_sales_tax()
}
fn set_price(&mut self, price: f32) {
self.price = price;
}
fn get_price(&self) -> f32 {
self.price
}
fn buy(self) -> u32 {
let name = self.name;
println!("buy {}", name);
123
}
}关联函数(没有 self 参数)
fn new(name: String, price: f32) -> Productfn get_default_sales_tax() -> f32- 特点:
- 调用方式:
Product::new(...)、Product::get_default_sales_tax() - 不依赖具体实例
- 无法访问
self.id、self.price等字段
- 调用方式:
方法(第一个参数是 self / &self / &mut self)
fn calculate_price(&self) -> f32fn set_price(&mut self, price: f32)fn get_price(&self) -> f32fn buy(self) -> u32- 特点:
- 调用方式:
book.calculate_price()、book.set_price(20.0) - 能访问字段:
self.id、self.price等 - 编译器会自动把
book.calculate_price()翻译成Product::calculate_price(&book)
- 调用方式:
一句话概括:
看函数签名里有没有
self参数,有就是方法,没有就是关联函数。
五、self / &self / &mut self / Self:背后是所有权语义
方法的第一个参数有三种常见写法:
&self等价于self: &Self&mut self等价于self: &mut Selfself等价于self: Self
Self 是类型自身的别名,这里就是 Product。
对应到代码中:
1️⃣ &self:只读借用
rust
fn calculate_price(&self) -> f32 {
self.price * Product::get_default_sales_tax()
}含义:
- 方法接收
&Product,即对实例的不可变借用 - 可以读字段:
self.price - 不能改字段
- 调用结束后,借用结束,外部变量还能继续用
2️⃣ &mut self:可变借用
rust
fn set_price(&mut self, price: f32) {
self.price = price;
}含义:
- 方法接收
&mut Product,即对实例的可变借用 - 可以修改字段:
self.price = price - 调用时要求外部变量本身是
mut - 调用期间不能有其他对同一实例的借用
3️⃣ self:拿走所有权
rust
fn buy(self) -> u32 {
let name = self.name;
println!("buy {}", name);
123
}含义:
- 方法接收的是
Product本身,即直接拿走所有权 - 调用
book.buy()后,book在外面就不能再用了 - 非常适合“一次性消费掉对象”的场景
六、“构造函数”:Rust 没有关键字,但有约定
Rust 没有 constructor 关键字,但社区有非常稳定的约定:
rust
impl Product {
fn new(name: String, price: f32) -> Product {
Product { id: 1, name, price }
}
}这是一个典型的“构造函数”:
- 写在
impl里 - 不带
self→ 关联函数 - 名字叫
new - 返回类型是
Product(也可以写Self)
调用方式:
rust
let book1 = Product::new(String::from("Book1"), 10.0);对比字面量构造:
rust
let mut book = Product {
id: 1,
name: String::from("Book"),
price: 10.0,
};两种方式的取舍:
- 字面量:
- 自由、直观
- 所有字段都得显式写出来
new构造函数:- 可以隐藏内部细节(比如
id自动生成) - 可以做参数校验、默认值填充等逻辑
- 给外部一个统一、安全的创建入口
- 可以隐藏内部细节(比如
在实际项目中,对外暴露的类型推荐总是提供一个 pub fn new(...) -> Self。
七、自由函数 vs 方法:同一逻辑,两种组织方式
文件结尾的自由函数:
rust
fn calculate_price(product: &Product) -> f32 {
product.price * 0.1
}和 impl 里的方法:
rust
fn calculate_price(&self) -> f32 {
self.price * Product::get_default_sales_tax()
}在 main 里被同时调用:
rust
let price_sales = calculate_price(&book);
println!("price sales {}", price_sales);
let price_sales1 = book.calculate_price();
println!("price sales1 {}", price_sales1);对比:
自由函数:
- 定义在模块级别
- 调用:
calculate_price(&book) - 参数显式写出
&Product - 更偏“函数式”风格
方法:
- 定义在
impl Product里 - 调用:
book.calculate_price() - 编译器自动加上
&book - 更偏“面向对象”风格
- 定义在
选择建议:
- 如果逻辑很明显是这个类型的行为,适合作为方法;
- 如果逻辑更通用,或者将来可能作用在多种类型上,可以作为自由函数。
八、main 中的调用顺序:从所有权视角重新走一遍
挑出与 Product 相关的部分:
rust
let mut book = Product {
id: 1,
name: String::from("Book"),
price: 10.0,
};
println!("id {}", book.id);
println!("name {}", book.name);
let price = book.price;
book.price = 15.0;
println!("price {}", price);
let price_sales = calculate_price(&book);
println!("price sales {}", price_sales);
let price_sales1 = book.calculate_price();
println!("price sales1 {}", price_sales1);
book.set_price(20.0);
let price = book.get_price();
println!("price {}", price);
let order_id = book.buy();
println!("order id {}", order_id);按所有权/借用分析:
let mut book = Product { ... };book拥有一个完整的Product
访问字段和修改字段:
rustprintln!("id {}", book.id); println!("name {}", book.name); let price = book.price; book.price = 15.0; println!("price {}", price);u32、f32是Copy类型,let price = book.price;会复制一份数值book的所有权不受影响
不可变借用给自由函数:
rustlet price_sales = calculate_price(&book);- 将
&book传给fn calculate_price(product: &Product) - 只是借用,不夺走所有权
- 将
调用只读方法:
rustlet price_sales1 = book.calculate_price();- 等价于
Product::calculate_price(&book) - 同样是不可变借用
- 等价于
调用可变方法:
rustbook.set_price(20.0); let price = book.get_price();set_price需要&mut book- 调用期间不能有其他对
book的借用
最后消费掉对象:
rustlet order_id = book.buy();- 等价于
Product::buy(book) - 所有权移动进方法内部
- 方法内部可以自由“拆解”这个
Product,比如把name拿出来 - 调用之后,
book在外部就失效了
- 等价于
九、元组与元组结构体:另一种定义数据的方式
如果我们扩展 main,加入下面代码:
rust
let color1 = (255, 123, 167);
println!("color1-red {}", color1.0);
println!("color1-green {}", color1.1);
println!("color1-blue {}", color1.2);
let mut color2 = (0, 255, 0, 100);
println!("color2-red {}", color2.0);
println!("color2-green {}", color2.1);
println!("color2-blue {}", color2.2);
println!("color2-alpha {}", color2.3);
color2.3 = 200;
println!("color2-alpha {}", color2.3);
struct RGB(i32, i32, i32);
struct CMYK(i32, i32, i32, i32);
let color3 = RGB(255, 0, 0);
println!("color3-red {}", color3.0);
println!("color3-green {}", color3.1);
println!("color3-blue {}", color3.2);
let color4 = CMYK(0, 255, 0, 100);
println!("color4-cyan {}", color4.0);
println!("color4-magenta {}", color4.1);
println!("color4-yellow {}", color4.2);
println!("color4-black {}", color4.3);可以看到三种不同层次:
普通元组
(T1, T2, T3, ...)- 类型是匿名的
- 根据位置
.0 .1 .2访问
可变元组
let mut color2 = (...)- 可以修改具体位置上的值
元组结构体
struct RGB(i32, i32, i32);- 有名字的“元组类型”
- 数据结构和普通元组一样,但类型变成
RGB - 既能用
.0 .1 .2访问,又有更强的类型安全
和 struct Product { ... } 比较:
Product {}是具名字段结构体(字段名是id、name、price)RGB(...)是元组结构体(字段靠位置区分)
选择建议:
- 若字段语义明显不同,推荐具名字段结构体;
- 若字段只是单纯组合、顺序很重要,且用途局部,可以考虑元组结构体。
十、一句话总记忆
Rust 把“数据”和“行为”拆开:
struct只描述数据形状,impl把行为贴到类型上。
再补几条关键记忆点:
- struct
- 定义字段的集合,是数据的“形状”
- impl
- 给类型增加行为(方法 + 关联函数)
- 关联函数
- 不带
self,通过Type::func()调用 - 典型用法是
fn new(...) -> Self
- 不带
- 方法
- 第一个参数是
self/&self/&mut self - 和所有权系统结合得非常紧密
- 第一个参数是
- 构造函数
- 没有关键字,社区约定使用
fn new(...) -> Self
- 没有关键字,社区约定使用
- 自由函数 vs 方法
- 自由函数:模块级别,参数显式
- 方法:挂在类型上,更贴近“这个类型的行为”
✅ 这篇文章可以作为你理解 Rust struct / impl / 方法体系的长期复习入口。
十一、多个 impl 块:按功能拆分代码
在实际项目里,一个类型往往会很大。Rust 允许你为同一个类型写多个 impl 块:
rust
struct Product {
id: u32,
name: String,
price: f32,
}
impl Product {
fn new(name: String, price: f32) -> Self {
Self { id: 1, name, price }
}
}
impl Product {
fn price_with_tax(&self) -> f32 {
self.price * 1.1
}
fn rename(&mut self, name: String) {
self.name = name;
}
}可以按“功能模块”拆分:
- 一个
impl专门放构造函数 - 一个
impl专门放与价格相关的方法 - 一个
impl专门放与名称相关的方法
大型项目里,这种拆分能显著提高可读性,配合模块和子模块更明显。
十二、pub 与 API 设计:哪些方法暴露给外部
目前示例里的方法都没有 pub,默认是模块私有:
rust
impl Product {
fn new(name: String, price: f32) -> Product { ... }
fn set_price(&mut self, price: f32) { ... }
}在真正的库/模块里,更常见的写法是:
rust
impl Product {
pub fn new(name: String, price: f32) -> Self {
Self { id: 1, name, price }
}
pub fn set_price(&mut self, price: f32) {
self.price = price;
}
pub fn price(&self) -> f32 {
self.price
}
}设计思路:
- 结构体字段可以是私有的(不加
pub) - 通过
pub fn对外暴露受控的访问接口 - 用方法来保证不变量,比如价格不能为负、名字不能为空
这是 Rust 推荐的封装方式:字段私有 + 公共 API 控制访问。
十三、trait impl vs 固有 impl:两套“扩展机制”
当前的 impl Product { ... } 叫做“固有 impl”(inherent impl):
- 不依赖任何 trait
- 直接给类型自己增加方法
Rust 还有另一种形式:
rust
trait Describable {
fn describe(&self) -> String;
}
impl Describable for Product {
fn describe(&self) -> String {
format!("Product({}, {}, {})", self.id, self.name, self.price)
}
}两者区别:
impl Product { ... }- 只能给自己定义的方法
- 调用方式:
book.calculate_price()
impl Trait for Product { ... }- 实现某个 trait 的要求
- 调用方式:
book.describe()(前提是当前作用域看得到这个 trait) - 让
Product可以被泛型/框架使用,比如T: Describable
在 API 设计上:
- 类型自带的核心语义 → 放到固有
impl里 - 和某个“协议/接口”有关的行为 → 放到
impl Trait for Type里
十四、方法调用语法糖与 UFCS
Rust 的方法调用本质上是语法糖。
例如:
rust
book.calculate_price();编译器会自动 desugar 成:
rust
Product::calculate_price(&book);这背后有两个重要概念:
- 自动借用 / 自动引用解引用
- UFCS(统一函数调用语法)
UFCS 写法类似:
rust
Product::set_price(&mut book, 20.0);特点:
- 明确指出调用的是哪个类型的哪个方法
- 在多种 trait/类型都提供同名方法时,更清晰
在日常使用中:
- 普通代码 → 用
book.method()即可 - 需要消除歧义、或在 trait 较多场景 → 使用
Type::method(&value, ...)
十五、“构造函数家族”:不仅仅是 new
真实项目里,构造函数往往不止一个:
rust
impl Product {
pub fn new(name: String, price: f32) -> Self {
Self { id: 1, name, price }
}
pub fn free_sample(name: String) -> Self {
Self { id: 0, name, price: 0.0 }
}
pub fn with_id(id: u32, name: String, price: f32) -> Self {
Self { id, name, price }
}
}命名习惯:
new:最常用、语义最朴素的构造函数- 其他构造函数在
new基础上加语义前缀:from_xxx:从其他类型转换而来with_xxx:在new基础上多指定一些配置default:实现Defaulttrait 后的零值构造
好处:
- 调用点即文档:
Product::free_sample("Book".into())一眼知道做什么 - 避免
new(true, false, 3)这种“布尔地狱”构造方式
十六、所有权模式小结:把 self 当成第一维度
回到最核心的三个签名:
rust
fn foo(&self, ...) // 只读借用
fn bar(&mut self, ...) // 可写借用
fn baz(self, ...) // 消费所有权可以形成一个思考模板:
- 如果方法只是“读数据、算个结果”,用
&self - 如果方法要“修改内部状态”,用
&mut self - 如果方法表示“生命周期的终结/消费”,用
self
用这个模板审视 API 设计:
calculate_price:只是计算 →&selfset_price:修改价格 →&mut selfbuy:提交订单后,这个Product实例就不再有意义 →self
这样设计出来的方法与所有权系统是统一的,读者也更容易理解。