自定义类型:结构体

学习目标

学完本章后,你将能够:

  • struct 创建自定义的数据结构
  • 创建并使用结构体实例
  • 为结构体添加方法(impl
  • 理解关联函数(构造器模式)
  • 区分普通结构体、元组结构体、单元结构体

一、为什么要自己定义类型?

到目前为止,我们用的都是 Rust 自带的类型(i32, String, bool 等)。但现实世界中的东西往往由多个数据组成。

比如,描述一个”人”需要:

  • 名字(字符串)
  • 年龄(整数)
  • 身高(小数)

如果分开写:

let name1 = "小明";
let age1 = 18;
let height1 = 1.75;
 
let name2 = "小红";
let age2 = 17;
let height2 = 1.65;

这样很乱!名字、年龄、身高没有绑在一起,不小心就会对应错。

结构体(struct)把它们打包成一个新的类型


二、最简单的结构体

struct Person {
    name: String,
    age: u32,
    height: f64,
}
 
fn main() {
    let p = Person {
        name: String::from("小明"),
        age: 18,
        height: 1.75,
    };
 
    println!("名字:{}", p.name);
    println!("年龄:{}", p.age);
    println!("身高:{}米", p.height);
}

输出:

名字:小明
年龄:18
身高:1.75米

让我们一步步拆解:

2.1 定义结构体

struct Person {
    name: String,
    age: u32,
    height: f64,
}
struct  [类型名] {
    字段1: 类型,
    字段2: 类型,
    字段3: 类型,
}

这就像是做了一个”数据表格”的模板:

字段类型
nameString
ageu32
heightf64

2.2 创建实例

let p = Person {
    name: String::from("小明"),
    age: 18,
    height: 1.75,
};

格式:类型名 { 字段: 值, 字段: 值, ... }

2.3 访问字段

p.name    // 小明
p.age     // 18
p.height  // 1.75

.字段名 来访问。


三、修改结构体字段

结构体默认是不可变的(和变量一样):

fn main() {
    let p = Person {
        name: String::from("小明"),
        age: 18,
        height: 1.75,
    };
 
    p.age = 19;  // 编译错误!p 不是 mut
}

需要加 mut

fn main() {
    let mut p = Person {
        name: String::from("小明"),
        age: 18,
        height: 1.75,
    };
 
    p.age = 19;  // 可以改了
    println!("小明今年{}岁", p.age);
}

注意:mut 加在变量名上,整个结构体就是可变的。Rust 不支持”部分字段可变”。


四、字段初始化简写

如果变量名和字段名相同,可以省略 :值

fn main() {
    let name = String::from("小明");
    let age = 18;
    let height = 1.75;
 
    let p = Person {
        name,    // 等价于 name: name
        age,     // 等价于 age: age
        height,  // 等价于 height: height
    };
 
    println!("{},{}岁,{}米", p.name, p.age, p.height);
}

五、结构体更新语法

从已有的结构体创建新结构体:

fn main() {
    let p1 = Person {
        name: String::from("小明"),
        age: 18,
        height: 1.75,
    };
 
    let p2 = Person {
        name: String::from("小明的弟弟"),
        ..p1  // 其余字段从 p1 复制
    };
 
    println!("p2:{}, {}岁, {}米", p2.name, p2.age, p2.height);
}

输出:

p2:小明的弟弟, 18岁, 1.75米

注意:..p1移动字段。如果字段不是 Copy 类型(如 String),p1 的那个字段会失效。在这个例子里,p1.name 不受影响,因为 p2.name 是单独赋值的。但如果你用了 ..p1 且没有单独赋 namep1 整体就失效了。


六、给结构体添加方法:impl

结构体不只是”数据的容器”,还可以有自己的方法(功能)。就像一个人有名字和年龄,还可以”打招呼”。

struct Person {
    name: String,
    age: u32,
}
 
impl Person {
    // 方法:&self 表示"这个方法作用于这个结构体"
    fn greet(&self) {
        println!("大家好,我叫{},今年{}岁。", self.name, self.age);
    }
}
 
fn main() {
    let p = Person {
        name: String::from("小明"),
        age: 18,
    };
 
    p.greet();  // 调用方法
}

输出:

大家好,我叫小明,今年18岁。

6.1 self 是什么?

fn greet(&self) { ... }
  • &self — “我自己,但我只是借来看看”(只读)
  • &mut self — “我自己,我要修改”(可写)
  • self — “我自己,所有权转移”(很少用)

&selfself: &Self 的缩写。Self 指的是 impl 块对应的类型(这里就是 Person)。

6.2 带参数的方法

impl Person {
    fn celebrate_birthday(&mut self) {
        self.age += 1;
        println!("{}生日啦!现在{}岁。", self.name, self.age);
    }
 
    fn can_drink(&self) -> bool {
        self.age >= 18
    }
}
 
fn main() {
    let mut p = Person {
        name: String::from("小明"),
        age: 17,
    };
 
    println!("能喝酒吗?{}", p.can_drink());  // false
 
    p.celebrate_birthday();  // 18岁了
    println!("能喝酒吗?{}", p.can_drink());  // true
}

七、关联函数:不用 self 的函数

有些函数和类型相关,但不需要具体的实例。比如”创建一个新人”:

impl Person {
    // 关联函数(没有 self)—— 常被用作构造器
    fn new(name: String, age: u32) -> Person {
        Person { name, age }
    }
}
 
fn main() {
    let p = Person::new(String::from("小红"), 17);
    p.greet();
}

注意调用方式:Person::new(...)::,而实例方法用 .

调用方式含义例子
实例.方法调用实例方法p.greet()
类型::函数调用关联函数Person::new(...)

String::from(...) 就是一个关联函数!现在你明白它的写法了。


八、元组结构体:没有字段名的结构体

有时候字段名不那么重要,可以直接写:

struct Color(i32, i32, i32);
struct Point(f64, f64, f64);
 
fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0, 0.0);
 
    println!("红色分量:{}", black.0);  // 用索引访问
    println!("x坐标:{}", origin.0);
}

