《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处理枚举
-
订单状态、消息类型与支付状态实战