Rust impl详解:为Struct定义方法与关联函数

《Rust编程实战》系列第42篇

上一篇文章中,我们学习了Rust的Struct结构体,已经知道如何定义:

rust 复制代码
struct User {
    id: u64,
    name: String,
    age: u32,
}

Struct主要负责描述:

一个对象拥有哪些数据。

但真实项目中的对象通常不仅有数据,还会拥有与这些数据相关的行为。

例如一个用户可能需要:

  • 获取用户名

  • 修改用户名

  • 判断是否成年

  • 增加积分

    一个商品可能需要:

  • 修改库存

  • 计算总价

  • 判断是否有库存

    一个订单可能需要:

  • 计算订单金额

  • 修改订单状态

  • 判断是否已经完成

    如果把这些功能全部写成普通函数:

php 复制代码
fn print_user(user: &User) {}
fn update_user(user: &mut User) {}
fn user_is_adult(user: &User) -> bool {}

代码当然可以运行,但随着项目变大,数据和行为会逐渐分散。

Rust提供:

rust 复制代码
impl

用于为Struct定义属于这个类型的方法和关联函数。

本文将介绍:

  • 什么是impl

  • impl基本语法

  • 方法与普通函数的区别

  • &self

  • &mut self

  • self

  • Self

  • 关联函数

  • new()构造模式

  • Getter方法

  • 一个Struct多个impl

  • 自动借用与解引用

  • 链式调用

  • 用户、商品与订单实战

什么是impl

impl是implementation的缩写,可以理解为:

复制代码
为某个类型实现功能

例如定义一个User:

rust 复制代码
struct User {
    name: String,
    age: u32,
}

可以使用:

rust 复制代码
impl User {
}

为User添加行为。

完整示例:

rust 复制代码
struct User {
    name: String,
    age: u32,
}
impl User {
    fn print(&self) {
        println!("姓名:{}", self.name);
        println!("年龄:{}", self.age);
    }
}
fn main() {
    let user = User {
        name: String::from("Tom"),
        age: 20,
    };
    user.print();
}

调用方式:

go 复制代码
user.print();

这种定义在impl中,并且第一个参数为self相关类型的函数,通常称为:

sql 复制代码
Method
方法

impl基本语法

基本结构:

rust 复制代码
impl 类型名称 {
    // 方法
    // 关联函数
}

例如:

rust 复制代码
struct Product {
    name: String,
    price: f64,
}
impl Product {
    fn print(&self) {
        println!("{} {}", self.name, self.price);
    }
}

这里:

rust 复制代码
impl Product

表示下面代码块中的功能属于:

复制代码
Product

类型。

方法与普通函数的区别

普通函数:

arduino 复制代码
fn print_user(user: &User) {
    println!("{}", user.name);
}

调用:

scss 复制代码
print_user(&user);

方法:

rust 复制代码
impl User {
    fn print(&self) {
        println!("{}", self.name);
    }
}

调用:

go 复制代码
user.print();

两种方式都可以实现功能,但方法能够明确表达:

这个行为属于User。

因此面向业务对象的逻辑通常更适合放入impl

什么是self

方法中的:

lua 复制代码
self

表示当前正在操作的对象。

例如:

rust 复制代码
impl User {
    fn print(&self) {
        println!("{}", self.name);
    }
}

调用:

go 复制代码
user.print();

那么方法中的:

lua 复制代码
self

就代表:

sql 复制代码
user

因此:

lua 复制代码
self.name

等价于访问当前User的name字段。

&self:不可变借用当前对象

最常见的方法参数是:

lua 复制代码
&self

例如:

rust 复制代码
impl User {
    fn name(&self) -> &str {
        &self.name
    }
}

完整示例:

