Rust Struct结构体详解:定义自己的复杂数据类型

《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块

  • 方法调用自动借用与解引用

  • 链式调用

  • 商品、用户、订单完整实战

相关推荐
风流 少年1 小时前
Spring AI 2.0:MCP
java·后端·spring
北斗落凡尘1 小时前
LangGraph 入门实战(7)
后端·langchain
uzong2 小时前
业务新老系统数据迁移-负责人经验总结和复盘
后端
dogstarhuang2 小时前
大模型 API 停服怎么办:用 API 网关实现多模型统一接入与可切换架构
人工智能·后端·架构·大模型·api·数字化转型·ai应用
苏灿烤鱼2 小时前
公司**不可计算,就自己做操作系统
rust·typescript·agent
程序猿DD2 小时前
OctaFuse Gateway 2.5.0:接入 Responses 端点、更易理解的路由配置
后端·agent
Lzh编程小栈3 小时前
【嵌入式底层】SPI超透彻详解:通信原理、四线结构、四种模式、寄存器底层 + 面试全集
c语言·stm32·单片机·面试·职场和发展
nnerddboy4 小时前
Rust教程04:引用、借用与切片
开发语言·后端·rust