虽然 ColorPoint 的内部数据一样(三个数字),但它们是不同的类型!你不能把 Point 传给需要 Color 的地方。

fn paint(c: Color) {
    println!("颜色:({}, {}, {})", c.0, c.1, c.2);
}
 
fn main() {
    let p = Point(1.0, 2.0, 3.0);
    // paint(p);  // 编译错误!Point 不能当 Color 用
}

九、单元结构体:没有任何字段

struct AlwaysEqual;  // 没有字段
 
fn main() {
    let subject = AlwaysEqual;
    // 这个结构体没有数据,但可以作为"标记"
}

单元结构体看起来没用,但在某些场景(如 trait 实现、标记类型)中很有用。


十、打印结构体:Debug trait

你可能会尝试这样:

println!("{}", p);  // 编译错误!

因为 Rust 不知道如何把 Person 显示成文字。需要加上 #[derive(Debug)]

#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}
 
fn main() {
    let p = Person {
        name: String::from("小明"),
        age: 18,
    };
 
    println!("{:?}", p);  // Debug 打印
    println!("{:#?}", p); // 美化打印
}

输出:

Person { name: "小明", age: 18 }
Person {
    name: "小明",
    age: 18,
}

{:?} 用 Debug 格式,{:#?} 是美化的 Debug 格式。


十一、结构体实战:图书管理系统

#[derive(Debug)]
struct Book {
    title: String,
    author: String,
    pages: u32,
    is_read: bool,
}
 
impl Book {
    fn new(title: String, author: String, pages: u32) -> Book {
        Book {
            title,
            author,
            pages,
            is_read: false,  // 默认没读过
        }
    }
 
    fn read(&mut self) {
        self.is_read = true;
        println!("你读完了《{}》", self.title);
    }
 
    fn summary(&self) -> String {
        let status = if self.is_read { "已读" } else { "未读" };
        format!("《{}》- {}({}页,{})", self.title, self.author, self.pages, status)
    }
}
 
fn main() {
    let mut book = Book::new(
        String::from("Rust编程"),
        String::from("小明"),
        300,
    );
 
    println!("{}", book.summary());
    book.read();
    println!("{}", book.summary());
}

输出:

《Rust编程》- 小明(300页,未读)
你读完了《Rust编程》
《Rust编程》- 小明(300页,已读)

本章小结

结构体让你能创建自己的数据类型:

  • struct 定义:struct Name { field: Type }
  • 创建实例:Name { field: value }
  • 访问字段:instance.field
  • 修改需要 mutlet mut instance = ...;
  • 方法用 implfn method(&self) { ... }
  • 关联函数用 :: 调用:Type::function()
  • 三种结构体:普通(有名)、元组(无名)、单元(无字段)
  • #[derive(Debug)] 可以让结构体能被打印

接下来,我们要学习如何表示”多种可能性”的数据 — 枚举。

08-多种选择:枚举与匹配


章节考查

总分100分:概念考查40分 + 判断正误20分 + 代码分析15分 + 编程大题15分 + 填空题5分 + 代码补全5分

一、概念考查(每题4分,共40分)

1. struct 关键字用于什么?

  • A. 定义函数
  • B. 定义循环
  • C. 定义自定义数据类型
  • D. 导入模块
点击查看答案

Cstruct 用于定义结构体——一种自定义的复合数据类型。

2. 结构体字段用哪个符号访问?

  • A. ->
  • B. .
  • C. ::
  • D. #
点击查看答案

Binstance.field. 访问字段。:: 用于关联函数。

3. impl 块的主要作用是?

  • A. 导入外部模块
  • B. 为结构体定义方法
  • C. 创建新的结构体
  • D. 实现循环
点击查看答案

Bimpl Type { ... } 为类型添加方法。

4. 方法签名 fn greet(&self)&self 表示什么?

  • A. 方法的所有者
  • B. 调用该方法的实例的引用
  • C. 一个新的结构体
  • D. 方法的返回值
点击查看答案

B&self 是方法调用者本身的不可变引用,self 指代调用该方法的实例。

5. 关联函数的调用方式是?

  • A. instance.function()
  • B. Type::function()
  • C. Type.function()
  • D. function(Type)
点击查看答案

B。关联函数用 :: 调用,如 String::from(...)

6. 结构体字段初始化的简写规则是?

  • A. 所有字段都可以省略
  • B. 变量名和字段名相同时可以省略 :值
  • C. 只有第一个字段可以省略
  • D. 字符串字段可以省略
点击查看答案

B。当变量名和字段名相同时,name: name 可简写为 name

7. struct Color(i32, i32, i32); 是什么?

  • A. 普通结构体
  • B. 元组结构体
  • C. 单元结构体
  • D. 数组
点击查看答案

B。只有类型没有字段名的叫元组结构体,用索引访问。

8. #[derive(Debug)] 的作用是?

  • A. 让结构体可以打印调试信息
  • B. 让结构体不能被修改
  • C. 加速程序运行
  • D. 隐藏结构体
点击查看答案

ADebug trait 允许用 {:?} 格式化输出结构体。

9. p1 为结构体实例,let p2 = Person { name: String::from("小刚"), ..p1 }; 之后?

  • A. p1 的所有字段仍然可用
  • B. p1 的 Copy 字段仍可用,非 Copy 字段可能失效
  • C. p1 完全失效
  • D. p1 自动被复制
点击查看答案

B。更新语法中,未显式赋值的字段从 p1 移动或复制。如果字段是 Copy 的,p1 中该字段仍可用;如果是非 Copy(如 String),则会失效。

10. Selfimpl 块中指代什么?

  • A. 调用者本人
  • B. 当前 impl 块对应的类型
  • C. 方法的参数
  • D. 返回值类型
点击查看答案

BSelfimpl 块所针对的类型名(此处是 Person)的别名。

二、判断正误(每题2分,共20分)

1. 结构体字段默认是可变的。

点击查看答案

错误。结构体和普通变量一样,默认不可变。需要加 mut

2. 结构体部分字段可以单独标记为 mut

点击查看答案

错误。Rust 不支持部分字段可变,整个实例要么可变要么不可变。

3. 实例方法调用用 ::,关联函数调用用 .

点击查看答案

错误。恰恰相反:实例方法用 .(如 p.greet()),关联函数用 ::(如 Person::new())。

4. 同一个结构体可以有多个 impl 块。

点击查看答案

正确。Rust 允许为同一类型编写多个 impl 块。

5. 元组结构体和普通结构体在类型上是等效的。

点击查看答案

错误。即使内部数据相同,它们也是不同的类型。

6. &mut self 方法可以修改结构体实例。

点击查看答案

正确&mut self 提供了可变引用,可以修改实例字段。

7. format!println! 的区别是前者返回字符串,后者打印。

点击查看答案

正确format! 返回拼接好的 String,不打印。

8. 单元结构体 struct Foo; 不能创建实例。

点击查看答案

错误。可以创建:let f = Foo;

9. Debug trait 需要手动实现,不能派生。

点击查看答案

错误#[derive(Debug)] 会自动生成实现,无需手动写。

10. 结构体方法可以直接访问字段而不需要 getter。

点击查看答案

正确impl 块中的方法可以直接访问 self.field

三、代码分析(每题3分,共15分)

1. 下面代码的输出是什么?

struct Point {
    x: i32,
    y: i32,
}
 
fn main() {
    let p = Point { x: 10, y: 20 };
    println!("{}", p.x + p.y);
}
  • A. Point { x: 10, y: 20 }
  • B. 30
  • C. 1020
  • D. 编译错误
点击查看答案

Bp.x 是 10,p.y 是 20,10 + 20 = 30。

2. 下面代码的输出是什么?

struct Counter {
    count: i32,
}
 
impl Counter {
    fn increment(&mut self) {
        self.count += 1;
    }
}
 
fn main() {
    let mut c = Counter { count: 0 };
    c.increment();
    c.increment();
    println!("{}", c.count);
}
  • A. 0
  • B. 1
  • C. 2
  • D. 编译错误
点击查看答案

Cincrement 被调用了两次,count 从 0 加到 2。

3. 下面代码有什么问题?

struct Rect {
    width: u32,
    height: u32,
}
 
fn main() {
    let r = Rect { width: 10, height: 20 };
    println!("面积:{}", r.area());
}
  • A. 没有定义 area 方法
  • B. Rect 没有实现 Debug
  • C. 字段访问方式不对
  • D. width 类型错误
点击查看答案

Ar.area() 调用了不存在的方法,需要在 impl Rect 中定义。

4. 下面代码的输出是什么?

#[derive(Debug)]
struct Student {
    name: String,
    score: u32,
}
 
fn main() {
    let s1 = Student { name: String::from("小明"), score: 90 };
    let s2 = Student { name: String::from("小红"), ..s1 };
    println!("{:?}", s2);
}
  • A. Student { name: “小红”, score: 0 }
  • B. Student { name: “小红”, score: 90 }
  • C. 编译错误(s1 失效了)
  • D. 编译错误(..s1 不能用于含 String 的结构体)
点击查看答案

B..s1 复制其余字段。scoreu32(Copy),被自动复制。name 被显式覆盖。输出 score 为 90。

5. 下面代码的输出是什么?

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}
 