rust 复制代码
struct User {
    name: String,
    age: u32,
}
impl User {
    fn name(&self) -> &str {
        &self.name
    }
    fn is_adult(&self) -> bool {
        self.age >= 18
    }
}
fn main() {
    let user = User {
        name: String::from("Tom"),
        age: 20,
    };
    println!("{}", user.name());
    println!("{}", user.is_adult());
}

&self表示:

方法只借用当前对象,不取得所有权,也不能修改对象。

调用之后:

sql 复制代码
user

仍然可以继续使用。

&mut self:可变借用当前对象

如果方法需要修改结构体字段,可以使用:

rust 复制代码
&mut self

例如:

rust 复制代码
impl User {
    fn set_age(&mut self, age: u32) {
        self.age = age;
    }
}

调用者必须是可变变量:

rust 复制代码
fn main() {
    let mut user = User {
        name: String::from("Tom"),
        age: 20,
    };
    user.set_age(21);
    println!("{}", user.age);
}

这里:

ini 复制代码
user.set_age(21);

会临时可变借用user

等价于方法获得:

rust 复制代码
&mut User

self:取得当前对象所有权

除了:

rust 复制代码
&self
&mut self

还可以直接写:

lua 复制代码
self

例如:

rust 复制代码
impl User {
    fn delete(self) {
        println!("删除用户:{}", self.name);
    }
}

调用:

go 复制代码
user.delete();

之后:

sql 复制代码
user

通常不能继续使用,因为对象所有权已经移动进方法。

例如:

csharp 复制代码
fn main() {
    let user = User {
        name: String::from("Tom"),
        age: 20,
    };
    user.delete();
    // println!("{}", user.name);
}

最后一行会报错。

因此可以记住:

rust 复制代码
&self
只读借用
&mut self
可变借用
self
取得所有权

Self是什么

Rust中:

rust 复制代码
Self

代表当前正在实现的类型。

例如:

rust 复制代码
impl User {
    fn new(
        name: String,
        age: u32,
    ) -> Self {
        Self {
            name,
            age,
        }
    }
}

这里:

rust 复制代码
Self

等价于:

sql 复制代码
User

所以也可以写成:

rust 复制代码
fn new(
    name: String,
    age: u32,
) -> User {
    User {
        name,
        age,
    }
}

但在impl内部通常更推荐使用:

rust 复制代码
Self

这样如果以后修改类型名称,代码更加容易维护。

关联函数

impl中不接收:

lua 复制代码
self

参数的函数称为:

javascript 复制代码
Associated Function
关联函数

例如:

rust 复制代码
impl User {
    fn new(name: String, age: u32) -> Self {
        Self {
            name,
            age,
        }
    }
}

调用时不能写:

csharp 复制代码
user.new(...)

而应该使用:

arduino 复制代码
User::new(...)

完整示例:

rust 复制代码
struct User {
    name: String,
    age: u32,
}
impl User {
    fn new(name: &str, age: u32) -> Self {
        Self {
            name: name.to_string(),
            age,
        }
    }
}
fn main() {
    let user = User::new("Tom", 20);
    println!("{}", user.name);
}

new并不是Rust关键字

需要注意:

scss 复制代码
new()

并不是Rust规定必须存在的构造函数,也不是关键字。

下面都可以:

sql 复制代码
User::new()
User::create()
User::from_name()
User::default_user()

只是Rust社区普遍习惯使用:

scss 复制代码
new()

表示创建新对象。

例如:

php 复制代码
Vec::new()
String::new()
HashMap::new()

都是类似设计。

多个关联函数

一个类型可以定义多个创建函数:

rust 复制代码
impl User {
    fn new(
        name: &str,
        age: u32,
    ) -> Self {
        Self {
            name: name.to_string(),
            age,
        }
    }
    fn guest() -> Self {
        Self {
            name: String::from("Guest"),
            age: 0,
        }
    }
}

调用:

ini 复制代码
let first = User::new("Tom", 20);
let second = User::guest();

这可以让创建对象的逻辑更加集中。

Getter方法

