自定义类型:结构体
学习目标
学完本章后,你将能够:
- 用
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: 类型,
}
这就像是做了一个”数据表格”的模板:
| 字段 | 类型 |
|---|---|
| name | String |
| age | u32 |
| height | f64 |
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 且没有单独赋 name,p1 整体就失效了。
六、给结构体添加方法: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— “我自己,所有权转移”(很少用)
&self 是 self: &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);
}虽然 Color 和 Point 的内部数据一样(三个数字),但它们是不同的类型!你不能把 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 - 修改需要
mut:let mut instance = ...; - 方法用
impl:fn method(&self) { ... } - 关联函数用
::调用:Type::function() - 三种结构体:普通(有名)、元组(无名)、单元(无字段)
#[derive(Debug)]可以让结构体能被打印
接下来,我们要学习如何表示”多种可能性”的数据 — 枚举。
章节考查
总分100分:概念考查40分 + 判断正误20分 + 代码分析15分 + 编程大题15分 + 填空题5分 + 代码补全5分
一、概念考查(每题4分,共40分)
1. struct 关键字用于什么?
- A. 定义函数
- B. 定义循环
- C. 定义自定义数据类型
- D. 导入模块
点击查看答案
C。struct 用于定义结构体——一种自定义的复合数据类型。
2. 结构体字段用哪个符号访问?
- A.
-> - B.
. - C.
:: - D.
#
点击查看答案
B。instance.field 用 . 访问字段。:: 用于关联函数。
3. impl 块的主要作用是?
- A. 导入外部模块
- B. 为结构体定义方法
- C. 创建新的结构体
- D. 实现循环
点击查看答案
B。impl 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. 隐藏结构体
点击查看答案
A。Debug 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. Self 在 impl 块中指代什么?
- A. 调用者本人
- B. 当前 impl 块对应的类型
- C. 方法的参数
- D. 返回值类型
点击查看答案
B。Self 是 impl 块所针对的类型名(此处是 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. 编译错误
点击查看答案
B。p.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. 编译错误
点击查看答案
C。increment 被调用了两次,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 类型错误
点击查看答案
A。r.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 复制其余字段。score 是 u32(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 结构体:
- 字段:
width: u32,height: u32 - 方法
area:返回矩形面积 - 方法
perimeter:返回矩形周长 - 方法
can_hold:接收另一个Rectangle引用,判断当前矩形是否能容纳另一个 - 关联函数
square:接收size: u32,创建正方形(宽=高=size) - 使用
#[derive(Debug)] - 在 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. 定义结构体用关键字 ______,为类型添加方法用 ______。
点击查看答案
struct 和 impl。struct 定义数据布局,impl 添加行为。
2. 方法中的 ______ 代表调用该方法的实例本身。
点击查看答案
self。&self、&mut self、self 都代表调用者。
3. struct Point(______, ______); 定义了一个有两个 i32 字段的元组结构体。
点击查看答案
i32 和 i32。struct Point(i32, i32);
4. ______::new() 是调用关联函数的语法。
点击查看答案
Type。Type::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分
练习
练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| P1001 | A+B Problem | https://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 | 字符串、排序 |