Rust 结构体

结构体(struct)是 Rust 组织数据的核心工具,相当于其他语言的"对象"。但 Rust 没有"类"的概念,而是把数据(struct)和行为(impl 块)分开定义。这一篇我们讲三种结构体、impl 方法、关联函数,这是写任何 Rust 程序的基础。

1. 命名结构体(最常用)

命名结构体有具名字段,是最常用的形式。每个字段有名字和类型:

// 命名结构体:最常用,字段有名字
struct User {
    name: String,
    age: u32,
    active: bool,
}

fn main() {
    // 创建实例:每个字段都初始化
    let user1 = User {
        name: String::from("小明"),
        age: 20,
        active: true,
    };

    // 读取字段用 . 操作符
    println!("{} {} {}", user1.name, user1.age, user1.active);

    // 修改字段:整个实例必须 mut(不能只让某字段可变)
    let mut user2 = User {
        name: String::from("小红"),
        age: 22,
        active: true,
    };
    user2.age = 23;          // OK,因为 user2 是 mut
    // user1.age = 25;       // 编译错误!user1 不可变

    // 字段简写:变量名和字段名相同时
    let name = String::from("小刚");
    let age = 25;
    let user3 = User { name, age, active: true };   // 等同 name: name
}

几个细节:

2. 结构体更新语法

基于旧实例创建新实例时,可以用 ..old 语法,只改几个字段,其余从旧实例拿:

fn main() {
    let user1 = User {
        name: String::from("小明"),
        age: 20,
        active: true,
    };

    // 结构体更新语法:基于旧实例创建新实例,只改几个字段
    let user2 = User {
        age: 30,
        ..user1              // 其余字段从 user1 拿
    };
    println!("{} {}", user2.name, user2.age);   // 小明 30

    // 注意:..user1 会【move】user1 中被用到的字段!
    // 这里 name 被 move 到 user2,所以 user1.name 失效
    // println!("{}", user1.name);   // 编译错误!name 已转移
    // 但 user1.active 仍可用(它是 Copy 类型,bool)
    println!("{}", user1.active);    // OK
}

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

重要细节:..user1move 旧实例中被用到的字段(非 Copy 类型)。所以使用更新语法后,旧实例中被 move 的字段就失效了,但 Copy 字段仍可用。这是所有权规则在结构体上的体现。

3. 元组结构体与单元结构体

除了命名结构体,Rust 还有两种结构体形式:

fn main() {
    // 元组结构体:字段没有名字,只有类型
    // 适合"语义化包装"某个类型,但又不想写一堆字段名
    struct Color(i32, i32, i32);          // RGB 颜色
    struct Point(i32, i32, i32);          // 3D 点
    struct Kilometers(f64);               // 包装距离

    let red = Color(255, 0, 0);
    let origin = Point(0, 0, 0);
    let dist = Kilometers(3.5);

    // 访问元组结构体字段,用 .0 .1 .2
    println!("R={} G={} B={}", red.0, red.1, red.2);
    println!("距离 {} 千米", dist.0);

    // 注意:Color 和 Point 虽然内部类型相同,
    // 但是不同的类型,不能互相赋值!
    // let c: Color = origin;    // 编译错误,类型不匹配
    // 这就是"newtype 模式"——避免误用基础类型
}

// 单元结构体:没有任何字段
struct AlwaysEqual;        // 常用于"实现 trait 但不需要存数据"
fn main2() {
    let subject = AlwaysEqual;
    // 通常配合 trait 使用,后面 trait 篇会讲
}

关键点:

4. impl 块:给结构体加方法

这是 Rust 的面向对象方式——把数据(struct)和行为(impl)分开。impl 块为结构体定义方法和关联函数:

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

