《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设计最佳实践