Skip to content

Rust struct 与 impl 完全理解|关联函数、方法与“构造函数”

本文基于一段完整可运行代码,系统梳理 Rust 中
structimpl、关联函数、方法以及“构造函数”的核心概念。
目标:一次搞清楚,之后只看这一篇就能想明白。


一、示例代码(全文)

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: u32
    • name: String
    • price: 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) -> Product
    • fn get_default_sales_tax() -> f32
    • 特点:
      • 调用方式:Product::new(...)Product::get_default_sales_tax()
      • 不依赖具体实例
      • 无法访问 self.idself.price 等字段
  • 方法(第一个参数是 self / &self / &mut self)

    • fn calculate_price(&self) -> f32
    • fn set_price(&mut self, price: f32)
    • fn get_price(&self) -> f32
    • fn buy(self) -> u32
    • 特点:
      • 调用方式:book.calculate_price()book.set_price(20.0)
      • 能访问字段:self.idself.price
      • 编译器会自动把 book.calculate_price() 翻译成 Product::calculate_price(&book)

一句话概括:

看函数签名里有没有 self 参数,有就是方法,没有就是关联函数。


五、self / &self / &mut self / Self:背后是所有权语义

方法的第一个参数有三种常见写法:

  • &self 等价于 self: &Self
  • &mut self 等价于 self: &mut Self
  • self 等价于 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);

按所有权/借用分析:

  1. let mut book = Product { ... };

    • book 拥有一个完整的 Product
  2. 访问字段和修改字段:

    rust
    println!("id {}", book.id);
    println!("name {}", book.name);
    
    let price = book.price;
    book.price = 15.0;
    println!("price {}", price);
    • u32f32Copy 类型,let price = book.price; 会复制一份数值
    • book 的所有权不受影响
  3. 不可变借用给自由函数:

    rust
    let price_sales = calculate_price(&book);
    • &book 传给 fn calculate_price(product: &Product)
    • 只是借用,不夺走所有权
  4. 调用只读方法:

    rust
    let price_sales1 = book.calculate_price();
    • 等价于 Product::calculate_price(&book)
    • 同样是不可变借用
  5. 调用可变方法:

    rust
    book.set_price(20.0);
    let price = book.get_price();
    • set_price 需要 &mut book
    • 调用期间不能有其他对 book 的借用
  6. 最后消费掉对象:

    rust
    let 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 {}具名字段结构体(字段名是 idnameprice
  • 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:实现 Default trait 后的零值构造

好处:

  • 调用点即文档: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:只是计算 → &self
  • set_price:修改价格 → &mut self
  • buy:提交订单后,这个 Product 实例就不再有意义 → self

这样设计出来的方法与所有权系统是统一的,读者也更容易理解。