有些字段不希望直接暴露,可以把字段定义为私有,然后提供方法访问。

例如:

rust 复制代码
pub struct User {
    name: String,
    age: u32,
}
impl User {
    pub fn new(
        name: String,
        age: u32,
    ) -> Self {
        Self {
            name,
            age,
        }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn age(&self) -> u32 {
        self.age
    }
}

外部代码无法直接修改:

复制代码
user.name

但可以调用:

scss 复制代码
user.name()

这种方式可以保护内部数据。

Setter是否应该大量使用

可以写:

rust 复制代码
impl User {
    fn set_name(&mut self, name: String) {
        self.name = name;
    }
}

但在实际业务中,不建议机械地为所有字段生成Getter和Setter。

例如订单状态从"未支付"变成"已支付",与其:

scss 复制代码
order.set_status(...)

更好的设计可能是:

scss 复制代码
order.pay();

这样方法表达的是业务行为,而不是简单字段修改。

一个Struct可以有多个impl

Rust允许:

rust 复制代码
impl User {
    fn new() {}
}
impl User {
    fn print(&self) {}
}
impl User {
    fn update(&mut self) {}
}

多个impl块最终都会为同一个类型增加功能。

通常简单类型放在一个impl中即可。

大型项目中,可以根据:

  • 构造逻辑

  • 查询方法

  • 业务操作

    进行适当拆分。

方法可以接收其他参数

方法除了self之外,还可以拥有普通参数。

例如:

rust 复制代码
impl Product {
    fn total_price(
        &self,
        count: u32,
    ) -> f64 {
        self.price * count as f64
    }
}

调用:

ini 复制代码
let total = product.total_price(3);

这里:

lua 复制代码
&self

是当前Product。

复制代码
count

是额外参数。

方法可以返回Result

真实业务方法经常需要错误处理。

例如商品扣库存:

rust 复制代码
#[derive(Debug)]
struct Product {
    name: String,
    stock: u32,
}
impl Product {
    fn buy(
        &mut self,
        count: u32,
    ) -> Result<(), String> {
        if count > self.stock {
            return Err(
                String::from("库存不足")
            );
        }
        self.stock -= count;
        Ok(())
    }
}

调用:

rust 复制代码
fn main() {
    let mut product = Product {
        name: String::from("Rust Book"),
        stock: 10,
    };
    match product.buy(3) {
        Ok(()) => println!("购买成功"),
        Err(error) => println!("{}", error),
    }
    println!("剩余库存:{}", product.stock);
}

这已经非常接近真实业务对象的写法。

自动借用

假设方法定义:

php 复制代码
fn print(&self)

正常来说,它需要:

sql 复制代码
&User

但调用时通常直接:

go 复制代码
user.print();

不需要:

scss 复制代码
(&user).print();

Rust会自动完成借用。

同样:

php 复制代码
fn update(&mut self)

调用:

ini 复制代码
user.update();

Rust会根据方法签名自动创建:

rust 复制代码
&mut user

这种机制让方法调用非常自然。

自动解引用

如果拥有引用:

ini 复制代码
let user_ref = &user;

依然可以:

go 复制代码
user_ref.print();

而不需要手动写:

scss 复制代码
(*user_ref).print();

Rust在方法调用时会进行自动借用和自动解引用,这通常称为:

sql 复制代码
Automatic Referencing and Dereferencing

因此方法调用比普通函数更加简洁。

方法返回self

有些方法修改对象后返回自身,可以用于链式调用。

例如:

rust 复制代码
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}
impl User {
    fn new() -> Self {
        Self {
            name: String::new(),
            age: 0,
        }
    }
    fn with_name(
        mut self,
        name: &str,
    ) -> Self {
        self.name = name.to_string();
        self
    }
    fn with_age(
        mut self,
        age: u32,
    ) -> Self {
        self.age = age;
        self
    }
}

调用:

css 复制代码
let user = User::new()
    .with_name("Tom")
    .with_age(20);

这种写法常见于:

sql 复制代码
Builder Pattern
构建器模式

&mut self链式调用

也可以返回:

rust 复制代码
&mut Self

例如:

rust 复制代码
impl User {
    fn set_name(
        &mut self,
        name: &str,
    ) -> &mut Self {
        self.name = name.to_string();
        self
    }
    fn set_age(
        &mut self,
        age: u32,
    ) -> &mut Self {
        self.age = age;
        self
    }
}

调用:

sql 复制代码
let mut user = User::new();
user
    .set_name("Tom")
    .set_age(20);

这里不会消费user,而是连续可变借用。

方法和字段同名

Rust允许方法名与字段名相同。

例如:

rust 复制代码
struct User {
    name: String,
}
impl User {
    fn name(&self) -> &str {
        &self.name
    }
}

访问字段:

复制代码
user.name

调用方法:

scss 复制代码
user.name()

有没有括号就是区分方式。

这种Getter写法非常常见。

实战:商品impl

rust 复制代码
#[derive(Debug)]
struct Product {
    id: u64,
    name: String,
    price: f64,
    stock: u32,
}
impl Product {
    fn new(
        id: u64,
        name: &str,
        price: f64,
    ) -> Self {
        Self {
            id,
            name: name.to_string(),
            price,
            stock: 0,
        }
    }
    fn add_stock(
        &mut self,
        count: u32,
    ) {
        self.stock += count;
    }
    fn has_stock(&self) -> bool {
        self.stock > 0
    }
    fn total_price(
        &self,
        count: u32,
    ) -> f64 {
        self.price * count as f64
    }
}
fn main() {
    let mut product =
        Product::new(
            1,
            "Rust Book",
            99.0,
        );
    product.add_stock(100);
    println!("有库存:{}", product.has_stock());
    println!(
        "3本价格:{}",
        product.total_price(3)
    );
}

实战:订单impl

rust 复制代码
#[derive(Debug, PartialEq)]
enum OrderStatus {
    Pending,
    Paid,
    Finished,
}
#[derive(Debug)]
struct Order {
    id: u64,
    amount: f64,
    status: OrderStatus,
}
impl Order {
    fn new(
        id: u64,
        amount: f64,
    ) -> Self {
        Self {
            id,
            amount,
            status: OrderStatus::Pending,
        }
    }
    fn pay(&mut self) {
        self.status = OrderStatus::Paid;
    }
    fn finish(&mut self) {
        self.status = OrderStatus::Finished;
    }
    fn is_finished(&self) -> bool {
        self.status == OrderStatus::Finished
    }
}

调用:

css 复制代码
fn main() {
    let mut order =
        Order::new(1001, 299.0);
    order.pay();
    order.finish();
    println!("{:?}", order);
    println!(
        "是否完成:{}",
        order.is_finished()
    );
}

这种设计把:

diff 复制代码
订单数据
+
订单行为

统一组织在一起。

impl与封装

Struct配合impl可以实现基本封装。

例如:

rust 复制代码
pub struct Account {
    balance: f64,
}
impl Account {
    pub fn new() -> Self {
        Self {
            balance: 0.0,
        }
    }
    pub fn balance(&self) -> f64 {
        self.balance
    }
    pub fn deposit(
        &mut self,
        amount: f64,
    ) {
        if amount > 0.0 {
            self.balance += amount;
        }
    }
}

外部代码不能直接随意修改:

复制代码
balance

只能通过:

scss 复制代码
deposit()

改变余额。

这样可以把业务规则集中在类型内部。

常见错误

忘记给修改方法使用&mut self

错误:

php 复制代码
fn set_age(&self, age: u32) {
    self.age = age;
}

需要:

rust 复制代码
fn set_age(&mut self, age: u32) {
    self.age = age;
}

调用&mut self方法时变量没有mut