fn main() {
    let c = Counter::new();
    println!("{}", c.count);
}
  • A. 0
  • B. 编译错误(没有 Counter 定义)
  • C. 编译错误(Counter 结构体未定义)
  • D. 运行时错误
点击查看答案

C(假设 Counter 结构体之前定义了,则选 A)。若结构体和 impl 在不同地方,需要看到完整代码。本题实际代码缺少 struct 定义,所以是编译错误。

四、编程大题(15分)

题目: 设计和实现一个 Rectangle 结构体:

  1. 字段:width: u32, height: u32
  2. 方法 area:返回矩形面积
  3. 方法 perimeter:返回矩形周长
  4. 方法 can_hold:接收另一个 Rectangle 引用,判断当前矩形是否能容纳另一个
  5. 关联函数 square:接收 size: u32,创建正方形(宽=高=size)
  6. 使用 #[derive(Debug)]
  7. 在 main 中测试以上功能
点击查看答案
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
 
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
 
    fn perimeter(&self) -> u32 {
        2 * (self.width + self.height)
    }
 
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width >= other.width && self.height >= other.height
    }
 
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}
 
fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };
    let sq = Rectangle::square(25);
 
    println!("rect1 面积:{},周长:{}", rect1.area(), rect1.perimeter());
    println!("rect1 能容纳 rect2?{}", rect1.can_hold(&rect2));
    println!("rect1 能容纳 rect3?{}", rect1.can_hold(&rect3));
    println!("正方形:{:?}", sq);
    println!("正方形面积:{}", sq.area());
}

