找回密码
立即注册
搜索
发回帖 发新帖

6347

积分

0

好友

807

主题
发表于 2 小时前 | 查看: 6| 回复: 0

本篇导读

前面几篇我们一直在和零散的变量、字符串、元组打交道。但真实程序里,数据往往是成组出现的:一个“用户”有姓名、年龄、邮箱;一个“矩形”有宽和高;一本书有书名、作者、页数。把这些本就相关的数据拆散成一个个独立变量,既不直观也容易出错。

今天的主角——结构体(struct)——就是 Rust 用来把相关数据打包成一个有意义整体的工具,它类似其他语言中“对象的数据部分”“记录(record)”。

本篇你将学会:定义与实例化具名结构体、字段简写与结构体更新语法、元组结构体与单元结构体、用 impl 块定义方法与关联函数,以及一个贯穿全篇的“矩形面积”案例。


一、定义并实例化结构体

1. 定义结构体

用 struct 关键字给结构体命名,并在花括号中列出每个 字段(field) 的名字和类型:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

2. 创建实例

定义只是“蓝图”,使用时要创建具体的 实例,为每个字段赋值:

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("小明"),
        email: String::from("xiaoming@example.com"),
        sign_in_count: 1,
    };

    // 用点号访问字段
    println!("用户名:{}", user1.username);

    // 修改字段(要求整个实例是 mut 的)
    user1.email = String::from("new@example.com");
    println!("新邮箱:{}", user1.email);
}

两个要点:

  • 访问字段用 实例.字段名;
  • Rust 不允许只把个别字段标记为可变——要么整个实例是 mut,要么都不可变。

3. 字段初始化简写

当函数参数名与字段名相同时,可以使用 字段简写(field init shorthand),省去重复书写:

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username, // 等价于 username: username
        email,    // 等价于 email: email
        sign_in_count: 1,
    }
}

4. 结构体更新语法:从旧实例创建新实例

想基于一个已有实例、只改少数字段来创建新实例,可以用 .. 语法:

let user2 = User {
    email: String::from("another@example.com"),
    ..user1 // 其余字段从 user1 取值
};

注意:..user1 必须放在最后。这里有一个与所有权相关的重要细节——它会“移动”那些没有被显式给出的字段。因为 username 是 String(堆类型),它的所有权从 user1 移动到了 user2,所以更新后 user1 中的 username 不能再用;但 active、sign_in_count 是 Copy 类型,不受影响。


二、元组结构体与单元结构体

并非所有结构体都需要给字段命名,Rust 还提供了两种变体。

1. 元组结构体(Tuple Structs)

元组结构体有结构体的名字,但字段没有名字,只有类型:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    println!("红色分量:{}", black.0);
}

它特别适合“我想给一个元组起个专门的类型名”的场景。注意 Color 和 Point 虽然字段结构相同,但它们是 不同的类型,不能互相赋值——这正是元组结构体的价值:用类型区分含义相同但语义不同的数据(newtype 模式,第 21 篇还会见到)。

2. 单元结构体(Unit-like Structs)

没有任何字段的结构体:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

它只占一个类型名、不占数据空间。当你需要为某个类型实现 trait(行为),但类型本身不需要存储任何数据时会用到,在第 11 篇之后会更有体会。


三、结构体的数据所有权

你可能注意到 User 中我们用的是 String 而不是 &str。这是有意的:结构体默认拥有它字段的数据,只要结构体有效,字段数据就有效,这种设计最简单直接。

结构体当然也可以存放引用,但引用意味着结构体“借用”了别处的数据,为了保证在结构体存活期间被引用的数据一直有效,就必须为引用标注生命周期。这是第 12 篇的内容,现在我们先让结构体拥有全部数据。


四、为结构体定义方法

光有数据还不够,我们还希望数据“能做事”。在 Rust 中,通过 impl(implementation 的缩写)块为结构体定义 方法。

1. 矩形面积:从元组到结构体

假设要计算矩形面积。用结构体表达矩形非常自然:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // &self 是 self: &Self 的简写,表示方法借用当前实例(只读)
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("矩形面积:{} 平方像素", rect.area());
}

2. 读懂 &self

  • 方法的第一个参数通常是 self,代表“调用这个方法的那个实例”;
  • &self 是 self: &Self 的简写,表示 只读地借用 实例。Self 是 impl 块所针对类型的别名(这里就是 Rectangle);
  • 如果方法需要修改实例,就写 &mut self;如果方法要拿走实例的所有权,就直接写 self。

方法与函数的区别仅在于:方法定义在 impl 块中、第一个参数是实例,并通过 实例.方法名() 调用。

3. 方法可以和字段同名

Rust 允许方法与字段同名,常用于实现“访问器(getter)”:

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}
// rect.width 带括号是方法,不带括号是字段

4. 带更多参数的方法

比如判断一个矩形能否完全容纳另一个矩形:

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("rect1 能容纳 rect2 吗?{}", rect1.can_hold(&rect2)); // true
    println!("rect1 能容纳 rect3 吗?{}", rect1.can_hold(&rect3)); // false
}

5. 关联函数:不以 self 为参数

impl 块中还可以定义 不以 self 为第一个参数 的函数,它们叫 关联函数(associated functions),也就是我们早就用过的 String::new()。它们不针对某个实例,而是属于类型本身,用 类型名::函数名() 调用。