错误:

ini 复制代码
let user = User::new(...);
user.set_age(20);

应该:

ini 复制代码
let mut user = User::new(...);

不必要地使用self

如果方法只读取对象:

php 复制代码
fn print(self)

会消费整个对象。

更合适:

php 复制代码
fn print(&self)

把关联函数当方法调用

如果:

rust 复制代码
fn new() -> Self

没有self参数,应调用:

arduino 复制代码
User::new()

而不是:

csharp 复制代码
user.new()

所有逻辑都写在impl中

并不是任何函数都应该成为方法。

如果逻辑与某个类型没有明显关系,普通函数可能更加合理。

impl最佳实践

创建对象使用关联函数

例如:

php 复制代码
User::new()
Product::new()

只读方法使用&self

rust 复制代码
fn name(&self) -> &str

修改对象使用&mut self

php 复制代码
fn update(&mut self)

消费对象时使用self

例如:

rust 复制代码
fn into_string(self) -> String

这种into_风格方法经常表示取得对象所有权并转换。

方法名称体现业务行为

优先:

scss 复制代码
order.pay()
product.buy()
user.disable()

而不是大量:

scss 复制代码
set_status()
set_stock()
set_enabled()

不要过度设计

简单Struct不一定需要几十个Getter和Setter。方法应该真正帮助表达业务逻辑。

本章小结

impl用于为Rust类型增加方法和关联函数,是Struct从"数据结构"升级为"业务对象"的核心机制。

本文学习了:

  • impl基本语法

  • 方法与普通函数的区别

  • self表示当前对象

  • &self用于只读借用

  • &mut self用于修改对象

  • self用于取得对象所有权

  • Self代表当前类型

  • 不带self参数的是关联函数

  • new()是一种常见构造模式

  • Getter可以保护内部字段

  • 一个类型可以拥有多个impl块

  • Rust方法调用支持自动借用和自动解引用

  • 返回Self&mut Self可以实现链式调用

  • Struct与impl可以实现基本封装和业务建模

    可以记住:

Struct负责保存数据,impl负责定义行为。

&self读取自己,&mut self修改自己,self消费自己。

关联函数属于类型,方法属于具体对象。

下一篇预告

下一篇我们将继续深入Rust中的方法:

Rust 方法 Method详解:self、方法调用与API设计

内容包括:

  • 什么是方法

  • 方法调用完整过程

  • self&self&mut self

  • 自动借用与自动解引用

  • 方法参数设计

  • 方法返回引用

  • 方法返回Self

  • 链式调用

  • into_*as_*to_*命名习惯

  • Getter与业务方法的区别

  • 方法中的所有权与Move

  • 方法API设计最佳实践

相关推荐
程序员爱钓鱼40 分钟前
Go for 循环详解
后端·面试·go
weixin_431600441 小时前
NestJS 入门(9):连上数据库,SQL 写在哪?
数据库·后端·sql·学习·nest.js
用户059540174461 小时前
Qdrant 召回不一致踩坑实录:跑了 300 次测试才发现是索引没刷新
前端·css
k4m7v2pz1 小时前
Rust 程序在 ReactOS 上“加载即崩“:从崩溃地址 0x6107104d 到 CRT 依赖的深度排查
rust·交叉编译·逆向工程·reactos·pe格式·windows兼容
Highcharts.js1 小时前
Highcharts 主流前端框架无缝集成指南
前端·vue.js·前端框架·highcharts·可视化图表
qq_22589174661 小时前
基于Flask的城市地铁客流量数据预测系统设计与实现
后端·python·flask
JAVA面经实录9171 小时前
网络编程基础(Java Web/分布式前置·完整版)(十一)
java·前端·网络
IT_陈寒1 小时前
搞不定JavaScript的数组去重?你可能漏了这两个坑
前端·人工智能·后端
三8441 小时前
CSRF跨站请求伪造基础
前端·csrf