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

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

上一篇文章中,我们学习了Rust的impl,知道了如何为Struct定义方法和关联函数。本篇继续深入其中最常用的一部分:

sql 复制代码
Method

也就是"方法"。

在Rust中,方法与普通函数非常相似,但方法通常和某个类型绑定,并且第一个参数通常是:

rust 复制代码
self
&self
&mut self

例如:

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

调用:

ini 复制代码
let user = User {
    name: String::from("Tom"),
    age: 20,
};
user.print();

方法让数据和行为结合在一起,是Rust设计业务模型、库API和面向对象风格接口的重要工具。本文将重点介绍:

  • 什么是方法

  • 方法与普通函数区别

  • self&self&mut self

  • 方法参数设计

  • 自动借用与自动解引用

  • 方法返回值

  • 返回引用

  • 返回Self

  • 链式调用

  • 方法中的Move

  • into_*as_*to_*命名习惯

  • Getter与业务方法

  • 方法API设计最佳实践

什么是方法

方法本质上仍然是函数,只不过它定义在:

rust 复制代码
impl

代码块中,并且通常接收当前类型实例。

例如:

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

调用:

ini 复制代码
product.show();

而普通函数可能写成:

arduino 复制代码
fn show_product(product: &Product) {
    println!("{}:{}", product.name, product.price);
}

调用:

scss 复制代码
show_product(&product);

两者都能实现相同功能,但:

scss 复制代码
product.show()

更能表达"show这个行为属于Product"。

方法的基本语法

一个最基本的方法:

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

可以拆解为:

rust 复制代码
fn         定义函数
name       方法名称
&self      当前对象的不可变引用
-> &str    返回值类型

调用:

scss 复制代码
user.name()

方法和普通函数一样,也可以:

  • 接收多个参数

  • 返回值

  • 返回Result

  • 返回Option

  • 使用泛型

  • 使用生命周期

&self:只读取对象

如果方法只需要查看结构体数据,通常使用:

lua 复制代码
&self

例如:

rust 复制代码
struct User {
    name: String,
    age: u32,
}
impl User {
    fn is_adult(&self) -> bool {
        self.age >= 18
    }
    fn name(&self) -> &str {
        &self.name
    }
}

调用:

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

这里的方法不会取得user所有权,也不会修改对象。

调用结束后:

sql 复制代码
user

仍然可以继续使用。

所以可以记住:

&self适合查询、计算、格式化和读取操作。

&mut self:修改当前对象

如果方法需要修改字段,应使用:

rust 复制代码
&mut self

例如:

rust 复制代码
impl User {
    fn birthday(&mut self) {
        self.age += 1;
    }
}

调用:

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

输出:

复制代码
21

这里要求:

bash 复制代码
let mut user

因为方法需要可变借用当前对象。

常见场景包括:

  • 修改状态

  • 更新库存

  • 增加积分

  • 修改名称

  • 添加集合元素

self:消费当前对象

方法还可以直接接收:

lua 复制代码
self

例如:

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

调用:

ini 复制代码
let user = User {
    name: String::from("Tom"),
    age: 20,
};
let name = user.into_name();
println!("{}", name);

调用后:

sql 复制代码
user

不能继续使用,因为它已经被Move进方法。

这种方法适合:

  • 对象转换

  • 消费资源

  • 取出内部拥有的数据

  • 对象使用后不再需要

    可以记住:

rust 复制代码
&self      借用读取
&mut self  借用修改
self       取得所有权

self其实是语法糖

下面的方法:

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

可以近似理解成:

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

而:

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

可以理解为:

rust 复制代码
fn update(self: &mut Self)

直接写:

php 复制代码
fn consume(self)

就是:

php 复制代码
fn consume(self: Self)

平时开发中使用简写即可。

方法可以接收其他参数

除了self之外,方法可以接收普通参数。

例如:

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

调用:

ini 复制代码
let product = Product {
    price: 99.0,
};
let total = product.total_price(3);
println!("{}", total);

输出:

复制代码
297

第一个参数&self代表当前商品,count是普通参数。

