《Rust编程实战》系列第41篇
在上一篇文章中,我们学习了Rust生命周期常见错误,进一步理解了所有权、借用和生命周期之间的关系。本篇开始进入Rust数据建模的重要部分:
Struct
中文通常称为"结构体"。
在实际项目中,我们很少只处理单独的字符串、数字和布尔值,而是需要表达更加完整的业务对象,例如:
-
用户
-
商品
-
订单
-
配置
-
文章
-
数据库记录
例如一个用户可能包含:
用户ID
用户名
邮箱
年龄
是否启用
如果分别定义多个变量:
ini
let id = 1;
let name = String::from("Tom");
let email = String::from("tom@example.com");
let age = 20;
let enabled = true;
随着程序变大,这种方式会越来越难维护。
Rust可以使用Struct把相关数据组织成一个完整类型:
rust
struct User {
id: u64,
name: String,
email: String,
age: u32,
enabled: bool,
}
本文将介绍:
-
什么是Struct
-
结构体定义
-
创建结构体实例
-
字段访问与修改
-
字段初始化简写
-
Struct Update语法
-
所有权与Move
-
Tuple Struct
-
Unit Struct
-
#[derive(Debug)] -
结构体作为函数参数
-
返回结构体
-
String与&str字段选择
-
嵌套结构体
-
Option字段
-
实际业务建模
什么是Struct
Struct用于把多个相关值组合成一个自定义数据类型。
基本语法:
rust
struct User {
id: u64,
name: String,
age: u32,
}
这里定义了一个新的类型:
sql
User
它包含三个字段:
bash
id
name
age
每个字段都可以拥有不同类型。
这与数组不同。
数组要求所有元素类型相同:
ini
let numbers = [10, 20, 30];
而结构体可以组合:
rust
u64
String
u32
bool
Vec<T>
Option<T>
等各种类型。
创建结构体实例
定义结构体之后,可以创建实例:
rust
struct User {
id: u64,
name: String,
age: u32,
}
fn main() {
let user = User {
id: 1,
name: String::from("Tom"),
age: 20,
};
}
创建实例时通常需要为每个字段提供值。
字段顺序不强制与定义顺序一致,例如:
rust
let user = User {
age: 20,
id: 1,
name: String::from("Tom"),
};
也是合法的。
访问结构体字段
使用:
变量名.字段名
访问字段。
例如:
rust
fn main() {
let user = User {
id: 1,
name: String::from("Tom"),
age: 20,
};
println!("ID:{}", user.id);
println!("姓名:{}", user.name);
println!("年龄:{}", user.age);
}
这比使用元组:
sql
user.0
user.1
user.2
更加清晰。
修改结构体字段
如果需要修改字段,结构体变量必须声明为:
rust
mut
例如:
ini
fn main() {
let mut user = User {
id: 1,
name: String::from("Tom"),
age: 20,
};
user.age = 21;
user.name = String::from("Alice");
println!("{} {}", user.name, user.age);
}
Rust的可变性通常作用于整个变量,而不是单独字段。
不能这样定义:
rust
struct User {
mut age: u32,
}
字段本身不单独使用mut。
字段初始化简写
当变量名和结构体字段名相同时,可以省略重复名称。
例如:
yaml
fn create_user(
id: u64,
name: String,
age: u32,
) -> User {
User {
id: id,
name: name,
age: age,
}
}
可以简化为:
rust
fn create_user(
id: u64,
name: String,
age: u32,
) -> User {
User {
id,
name,
age,
}
}
这是Rust项目中非常常见的写法。
Struct Update语法
如果需要基于一个已有结构体创建新结构体,可以使用:
erlang
..
例如:
rust
#[derive(Debug)]
struct User {
id: u64,
name: String,
age: u32,
enabled: bool,
}
fn main() {
let first = User {
id: 1,
name: String::from("Tom"),
age: 20,
enabled: true,
};
let second = User {
id: 2,
name: String::from("Alice"),
..first
};
println!("{:?}", second);
}
这里second中的:
age
enabled
来自first。
Struct Update与Move
Struct Update需要特别注意所有权。
例如:
sql
let first = User {
id: 1,
name: String::from("Tom"),
age: 20,
enabled: true,
};
let second = User {
id: 2,
..first
};
如果name没有被重新赋值,那么:
first.name
的所有权会移动到second.name。
此后不能再整体使用:
sql
first
因为发生了部分Move。
如果字段全部实现Copy,则情况不同。
例如:
rust
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
这种结构体更新时可以直接复制字段。
结构体与所有权
结构体可以真正拥有字段数据。
例如:
rust
struct Product {
name: String,
price: f64,
}
这里:
vbnet
name: String
表示Product拥有这个字符串。
创建:
ini
let name = String::from("Rust Book");
let product = Product {
name,
price: 99.0,
};
之后,原变量:
name
已经发生Move。
字符串所有权现在属于:
product.name
当product离开作用域时,其拥有的String也会自动释放。
从结构体字段中Move
例如:
rust
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Tom"),
age: 20,
};
let name = user.name;
println!("{}", name);
println!("{}", user.age);
}
user.name被移动出去,但age是Copy类型,因此还能访问。
不过此时不能整体使用:
arduino
println!("{:?}", user);
因为结构体已经发生部分移动。
如果只需要读取字段,可以借用:
ini
let name = &user.name;
#derive(Debug)
默认情况下,自定义结构体不能直接使用:
arduino
println!("{}", user);
通常也不能直接:
arduino
println!("{:?}", user);
需要实现:
rust
Debug
最简单的方式:
rust
#[derive(Debug)]
struct User {
id: u64,
name: String,
}
然后:
arduino
println!("{:?}", user);
可以输出调试信息。
更加漂亮的格式:
arduino
println!("{:#?}", user);
适合字段较多的结构体。
dbg!宏
Rust还提供:
scss
dbg!()
用于快速调试。
例如:
rust
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Tom"),
age: 20,
};
dbg!(&user);
}
这里使用:
sql
&user
是为了避免把user移动进dbg!。
Tuple Struct元组结构体
Rust还支持没有字段名称的结构体:
rust
struct Color(u8, u8, u8);
创建:
ini
let red = Color(255, 0, 0);
访问:
arduino
println!("{}", red.0);
println!("{}", red.1);
println!("{}", red.2);
这种类型称为:
python
Tuple Struct
元组结构体
适合数据较简单但希望拥有独立类型语义的场景。
Newtype模式
Tuple Struct经常用于Newtype模式。
例如:
c
struct UserId(u64);
struct OrderId(u64);
虽然内部都是:
rust
u64
但它们变成了不同类型。
例如:
javascript
fn find_user(id: UserId) {}
就不会误把:
OrderId
传进去。
这种设计可以减少业务代码中的类型混用。
Unit Struct单元结构体
Rust还支持没有任何字段的结构体:
arduino
struct Logger;
创建:
ini
let logger = Logger;
这种结构体称为:
kotlin
Unit Struct
常用于:
-
标记类型
-
Trait实现
-
不需要保存状态的对象
例如:
arduino
struct ConsoleLogger;
后面可以为它实现某个日志Trait。
结构体作为函数参数
可以直接取得结构体所有权:
arduino
fn save_user(user: User) {
println!("{}", user.name);
}
调用:
scss
save_user(user);
之后原变量会失效。
如果函数只需要读取,更推荐:
arduino
fn print_user(user: &User) {
println!("{}", user.name);
}
调用:
scss
print_user(&user);
如果需要修改:
javascript
fn update_user(user: &mut User) {
user.age += 1;
}
调用:
scss
update_user(&mut user);
这与前面学习的Borrow规则完全一致。
函数返回结构体
函数经常用于创建对象:
rust
fn create_user(
id: u64,
name: &str,
) -> User {
User {
id,
name: name.to_string(),
age: 18,
}
}
调用:
ini
let user = create_user(1, "Tom");
这里函数创建新的User并把所有权返回给调用者。
这种方式比返回局部结构体引用安全得多。
String还是&str字段
结构体字段应该使用:
arduino
String
还是:
python
&str
是非常重要的设计问题。
长期存在的业务实体通常推荐:
arduino
struct User {
name: String,
}
因为User拥有自己的数据,不依赖外部变量。
如果写:
rust
struct User<'a> {
name: &'a str,
}
User会借用外部字符串,需要生命周期管理。
引用字段更适合:
-
临时解析结果
-
零拷贝结构
-
短生命周期视图
普通业务模型通常优先拥有数据。
嵌套结构体
结构体可以包含其他结构体:
rust
#[derive(Debug)]
struct Address {
city: String,
street: String,
}
#[derive(Debug)]
struct User {
id: u64,
name: String,
address: Address,
}
创建:
rust
let address = Address {
city: String::from("Tokyo"),
street: String::from("Shibuya"),
};
let user = User {
id: 1,
name: String::from("Tom"),
address,
};
访问:
arduino
println!("{}", user.address.city);
这种方式可以建立复杂业务模型。
Struct与Option
实际数据经常包含可选字段。
例如用户头像可能不存在:
rust
struct User {
id: u64,
name: String,
avatar: Option<String>,
}
创建没有头像的用户:
rust
let user = User {
id: 1,
name: String::from("Tom"),
avatar: None,
};
有头像:
rust
let user = User {
id: 2,
name: String::from("Alice"),
avatar: Some(
String::from("/avatar/a.jpg")
),
};
Rust不使用传统null,而是通过:
r
Option<T>
表达字段是否存在。
Struct与Vec
结构体中可以保存集合:
rust
#[derive(Debug)]
struct Order {
id: u64,
products: Vec<String>,
}
创建:
vbnet
let order = Order {
id: 1001,
products: vec![
String::from("Keyboard"),
String::from("Mouse"),
],
};
实际项目中的Struct往往就是各种类型的组合:
rust
String
数字
bool
Option<T>
Vec<T>
HashMap<K,V>
其他Struct
Enum
实战:商品结构体
rust
#[derive(Debug)]
struct Product {
id: u64,
name: String,
price: f64,
stock: u32,
enabled: bool,
}
fn create_product(
id: u64,
name: &str,
price: f64,
) -> Product {
Product {
id,
name: name.to_string(),
price,
stock: 0,
enabled: true,
}
}
fn main() {
let mut product =
create_product(
1,
"Rust Book",
99.0,
);
product.stock = 100;
println!("{:#?}", product);
}
这种方式已经非常接近真实业务实体。
实战:订单建模
rust
#[derive(Debug)]
struct OrderItem {
product_name: String,
price: f64,
count: u32,
}
#[derive(Debug)]
struct Order {
id: u64,
customer: String,
items: Vec<OrderItem>,
}
fn main() {
let order = Order {
id: 1001,
customer: String::from("Tom"),
items: vec![
OrderItem {
product_name:
String::from("Rust Book"),
price: 99.0,
count: 2,
},
OrderItem {
product_name:
String::from("Mouse"),
price: 59.0,
count: 1,
},
],
};
println!("{:#?}", order);
}
结构体可以层层组合,从而构建真实业务数据模型。
常见错误
创建实例时漏字段
错误:
rust
let user = User {
id: 1,
name: String::from("Tom"),
};
如果User还有age字段,会编译失败。
不声明mut就修改字段
错误:
ini
let user = User {
id: 1,
name: String::from("Tom"),
age: 20,
};
user.age = 21;
需要:
ini
let mut user = ...
Move字段后继续整体使用
错误:
ini
let name = user.name;
println!("{:?}", user);
应该借用:
ini
let name = &user.name;
或者根据需要Clone。
Struct Update后误用旧对象
如果非Copy字段通过:
sql
..old
移动进新结构体,旧结构体可能无法整体继续使用。
业务对象过度使用&str
大量:
rust
struct User<'a>
可能让生命周期管理复杂化。长期实体通常使用String更自然。
Struct设计最佳实践
一个结构体表达一个明确概念
例如:
css
User
Product
Order
Address
不要把完全无关的数据塞进同一个Struct。
字段名称表达业务含义
推荐:
product_name
created_at
total_price
避免:
css
a
b
data1
长期对象优先拥有数据
使用:
rust
String
Vec<T>
比大量借用字段更容易管理。
可选状态使用Option
不要用空字符串或特殊数字表示"不存在"。
推荐:
vbnet
avatar: Option<String>
复杂状态考虑Enum
例如订单状态不要简单使用:
lua
status: u8
更适合后续定义:
arduino
enum OrderStatus
行为不要全部写成独立函数
当函数明显属于某个结构体时,可以通过:
rust
impl
把行为和数据组织在一起。
这正是下一篇的重点。
本章小结
Struct是Rust中进行数据建模最重要的基础工具之一。
本文学习了:
-
使用
struct定义自定义数据类型 -
创建结构体实例
-
使用
.访问字段 -
使用
mut修改字段 -
字段初始化简写
-
Struct Update语法
-
Struct Update中的Move
-
结构体字段的所有权
-
#[derive(Debug)] -
dbg!()调试结构体 -
Tuple Struct
-
Unit Struct
-
Newtype模式
-
结构体作为函数参数和返回值
-
String与&str字段选择 -
嵌套结构体
-
Option和Vec字段
-
用户、商品、订单业务建模
可以记住:
Struct负责描述"一个对象有哪些数据"。
所有权决定这些字段由谁管理。
Borrow决定函数如何临时访问结构体。
当一个对象不仅有数据,还有属于它自己的行为时,就应该开始使用
impl。
下一篇预告
下一篇我们将学习:
Rust impl详解:为Struct定义方法与关联函数
内容包括:
-
什么是
impl -
方法与普通函数的区别
-
&self -
&mut self -
self -
Self -
关联函数
-
new()构造函数模式 -
Getter方法
-
修改结构体状态
-
一个Struct多个impl块
-
方法调用自动借用与解引用
-
链式调用
-
商品、用户、订单完整实战