评分标准

  • 结构体定义含 Debug(2分)
  • area 方法(2分)
  • perimeter 方法(2分)
  • can_hold 方法(3分)
  • square 关联函数(3分)
  • main 中测试(3分)

五、填空题(每题1分,共5分)

1. 定义结构体用关键字 ______,为类型添加方法用 ______

点击查看答案

structimplstruct 定义数据布局,impl 添加行为。

2. 方法中的 ______ 代表调用该方法的实例本身。

点击查看答案

self&self&mut selfself 都代表调用者。

3. struct Point(______, ______); 定义了一个有两个 i32 字段的元组结构体。

点击查看答案

i32i32struct Point(i32, i32);

4. ______::new() 是调用关联函数的语法。

点击查看答案

TypeType::function() 调用关联函数(用 ::)。

5. 要在 println! 中打印结构体,需要用 #[derive(______)] 并用 {:?} 格式。

点击查看答案

Debug#[derive(Debug)] + println!("{:?}", instance)

六、代码补全(共5分)

1. 补全结构体定义(2分)

______ Point {
    x: i32,
    y: i32,
}
点击查看答案
struct Point {

2. 补全方法定义(2分)

impl Point {
    fn distance_from_origin(______) -> f64 {
        ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }
}
点击查看答案
fn distance_from_origin(&self) -> f64 {

3. 补全实例创建(1分)

fn main() {
    let p = Point { x: ______, y: ______ };
    println!("({}, {})", p.x, p.y);
}
点击查看答案
let p = Point { x: 10, y: 20 };

计分:概念40 + 判断20 + 代码分析15 + 编程15 + 填空5 + 补全5 = 总分100分


练习

练习

题号题目链接知识点
P1001A+B Problemhttps://www.luogu.com.cn/problem/P1001输入输出
P1008全排列https://www.luogu.com.cn/problem/P1008递归、枚举
P1010幂次方https://www.luogu.com.cn/problem/P1010函数、递归
P1012拼数https://www.luogu.com.cn/problem/P1012字符串、排序