方法可以接收其他对象

例如:

rust 复制代码
struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn can_hold(
        &self,
        other: &Rectangle,
    ) -> bool {
        self.width > other.width
            && self.height > other.height
    }
}

调用:

ini 复制代码
let first = Rectangle {
    width: 100,
    height: 80,
};
let second = Rectangle {
    width: 50,
    height: 40,
};
println!("{}", first.can_hold(&second));

方法同样可以借用其他对象。

方法调用中的自动借用

假设方法定义:

php 复制代码
fn show(&self)

理论上需要传:

sql 复制代码
&user

但Rust允许直接:

ini 复制代码
user.show();

编译器会自动理解为类似:

sql 复制代码
User::show(&user);

如果方法是:

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

调用:

ini 复制代码
user.update();

Rust会根据需要自动创建可变借用。

因此方法调用不需要手动到处写:

scss 复制代码
(&user).show()
(&mut user).update()

自动解引用

如果拥有的是引用:

ini 复制代码
let user_ref = &user;

仍然可以:

ini 复制代码
user_ref.show();

而不用:

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

Rust在方法调用时会自动尝试解引用并寻找匹配的方法。

例如:

vbnet 复制代码
let text = String::from("Rust");
println!("{}", text.len());

String可以自动借用并调用相关方法。

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

方法返回普通值

最简单:

rust 复制代码
impl Product {
    fn price(&self) -> f64 {
        self.price
    }
}

因为f64实现Copy,所以直接返回即可。

再例如:

rust 复制代码
impl User {
    fn is_enabled(&self) -> bool {
        self.age >= 18
    }
}

这种查询方法通常不会产生所有权问题。

方法返回引用

如果字段是String,不一定每次都要Clone。

不推荐:

php 复制代码
fn name(&self) -> String {
    self.name.clone()
}

如果调用者只是读取,更推荐:

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

完整示例:

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

返回引用避免了字符串复制。

Rust会通过生命周期省略规则自动判断返回引用来自:

lua 复制代码
&self

方法返回可变引用

有时需要让调用者直接修改字段:

rust 复制代码
struct User {
    tags: Vec<String>,
}
impl User {
    fn tags_mut(&mut self) -> &mut Vec<String> {
        &mut self.tags
    }
}

调用:

ini 复制代码
let mut user = User {
    tags: vec![],
};
user.tags_mut()
    .push(String::from("VIP"));

不过这种接口会直接暴露内部可变数据,实际项目中要谨慎使用。

很多时候,更推荐提供业务方法:

rust 复制代码
fn add_tag(&mut self, tag: String) {
    self.tags.push(tag);
}

返回Self实现链式调用

方法可以取得对象所有权并返回:

rust 复制代码
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 复制代码
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 {
    name: String::new(),
    age: 0,
};
user
    .set_name("Tom")
    .set_age(20);

两种链式方式的区别:

rust 复制代码
返回Self
通常不断Move对象
返回&mut Self
不断可变借用同一个对象

方法中的所有权Move

例如:

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

调用:

ini 复制代码
let name = user.take_name();

整个user被消费。

如果只想读取:

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

如果想复制:

php 复制代码
fn cloned_name(&self) -> String {
    self.name.clone()
}

API设计时一定要明确:

调用这个方法之后,对象还能不能继续使用?

方法修改字段时的Move

例如:

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

调用:

ini 复制代码
let name = String::from("Alice");
user.set_name(name);

这里参数name的所有权Move进入user.name

之后原变量:

复制代码
name

不能继续使用。

如果业务需要保留原字符串,可以Clone或者调整参数设计。

&str还是String参数

方法参数设计非常重要。

例如:

rust 复制代码
fn set_name(&mut self, name: String)

表示调用者把字符串所有权交给对象。

调用:

ini 复制代码
user.set_name(name);

没有额外复制。

如果定义:

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

使用更方便:

arduino 复制代码
user.set_name("Tom");

但方法内部需要创建新的String。

两种设计都合理,要根据业务需求选择。

方法返回Result

业务方法通常可能失败。

例如:

rust 复制代码
struct Account {
    balance: f64,
}
impl Account {
    fn withdraw(
        &mut self,
        amount: f64,
    ) -> Result<(), String> {
        if amount <= 0.0 {
            return Err(
                String::from("金额必须大于0")
            );
        }
        if amount > self.balance {
            return Err(
                String::from("余额不足")
            );
        }
        self.balance -= amount;
        Ok(())
    }
}

调用:

scss 复制代码
match account.withdraw(100.0) {
    Ok(()) => println!("提现成功"),
    Err(error) => println!("{}", error),
}

相比直接修改:

ini 复制代码
account.balance -= 100.0;

方法可以把业务规则集中管理。

Getter与业务方法

例如:

php 复制代码
fn status(&self) -> &OrderStatus

属于Getter。

而:

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

属于业务方法。

真实项目中不要机械地写:

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

更推荐表达真实动作:

scss 复制代码
order.pay()
order.cancel()
product.add_stock()
product.sell()
user.disable()

这样可以避免调用者随意修改内部状态。

into_*命名习惯

Rust API中经常看到:

scss 复制代码
into_string()
into_inner()
into_bytes()

into_*通常表示:

消费self,把当前对象转换成另一种拥有所有权的数据。

例如:

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

调用后原对象通常不能继续使用。

as_*命名习惯

as_*通常表示:

借用当前对象,以另一种形式查看数据,不取得所有权。

例如标准库:

arduino 复制代码
String::as_str()

类似自己的代码:

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

通常不会产生昂贵复制。

to_*命名习惯

to_*通常表示:

根据当前数据创建一个新的拥有所有权的值。

例如:

rust 复制代码
str::to_string()

或者:

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

因此可以简单记住:

复制代码
as_*    借用视图
to_*    创建新值
into_*  消费当前值并转换

这些不是强制语法规则,但遵循Rust生态习惯能让API更容易理解。

实战:商品方法设计

rust 复制代码
#[derive(Debug)]
struct Product {
    name: String,
    price: f64,
    stock: u32,
}
impl Product {
    fn new(
        name: &str,
        price: f64,
    ) -> Self {
        Self {
            name: name.to_string(),
            price,
            stock: 0,
        }
    }
    fn name(&self) -> &str {
        &self.name
    }
    fn add_stock(
        &mut self,
        count: u32,
    ) {
        self.stock += count;
    }
    fn sell(
        &mut self,
        count: u32,
    ) -> Result<f64, String> {
        if self.stock < count {
            return Err(
                String::from("库存不足")
            );
        }
        self.stock -= count;
        Ok(self.price * count as f64)
    }
    fn has_stock(&self) -> bool {
        self.stock > 0
    }
}

调用:

scss 复制代码
fn main() {
    let mut product =
        Product::new("Rust Book", 99.0);
    product.add_stock(10);
    match product.sell(3) {
        Ok(total) => {
            println!("金额:{}", total);
        }
        Err(error) => {
            println!("{}", error);
        }
    }
    println!("{:?}", product);
}

这里:

arduino 复制代码
new        创建对象
name       读取对象
add_stock  修改对象
sell       修改并返回结果
has_stock  查询状态

方法职责非常清晰。

实战:订单状态

rust 复制代码
#[derive(Debug, PartialEq)]
enum OrderStatus {
    Pending,
    Paid,
    Cancelled,
}
#[derive(Debug)]
struct Order {
    id: u64,
    status: OrderStatus,
}
impl Order {
    fn new(id: u64) -> Self {
        Self {
            id,
            status: OrderStatus::Pending,
        }
    }
    fn pay(&mut self) -> Result<(), String> {
        if self.status
            != OrderStatus::Pending
        {
            return Err(
                String::from("当前状态不能支付")
            );
        }
        self.status = OrderStatus::Paid;
        Ok(())
    }
    fn cancel(&mut self) -> Result<(), String> {
        if self.status
            == OrderStatus::Paid
        {
            return Err(
                String::from("已支付订单不能取消")
            );
        }
        self.status =
            OrderStatus::Cancelled;
        Ok(())
    }
}

