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

34 阅读10分钟

《Rust编程实战》系列第42篇
上一篇文章中,我们学习了Rust的Struct结构体,已经知道如何定义:

struct User {
    id: u64,
    name: String,
    age: u32,
}

Struct主要负责描述:

一个对象拥有哪些数据。
但真实项目中的对象通常不仅有数据,还会拥有与这些数据相关的行为。
例如一个用户可能需要:

  • 获取用户名

  • 修改用户名

  • 判断是否成年

  • 增加积分
    一个商品可能需要:

  • 修改库存

  • 计算总价

  • 判断是否有库存
    一个订单可能需要:

  • 计算订单金额

  • 修改订单状态

  • 判断是否已经完成
    如果把这些功能全部写成普通函数:

fn print_user(user: &User) {}
fn update_user(user: &mut User) {}
fn user_is_adult(user: &User) -> bool {}

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

impl

用于为Struct定义属于这个类型的方法和关联函数。
本文将介绍:

  • 什么是impl

  • impl基本语法

  • 方法与普通函数的区别

  • &self

  • &mut self

  • self

  • Self

  • 关联函数

  • new()构造模式

  • Getter方法

  • 一个Struct多个impl

  • 自动借用与解引用

  • 链式调用

  • 用户、商品与订单实战

什么是impl

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

为某个类型实现功能

例如定义一个User:

struct User {
    name: String,
    age: u32,
}

可以使用:

impl User {
}

为User添加行为。
完整示例:

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();
}

调用方式:

user.print();

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

Method
方法

impl基本语法

基本结构:

impl 类型名称 {
    // 方法
    // 关联函数
}

例如:

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

这里:

impl Product

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

Product

类型。

方法与普通函数的区别

普通函数:

fn print_user(user: &User) {
    println!("{}", user.name);
}

调用:

print_user(&user);

方法:

impl User {
    fn print(&self) {
        println!("{}", self.name);
    }
}

调用:

user.print();

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

这个行为属于User。
因此面向业务对象的逻辑通常更适合放入impl

什么是self

方法中的:

self

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

impl User {
    fn print(&self) {
        println!("{}", self.name);
    }
}

调用:

user.print();

那么方法中的:

self

就代表:

user

因此:

self.name

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

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

最常见的方法参数是:

&self

例如:

impl User {
    fn name(&self) -> &str {
        &self.name
    }
}

完整示例:

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表示:

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

user

仍然可以继续使用。

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

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

&mut self

例如:

impl User {
    fn set_age(&mut self, age: u32) {
        self.age = age;
    }
}

调用者必须是可变变量:

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

这里:

user.set_age(21);

会临时可变借用user
等价于方法获得:

&mut User

self:取得当前对象所有权

除了:

&self
&mut self

还可以直接写:

self

例如:

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

调用:

user.delete();

之后:

user

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

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

最后一行会报错。
因此可以记住:

&self
只读借用
&mut self
可变借用
self
取得所有权

Self是什么

Rust中:

Self

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

impl User {
    fn new(
        name: String,
        age: u32,
    ) -> Self {
        Self {
            name,
            age,
        }
    }
}

这里:

Self

等价于:

User

所以也可以写成:

fn new(
    name: String,
    age: u32,
) -> User {
    User {
        name,
        age,
    }
}

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

Self

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

关联函数

impl中不接收:

self

参数的函数称为:

Associated Function
关联函数

例如:

impl User {
    fn new(name: String, age: u32) -> Self {
        Self {
            name,
            age,
        }
    }
}

调用时不能写:

user.new(...)

而应该使用:

User::new(...)

完整示例:

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关键字

需要注意:

new()

并不是Rust规定必须存在的构造函数,也不是关键字。
下面都可以:

User::new()
User::create()
User::from_name()
User::default_user()

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

new()

表示创建新对象。
例如:

Vec::new()
String::new()
HashMap::new()

都是类似设计。

多个关联函数

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

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,
        }
    }
}

调用:

let first = User::new("Tom", 20);
let second = User::guest();

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

Getter方法

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

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

但可以调用:

user.name()

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

Setter是否应该大量使用

可以写:

impl User {
    fn set_name(&mut self, name: String) {
        self.name = name;
    }
}

但在实际业务中,不建议机械地为所有字段生成Getter和Setter。
例如订单状态从“未支付”变成“已支付”,与其:

order.set_status(...)

更好的设计可能是:

order.pay();

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

一个Struct可以有多个impl

Rust允许:

impl User {
    fn new() {}
}
impl User {
    fn print(&self) {}
}
impl User {
    fn update(&mut self) {}
}

多个impl块最终都会为同一个类型增加功能。
通常简单类型放在一个impl中即可。
大型项目中,可以根据:

  • 构造逻辑

  • 查询方法

  • 业务操作
    进行适当拆分。

方法可以接收其他参数

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

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

调用:

let total = product.total_price(3);

这里:

&self

是当前Product。

count

是额外参数。

方法可以返回Result

真实业务方法经常需要错误处理。
例如商品扣库存:

#[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(())
    }
}

调用:

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);
}

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

自动借用

假设方法定义:

fn print(&self)

正常来说,它需要:

&User

但调用时通常直接:

user.print();

不需要:

(&user).print();

Rust会自动完成借用。
同样:

fn update(&mut self)

调用:

user.update();

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

&mut user

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

自动解引用

如果拥有引用:

let user_ref = &user;

依然可以:

user_ref.print();

而不需要手动写:

(*user_ref).print();

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

Automatic Referencing and Dereferencing

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

方法返回self

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

#[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
    }
}

调用:

let user = User::new()
    .with_name("Tom")
    .with_age(20);

这种写法常见于:

Builder Pattern
构建器模式

&mut self链式调用

也可以返回:

&mut Self

例如:

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
    }
}

调用:

let mut user = User::new();
user
    .set_name("Tom")
    .set_age(20);

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

方法和字段同名

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

struct User {
    name: String,
}
impl User {
    fn name(&self) -> &str {
        &self.name
    }
}

访问字段:

user.name

调用方法:

user.name()

有没有括号就是区分方式。
这种Getter写法非常常见。

实战:商品impl

#[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

#[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
    }
}

调用:

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

这种设计把:

订单数据
+
订单行为

统一组织在一起。

impl与封装

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

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

只能通过:

deposit()

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

常见错误

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

错误:

fn set_age(&self, age: u32) {
    self.age = age;
}

需要:

fn set_age(&mut self, age: u32) {
    self.age = age;
}

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

错误:

let user = User::new(...);
user.set_age(20);

应该:

let mut user = User::new(...);

不必要地使用self

如果方法只读取对象:

fn print(self)

会消费整个对象。
更合适:

fn print(&self)

把关联函数当方法调用

如果:

fn new() -> Self

没有self参数,应调用:

User::new()

而不是:

user.new()

所有逻辑都写在impl中

并不是任何函数都应该成为方法。
如果逻辑与某个类型没有明显关系,普通函数可能更加合理。

impl最佳实践

创建对象使用关联函数

例如:

User::new()
Product::new()

只读方法使用&self

fn name(&self) -> &str

修改对象使用&mut self

fn update(&mut self)

消费对象时使用self

例如:

fn into_string(self) -> String

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

方法名称体现业务行为

优先:

order.pay()
product.buy()
user.disable()

而不是大量:

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