最典型的用途是构造函数,惯例命名为 new:

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(20); // 注意是 :: 而不是 .
    println!("正方形面积:{}", sq.area());
}

一个类型可以有多个 impl 块(Rust 允许),但通常把相关方法组织在一个块里。


五、一个有用的小技巧:打印结构体调试信息

调试时你可能想直接打印整个结构体看看,但下面的代码默认编译不过:

println!("rect = {}", rect); // Rectangle 没有实现 Display

Rust 不知道该用什么“人类友好格式”显示自定义结构体。但它提供了专门的 调试格式,只需在结构体定义上方加上一个 派生属性:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("rect = {rect:?}");   // 单行调试格式
    println!("rect = {rect:#?}");  // 多行美化格式
}
  • #[derive(Debug)] 让编译器自动生成调试打印所需的代码;
  • {:?} 是调试占位符,{:#?} 是美化版。

输出:

rect = Rectangle { width: 30, height: 50 }
rect = Rectangle {
    width: 30,
    height: 50,
}

此外还可以用 dbg! 宏获取一个表达式的 文件、行号和值(它会拿走并归还所有权,打印到标准错误流):

let scale = 2;
let rect = Rectangle {
    width: dbg!(30 * scale), // 打印:[src/main_1:..] 30 * scale = 60
    height: 50,
};
dbg!(&rect);

derive(派生)是 Rust 极其常用的机制,编译器能为若干内置 trait 自动生成实现,第 11、24 篇会深入。


六、综合示例:用结构体建模一个银行账户

把今天的内容串成一个稍完整的例子:

#[derive(Debug)]
struct Account {
    owner: String,
    balance: f64,
}

impl Account {
    // 关联函数:开户
    fn new(owner: &str, initial: f64) -> Self {
        Self {
            owner: owner.to_string(),
            balance: initial,
        }
    }

    // 存款(需要 &mut self)
    fn deposit(&mut self, amount: f64) {
        if amount > 0.0 {
            self.balance += amount;
        }
    }

    // 取款,返回是否成功
    fn withdraw(&mut self, amount: f64) -> bool {
        if amount > 0.0 && amount <= self.balance {
            self.balance -= amount;
            true
        } else {
            false
        }
    }

    // 只读查询
    fn balance(&self) -> f64 {
        self.balance
    }
}

fn main() {
    let mut acc = Account::new("小明", 100.0);
    acc.deposit(50.0);

    if acc.withdraw(30.0) {
        println!("取款成功");
    }

    if !acc.withdraw(1000.0) {
        println!("余额不足,取款失败");
    }

    println!("账户信息:{acc:#?}");
    println!("当前余额:{}", acc.balance());
}

这个例子完整展示了:关联函数做构造器、&mut self 做修改、&self 做查询、返回值表达业务结果、derive(Debug) 打印。这就是 Rust 中“数据 + 行为”最基本的组织方式。


七、本篇小结

  1. 结构体 把若干相关字段打包成有意义的整体,用 struct 定义、点号访问字段,可变性作用于整个实例;
  2. 字段名与变量名相同时可用 字段简写;用 ..旧实例 可基于旧实例更新,注意其中堆字段会发生移动;
  3. 元组结构体 给元组命名、用于区分语义;单元结构体 无字段、用于实现行为;
  4. 结构体默认拥有字段数据;存放引用需要生命周期(第 12 篇);
  5. 用 impl 块 定义方法:&self 只读、&mut self 修改、self 获取所有权;
  6. 关联函数 没有 self,用 类型::函数() 调用,常用于名为 new 的构造器;
  7. #[derive(Debug)] 配合 {:?}、{:#?} 和 dbg! 可方便地调试结构体。

八、动手练习

  1. 给 Rectangle 增加一个方法 perimeter(&self) -> u32 计算周长;
  2. 定义一个 Circle 结构体(半径 f64),实现 area 方法,并写一个关联函数 diameter(&self) -> f64(注意:它其实是方法);
  3. 用元组结构体定义 Kilometers(i32) 和 Miles(i32),验证不能把二者直接相加或赋值;
  4. 扩展银行账户例子,增加一个 transfer(from: &mut Account, to: &mut Account, amount: f64) -> bool 关联函数实现转账;
  5. (选做)定义 struct Point { x: f64, y: f64 },实现方法 distance(&self, other: &Point) -> f64 计算两点距离(公式:√((x1−x2)²+(y1−y2)²),平方根方法是 (x).sqrt())。

下篇预告:第 07 篇|枚举与模式匹配

下一篇我们学习 Rust 类型系统中另一个极其强大的特性——枚举(enum),尤其是标准库中无处不在的 Option<T>(表示“可能有值、可能没有”)和 Result<T, E>。

Rust 的枚举不是 C 语言那种简单的整数代号,而是每个变体都可以携带不同的数据,称为代数数据类型。配合 match 穷尽匹配和 if let 语法,它能从根本上消灭“空指针异常”。这是 Rust 优雅与安全的重要来源,下篇见。




上一篇:一根双绞线怎么跑到1000Mbps?车载以太网PHY物理层入门
下一篇:Rust 枚举与 match 模式匹配:从类型系统彻底消灭空指针异常
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-10 05:35 , Processed in 0.094988 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表