相比开放status让外部随意修改,方法能够保证状态变化满足业务规则。

常见错误

只读方法错误使用self

不推荐:

rust 复制代码
fn name(self) -> String {
    self.name
}

如果只是读取,应使用:

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

修改方法忘记&mut self

错误:

php 复制代码
fn update(&self) {
    self.age += 1;
}

应该:

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

调用可变方法但对象没有mut

错误:

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

需要:

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

Getter无意义Clone

不推荐:

php 复制代码
fn name(&self) -> String {
    self.name.clone()
}

如果调用者只需要读取,返回:

python 复制代码
&str

即可。

方法职责过多

例如一个方法同时:

复制代码
修改用户
写数据库
发送邮件
生成日志
计算统计

会越来越难测试和维护,应根据模块职责拆分。

为所有字段创建Setter

这会破坏封装,让对象内部状态可以被随意修改。应优先设计业务行为方法。

方法API设计最佳实践

只读操作优先&self

rust 复制代码
fn total(&self) -> f64

修改状态使用&mut self

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

确实消费对象才使用self

php 复制代码
fn into_inner(self) -> T

Getter优先返回引用

对于String、Vec等非Copy数据,如果只读:

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

通常比Clone更好。

方法名称表达所有权语义

常见习惯:

lua 复制代码
as_*    借用
to_*    复制或转换出新值
into_*  消费self

业务对象优先业务方法

推荐:

scss 复制代码
order.pay()
order.cancel()

而不是让调用者直接修改字段。

一个方法只负责一个清晰行为

方法越简单,测试、复用和维护越容易。

本章小结

Rust方法是Struct和其他类型定义行为的重要方式,也是设计清晰API的核心工具。

本文学习了:

  • 方法定义在impl

  • &self表示只读借用当前对象

  • &mut self表示可变借用当前对象

  • self表示取得当前对象所有权

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

  • 方法可以接收其他参数和其他对象

  • 方法可以返回普通值、引用、Result和Self

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

  • 方法调用可能涉及Move

  • as_*to_*into_*能够表达不同所有权语义

  • Getter与业务行为方法应该合理区分

  • 高质量方法API应该通过参数和返回类型明确表达所有权意图

    可以记住:

&self是看一看自己,&mut self是修改自己,self是把自己交出去。

好的方法不仅能够完成操作,还应该让调用者从方法名称和类型签名中看懂所有权和业务意图。

下一篇预告

下一篇我们将学习Rust另一种非常重要的数据建模方式:

Rust Enum枚举详解:使用枚举表达有限状态与不同数据类型

内容包括:

  • 什么是Enum

  • 枚举基本定义

  • 枚举变体Variant

  • 枚举变体携带数据

  • Tuple风格枚举

  • Struct风格枚举

  • Enum与Struct的区别

  • 为Enum实现impl

  • Option的枚举本质

  • Result的枚举本质

  • match匹配Enum

  • if let处理枚举

  • 订单状态、消息类型与支付状态实战

相关推荐
橙子家1 小时前
MSDTC(微软分布式事务处理协调器)对系统稳定性的影响
后端
程序员爱钓鱼1 小时前
Go break、continue、goto 详解
后端·面试·go
凤山老林2 小时前
复杂检索引擎落地:Spring Boot + Elasticsearch 数据同步与高阶查询实战
spring boot·后端·elasticsearch
ShineWinsu9 小时前
对于C++:IO流状态的详细解析
c++·面试·io·cin·io流状态·goodbit·failbit
「PlanA」9 小时前
测试开发面经001
面试
笃行35010 小时前
SQLServer数据迁移之后,那张报表还能不能秒出
后端
LucianaiB10 小时前
别再被 AI 骗了:我把腾讯云 4 个 Skill 做成了个「AI 打假侦探」,过程的一个小配置坑惨了我
后端
JL1510 小时前
面试项目被问到自闭-如何把课设包装成亮眼项目
面试·职场和发展
candyTong11 小时前
Claude Code 如何恢复一段会话
后端·架构·ai编程