// impl 块:给结构体定义方法/关联函数
impl Rectangle {
    // 关联函数(没有 self 参数):类似其他语言的"静态方法"
    // 用 :: 调用,如 Rectangle::new(...)
    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }

    // 正方形工厂方法
    fn square(size: f64) -> Rectangle {
        Rectangle { width: size, height: size }
    }

    // 方法:&self 表示"借用 self"(只读)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // 方法:&mut self 表示"可变借用 self"(能改字段)
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // 方法:self(按值):消耗所有权,适合转换
    fn into_square(self) -> Rectangle {
        let size = (self.width + self.height) / 2.0;
        Rectangle { width: size, height: size }
    }

    // 方法参数和返回值可以是别的类型
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let mut rect = Rectangle::new(30.0, 50.0);    // 关联函数用 :: 调用
    println!("面积: {}", rect.area());             // 方法用 . 调用
    rect.scale(2.0);
    println!("放大后: {} x {}", rect.width, rect.height);

    let rect2 = Rectangle::new(10.0, 20.0);
    println!("rect 能装下 rect2? {}", rect.can_hold(&rect2));

    let sq = Rectangle::square(25.0);
    println!("正方形面积: {}", sq.area());
}

几个核心概念:

注意:Rust 没有"构造函数"语法糖。惯例是写一个名为 new 的关联函数,但它和别的关联函数没有本质区别——只是命名约定。

5. 多个 impl 块

一个结构体可以有多个 impl 块。这通常用于组织代码——把相关方法分组。功能上等价于一个大的 impl 块:

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

// 一个结构体可以有多个 impl 块(通常用于组织代码)
impl User {
    fn new(name: &str, age: u32) -> User {
        User { name: name.to_string(), age }
    }
}

impl User {
    fn greet(&self) -> String {
        format!("你好,我是 {},今年 {} 岁", self.name, self.age)
    }
}

impl User {
    fn birthday(&mut self) {
        self.age += 1;
        println!("{} 过生日啦!", self.name);
    }
}

fn main() {
    let mut u = User::new("小明", 20);
    println!("{}", u.greet());
    u.birthday();
    println!("{}", u.greet());
}

这在大型项目里很有用。比如把"业务方法"放一个 impl,把"序列化方法"放另一个 impl,代码更清晰。

6. 打印结构体(Debug trait)

结构体默认不能用 println! 直接打印。需要派生 Debug trait:

#[derive(Debug)]              // 让结构体可以用 {:?} 打印
struct Point { x: i32, y: i32 }

fn main() {
    let p = Point { x: 1, y: 2 };

    // 默认结构体不能直接 println! 打印
    // 需要派生 Debug trait
    println!("{:?}", p);          // Point { x: 1, y: 2 }
    println!("{:#?}", p);         // 美化多行打印

    // 更友好的输出:实现 Display trait(后面 trait 篇讲)
    // 或写自己的 to_string 方法
}

// 常用派生 trait:
//   Debug      — 调试输出 {:?}
//   Clone      — 调用 clone() 深拷贝
//   Copy       — 按位复制(所有字段都要 Copy)
//   PartialEq  — 用 == 比较
//   Hash       — 可作为 HashMap 的 key
#[derive(Debug, Clone, PartialEq)]
struct Color { r: u8, g: u8, b: u8 }

常用派生:

派生(derive)是 Rust 编译器的"代码生成"功能,自动为你实现这些 trait。后面 Trait 篇会深入。

7. 方法链式调用

如果方法返回 self(或修改后的 self),可以链式调用,类似 jQuery/Builder 模式:

8. 所有权与方法

方法接收 self 的方式,直接对应所有权语义:

选择哪种?默认用 &self;需要改用 &mut self;需要消耗/转换才用 self。这遵循"最小权限"原则,让代码意图清晰。

小结

这一篇你掌握了 Rust 组织数据的全部工具:命名结构体(具名字段,最常用)、元组结构体(语义化包装)、单元结构体(标记类型)、impl 块(定义方法和关联函数)、派生 trait(Debug/Clone 等)。Rust 用 struct + impl 替代了传统"类",把数据和 behavior 分开,更符合"组合优于继承"的现代理念。

下一篇我们看枚举与模式匹配——Rust 的枚举远比 C/Java 强大,是处理"多种可能"的利器,配合 match 表达式威力无穷。

← 上一篇 Rust 字符串

下一篇 Rust 枚举与模式匹配

✈️💬