外观
第 4 章 宠物图鉴
小螃蟹说
这一章我们要开一家"宠物登记处"!你告诉我宠物的名字、种类、年龄和可爱度,程序把它们登记成一张张档案卡,还能帮你选出"可爱度大赛冠军"。
你可能发现了:我们这章要学的结构体(struct),就像一张表格。表格谁都会填,填着填着,你就把结构体学会了。
还记得第 2 章我们答应过的事吗?当时我们说 Ordering::Less 里的 :: 和第 1 章 String::new() 里的 :: 长得一样,但一个是枚举变体、一个是关联函数——这些谜底,这一章会揭开一部分。还有第 3 章我们整天借来借去的 &str,今天也要派上大用场。
4.1 预览:我们要做什么
- 登记三只宠物(名字、种类、年龄、可爱度)
- 打印"宠物图鉴",每只宠物一张档案卡
- 看看有没有小宝宝(年龄 ≤ 1 岁)
- 举办可爱度大赛,选出冠军
运行结果
text
欢迎来到宠物图鉴!请登记三只宠物。
请输入宠物的名字:
小咪
请输入宠物的种类:
猫
请输入宠物的年龄:
2
请输入宠物的可爱度(1~10):
9
请输入宠物的名字:
旺财
请输入宠物的种类:
狗
请输入宠物的年龄:
0
请输入宠物的可爱度(1~10):
7
请输入宠物的名字:
圆圆
请输入宠物的种类:
仓鼠
请输入宠物的年龄:
1
请输入宠物的可爱度(1~10):
5
== 宠物图鉴 ==
1. 小咪 是一只 2 岁的小猫,可爱度 9/10
2. 旺财 是一只 0 岁的小狗,可爱度 7/10
3. 圆圆 是一只 1 岁的小仓鼠,可爱度 5/10
2. 旺财 还是小宝宝!
3. 圆圆 还是小宝宝!
可爱度大赛冠军:小咪!这一章你会学到:
| 知识 | 是什么 | 会用在哪儿 |
|---|---|---|
| 结构体 | 把相关的东西捆成一捆的表格 | Pet 把名字、种类、年龄、可爱度装在一起 |
| 字段 | 表格里的每一栏 | pet.name、pet.age |
| 方法 | 数据自己会做的事 | pet.describe() 让宠物自我介绍 |
&self | 借自己看一眼 | 方法里的"我" |
format! | 把话说完整,变成文字 | 生成宠物档案卡 |
4.2 动手做
步骤一:新建项目
老规矩:
bash
cargo new pet_gallery
cd pet_gallerypet_gallery 就是"宠物图鉴"。这一章不用请任何外部帮手,Cargo.toml 保持原样。
步骤二:设计宠物档案表格
一只宠物有名字、种类、年龄、可爱度四样信息。如果把每样信息分开放在四个变量里,登记三只宠物就要十二个变量——太乱了!
结构体就是把"相关的东西捆成一捆"的表格。把 src/main.rs 替换成下面这样:
rust
struct Pet {
name: String,
species: String,
age: u32,
cuteness: u32,
}
fn main() {
println!("欢迎来到宠物图鉴!");
}看这个表格:
struct Pet { ... }声明了一张叫Pet的表格。struct是 structure 的缩写,意思就是"结构"- 大括号里每一行是一个字段(field)——表格的一栏:
name名字、species种类、age年龄、cuteness可爱度 - 每栏都要写明装什么:
String装文字,u32装数字
小贴士
struct 的名字用大驼峰(每个单词首字母大写):Pet、PetGallery。函数和变量用小蛇(全部小写,下划线分开):read_pet、pet_gallery。这是 Rust 的全民约定,看名字就知道它是谁。
步骤三:填写第一张档案卡
有了表格,就来填第一张吧。把 main 函数改成:
rust
fn main() {
println!("欢迎来到宠物图鉴!");
let pet1 = Pet {
name: String::from("小咪"),
species: String::from("猫"),
age: 2,
cuteness: 9,
};
println!("{} 是一只 {} 岁的小{},可爱度 {}/10", pet1.name, pet1.age, pet1.species, pet1.cuteness);
}运行:
运行结果
text
欢迎来到宠物图鉴!
小咪 是一只 2 岁的小猫,可爱度 9/10两个新姿势:
创建实例。Pet { name: ..., species: ..., ... } 是"照表格填一张卡":给每一栏写上值,然后整张卡就诞生了。这张"填好的表格"叫一个实例(instance)。它被装进变量 pet1。
访问字段。pet1.name 是"看 pet1 这张卡的名字栏"。用点(.)来取字段——pet1.name、pet1.age、pet1.species……
注意 String::from("小咪"):还记得第 3 章吗?"小咪" 是 &str(借来的文字),而结构体的 name 字段要的是 String(自己拥有的文字)。String::from 就是把借来的文字买下来,变成自己的。它是个关联函数——这个我们马上在步骤五详细讲。
小贴士
这一行特别长,都超出 100 个字符了。好消息是:这样的长 println! 我们马上就不用写了——方法会替我们包办好。
步骤四:自动登记机
手写档案卡太慢,而且以后每一只宠物都得这么写。我们做一个"自动登记机":输名字、输种类、输年龄、输可爱度,一张卡自动填好。
在文件末尾加两个函数,并把 main 里那张手写的卡换掉:
rust
fn main() {
println!("欢迎来到宠物图鉴!请登记一只宠物。");
let pet1 = read_pet();
println!("{} 是一只 {} 岁的小{},可爱度 {}/10", pet1.name, pet1.age, pet1.species, pet1.cuteness);
}
fn read_pet() -> Pet {
let name = read_input("请输入宠物的名字:");
let species = read_input("请输入宠物的种类:");
let age = read_input("请输入宠物的年龄:").parse().expect("年龄要是数字!");
let cuteness = read_input("请输入宠物的可爱度(1~10):")
.parse()
.expect("可爱度要是数字!");
Pet {
name,
species,
age,
cuteness,
}
}
fn read_input(prompt: &str) -> String {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
input.trim().to_string()
}别忘了文件最上面还要加一行 use std::io;。
运行,输一只宠物试试:
运行结果
text
欢迎来到宠物图鉴!请登记一只宠物。
请输入宠物的名字:
小咪
请输入宠物的种类:
猫
请输入宠物的年龄:
2
请输入宠物的可爱度(1~10):
9
小咪 是一只 2 岁的小猫,可爱度 9/10登记机里有几个新知识:
read_input 是个通用小帮手。它接收一句提示词(借来的 &str),打印出来,读一行,擦掉首尾空格,把文字还回去。以后想读什么,喊它就对了。
read_pet 是"填卡机"。它连着喊四次 read_input,凑齐四个字段,然后:
rust
Pet {
name,
species,
age,
cuteness,
}这就是字段缩写魔法:如果变量名和字段名一模一样(都是 name,都是 species……),可以只写一遍,不必写 name: name。Rust 一看就懂:"这里有个 name 变量?好,放进 name 栏。"
.parse() 是老朋友。年龄和可爱度输入的是文字,要变成数字(第 2 章学的),所以后面跟着 .expect(...)。
步骤五:宠物会自我介绍
现在打印档案卡的那行超长代码,是手工拼的。更聪明的做法:让宠物自己介绍自己——这就是方法(method)。
在 struct Pet { ... } 的下面,加一个 impl 块:
rust
impl Pet {
fn describe(&self) -> String {
format!(
"{} 是一只 {} 岁的小{},可爱度 {}/10",
self.name, self.age, self.species, self.cuteness
)
}
fn is_baby(&self) -> bool {
self.age <= 1
}
}然后把 main 里那行超长的 println! 换成:
rust
println!("{}", pet1.describe());运行,效果一模一样,但代码清爽多了:
运行结果
text
欢迎来到宠物图鉴!请登记一只宠物。
请输入宠物的名字:
小咪
请输入宠物的种类:
猫
请输入宠物的年龄:
2
请输入宠物的可爱度(1~10):
9
小咪 是一只 2 岁的小猫,可爱度 9/10方法,是长在数据上的函数。看语法:
impl Pet { ... }——impl是 implementation 的缩写,意思是"给 Pet 这张表格配上功能"。大括号里的函数,都是 Pet 的专用函数fn describe(&self) -> String—— 方法有个特殊的第一个参数&self,"借自己看一眼"。它是第 3 章借用知识的现场应用:方法只是看一下这只宠物(不把它搬走),所以用&- 方法体里用
self.name访问自己的字段——self就是"我自己" - 调用方法:
pet1.describe(),点和函数名,和访问字段pet1.name一模一样,只是多了括号——括号就是"让它干活"
format! 也是新朋友:它和 println! 是双胞胎,唯一区别是——println! 把话喊出去(打印),format! 把话递给你(返回一个 String)。方法里不能直接喊话(喊话是副作用,方法只想干活),所以用 format! 拼好文字,交还给调用者。
小贴士
is_baby 返回 bool:"是不是小宝宝?"年龄小于等于 1 岁就是。还记得第 1 章的 judge 吗?那时候我们还没学结构体,只能判断两个 &str。现在有了结构体,判断的是"一只宠物",信息全多了。
步骤六:三只宠物和可爱度大赛
登记机和方法都齐了,把它们组装起来,举办大赛!把整个 main 函数替换成:
rust
fn main() {
println!("欢迎来到宠物图鉴!请登记三只宠物。");
let pet1 = read_pet();
let pet2 = read_pet();
let pet3 = read_pet();
println!();
println!("== 宠物图鉴 ==");
println!("1. {}", pet1.describe());
println!("2. {}", pet2.describe());
println!("3. {}", pet3.describe());
if pet1.is_baby() {
println!("1. {} 还是小宝宝!", pet1.name);
}
if pet2.is_baby() {
println!("2. {} 还是小宝宝!", pet2.name);
}
if pet3.is_baby() {
println!("3. {} 还是小宝宝!", pet3.name);
}
let winner = if pet1.cuteness > pet2.cuteness && pet1.cuteness > pet3.cuteness {
&pet1
} else if pet2.cuteness > pet3.cuteness {
&pet2
} else {
&pet3
};
println!();
println!("可爱度大赛冠军:{}!", winner.name);
}大赛的评判逻辑是一个 if 表达式(第 1 章说过,if 也是表达式,能当值用):
pet1比两个对手都可爱?→ 冠军是pet1- 不然
pet2比pet3可爱?→ 冠军是pet2 - 都不行 → 冠军是
pet3
注意 &pet1——我们只是"宣布冠军",不把宠物搬走,所以借(&)。winner 的类型就是"借来的宠物",用它访问 winner.name 没问题。
完整运行一遍:
运行结果
text
欢迎来到宠物图鉴!请登记三只宠物。
请输入宠物的名字:
小咪
请输入宠物的种类:
猫
请输入宠物的年龄:
2
请输入宠物的可爱度(1~10):
9
请输入宠物的名字:
旺财
请输入宠物的种类:
狗
请输入宠物的年龄:
0
请输入宠物的可爱度(1~10):
7
请输入宠物的名字:
圆圆
请输入宠物的种类:
仓鼠
请输入宠物的年龄:
1
请输入宠物的可爱度(1~10):
5
== 宠物图鉴 ==
1. 小咪 是一只 2 岁的小猫,可爱度 9/10
2. 旺财 是一只 0 岁的小狗,可爱度 7/10
3. 圆圆 是一只 1 岁的小仓鼠,可爱度 5/10
2. 旺财 还是小宝宝!
3. 圆圆 还是小宝宝!
可爱度大赛冠军:小咪!宠物登记处开业大吉!今天登记的宠物,明天还会回来吗?不会——程序一关,档案就没了。想留住它们?第 7 章、第 8 章,我们给登记处装"收藏柜"和"存档机"。
4.3 知识深挖
4.3.1 结构体:把相关的东西捆成一捆
没有结构体之前,一只宠物的四样信息散在四个变量里:
rust
let pet1_name = String::from("小咪");
let pet1_species = String::from("猫");
let pet1_age = 2;
let pet1_cuteness = 9;三只宠物就是十二个变量,还都叫 pet1_xxx,名字长得要命。更糟的是,它们之间没有任何联系——程序不知道"小咪"和"猫"是同一只宠物。
结构体把它们捆成一捆:
rust
struct Pet {
name: String,
species: String,
age: u32,
cuteness: u32,
}读作:"定义一种叫 Pet 的类型,它由四栏组成。"定义只是画表格,不填内容;真正的内容在实例里:
rust
let pet1 = Pet {
name: String::from("小咪"),
species: String::from("猫"),
age: 2,
cuteness: 9,
};一个结构体就是一门"自定义类型"。以后凡是要"一只宠物",就写 Pet——编译器知道它该有四栏,少一栏、多一栏、填错类型,当场报错。表格有标准,填错就露馅。
为什么这么设计?
还记得第 3 章的"一个值一个主人"吗?Pet 的每个 String 字段都拥有自己的文字。pet1 拥有整张卡,卡拥有自己的名字和种类。整个所有权链条清清楚楚,谁也别想偷偷改掉小咪的名字。
4.3.2 创建实例与访问字段
创建实例就是"填表":
rust
let pet1 = Pet {
name: String::from("小咪"),
species: String::from("猫"),
age: 2,
cuteness: 9,
};规矩:字段名在前,冒号,值,逗号。顺序无所谓,但一个都不能少——少一栏,编译器立刻报错。
访问字段用点:
rust
println!("{}", pet1.name); // 小咪
println!("{}", pet1.age); // 2字段默认和变量一样是锁着的(不可变)。想改字段,实例要加 mut:
rust
let mut pet1 = Pet { ... };
pet1.age = 3; // 可以,因为 pet1 有 mut字段缩写是偷懒神器:变量名和字段名一样时,只写一遍:
rust
let name = String::from("小咪");
let age = 2;
Pet { name, age, species: String::from("猫"), cuteness: 9 }(name 和 age 只写一遍,species 和 cuteness 是别的变量名,就要写全。)
4.3.3 方法:会做事的数据
String::new() 我们在第 1 章就见过,当时说它是"关联函数"。今天谜底揭晓:它和我们的 describe 一样,住在 impl String 块里!区别只有一点:
- 关联函数(associated function):不需要实例就能调用,用
类型::函数名()——String::new()是"凭空造一个空文字",不需要任何文字实例 - 方法(method):必须挂在一个实例上,用
实例.方法名()——pet1.describe()是"让这只宠物自我介绍"
方法的关键在 &self:
rust
impl Pet {
fn describe(&self) -> String {
format!("{} 是一只 {} 岁的小{}", self.name, self.age, self.species)
}
}&self 是"借我自己看一眼"。它是 self: &Pet 的简写(参数类型省略了),意思是:"调用我的人借我一下,我只需要读一读自己。"
想改自己?用 &mut self:
rust
impl Pet {
fn celebrate(&mut self) {
self.cuteness += 1; // 过生日,可爱度 +1
}
}别忘了第 3 章的借用规则:&mut self 意味着调用时要用 let mut pet1,而且同时不能有人借着看。
方法体里想返回什么就返回什么——describe 返回 String(用 format! 拼好),is_baby 返回 bool。
方法的三种"我"
&self:只看不改(最常用)&mut self:看看还要改self:整个拿走(不还了)——比如"宠物退休了,把档案烧掉"
4.3.4 format!:把话说完整
format! 是 println! 的双胞胎:
rust
println!("{}", 42); // 喊出去
let s = format!("{}", 42); // 递给你,String::from("42")用法完全一样({} 挖坑、填值),只是结果一个是"说",一个是"给"。方法里要把话交给调用者,所以用 format!;main 里要让用户看见,所以用 println!。
4.3.5 调试魔法:让结构体开口说话
你有没有好奇过:宠物档案卡到底是什么样子?直接 println!("{}", pet1) 试试——编译器会拒绝,因为它不知道 Pet 该怎么"开口说话"。
办法是给结构体加一行魔法:
rust
#[derive(Debug)]
struct Pet {
// ...
}#[derive(Debug)] 是"让编译器自动帮我写调试功能"的意思。然后就能用 {:?} 打印了:
rust
println!("{:?}", pet1);运行结果
text
Pet { name: "小咪", species: "猫", age: 2, cuteness: 9 }整张卡一目了然!这是调试程序时最常用的招数:哪只宠物不对劲,打印出来看看。{:?} 里的 ? 表示"调试用的格式",以后我们还会见到它的兄弟们。
4.4 动脑筋练习
练习一:宠物最爱吃的零食
给宠物档案加一栏"最喜欢的零食"(favorite_food)。这一栏牵一发动全身,要改三处:
struct Pet里加字段read_pet里读输入describe里把它加进自我介绍
全部改完,运行登记一只宠物,看看档案卡上有没有零食。
点开看答案
三处改动:
第一处,struct Pet 加一栏:
rust
struct Pet {
name: String,
species: String,
age: u32,
cuteness: u32,
favorite_food: String,
}第二处,read_pet 里读输入,并用字段缩写放进卡里:
rust
let favorite_food = read_input("请输入宠物最爱吃的零食:");
Pet {
name,
species,
age,
cuteness,
favorite_food,
}第三处,describe 里加一段:
rust
fn describe(&self) -> String {
format!(
"{} 是一只 {} 岁的小{},可爱度 {}/10,最爱吃 {}",
self.name, self.age, self.species, self.cuteness, self.favorite_food
)
}改完运行试试:档案卡会变成"小咪 是一只 2 岁的小猫,可爱度 9/10,最爱吃小鱼干"。
练习二:给宠物颁称号
给 Pet 加一个方法 award,按可爱度颁称号:
- 可爱度 ≥ 9:
"明星宠物" - 可爱度 ≥ 6:
"可爱宝贝" - 其它:
"潜力新星"
然后在 main 里打印三只宠物的称号。提示:方法和 is_baby 长得像,判断用 if / else if 链。
点开看答案
在 impl Pet 块里,is_baby 的下面加方法:
rust
fn award(&self) -> &'static str {
if self.cuteness >= 9 {
"明星宠物"
} else if self.cuteness >= 6 {
"可爱宝贝"
} else {
"潜力新星"
}
}在 main 里,冠军宣布的后面加三行:
rust
println!("{} 的称号:{}", pet1.name, pet1.award());
println!("{} 的称号:{}", pet2.name, pet2.award());
println!("{} 的称号:{}", pet3.name, pet3.award());注意顺序:先判断 >= 9,再判断 >= 6。如果反了,可爱度 9 的小咪也会先撞上 >= 6 拿到"可爱宝贝",拿不到"明星宠物"——判断顺序就是程序的脾气(第 2 章练习三的老朋友了)。
练习三:宠物长老
把大赛改成"最年长宠物大赛":谁年龄最大,谁就是"宠物长老"。不用写新方法,只改 main 里的大赛逻辑——想一想,要动哪几行?
点开看答案
只改两处。第一处,把 cuteness 换成 age:
rust
let winner = if pet1.age > pet2.age && pet1.age > pet3.age {
&pet1
} else if pet2.age > pet3.age {
&pet2
} else {
&pet3
};第二处,把宣布的话改掉:
rust
println!("宠物长老:{}!", winner.name);三只宠物年龄分别是 2、0、1,所以宠物长老是"小咪"。
小脑筋:如果两只宠物年龄一样大(比如两只都是 2 岁),谁会当长老?答案是——else 兜底的那只。想一想为什么,再想想怎么改成"年龄一样就并列冠军"?(提示:需要一个新的 == 分支。)
4.5 完整代码清单
项目结构:
text
pet_gallery/
├── Cargo.toml
└── src/
└── main.rs文件:Cargo.toml
toml
[package]
name = "pet_gallery"
version = "0.1.0"
edition = "2024"文件:src/main.rs
rust
use std::io;
struct Pet {
name: String,
species: String,
age: u32,
cuteness: u32,
}
impl Pet {
fn describe(&self) -> String {
format!(
"{} 是一只 {} 岁的小{},可爱度 {}/10",
self.name, self.age, self.species, self.cuteness
)
}
fn is_baby(&self) -> bool {
self.age <= 1
}
}
fn main() {
println!("欢迎来到宠物图鉴!请登记三只宠物。");
let pet1 = read_pet();
let pet2 = read_pet();
let pet3 = read_pet();
println!();
println!("== 宠物图鉴 ==");
println!("1. {}", pet1.describe());
println!("2. {}", pet2.describe());
println!("3. {}", pet3.describe());
if pet1.is_baby() {
println!("1. {} 还是小宝宝!", pet1.name);
}
if pet2.is_baby() {
println!("2. {} 还是小宝宝!", pet2.name);
}
if pet3.is_baby() {
println!("3. {} 还是小宝宝!", pet3.name);
}
let winner = if pet1.cuteness > pet2.cuteness && pet1.cuteness > pet3.cuteness {
&pet1
} else if pet2.cuteness > pet3.cuteness {
&pet2
} else {
&pet3
};
println!();
println!("可爱度大赛冠军:{}!", winner.name);
}
fn read_pet() -> Pet {
let name = read_input("请输入宠物的名字:");
let species = read_input("请输入宠物的种类:");
let age = read_input("请输入宠物的年龄:").parse().expect("年龄要是数字!");
let cuteness = read_input("请输入宠物的可爱度(1~10):")
.parse()
.expect("可爱度要是数字!");
Pet {
name,
species,
age,
cuteness,
}
}
fn read_input(prompt: &str) -> String {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
input.trim().to_string()
}怎么运行:
bash
cd pet_gallery
cargo run运行检查单:
能登记三只宠物,打印三张档案卡
年龄 ≤ 1 的宠物会被点名"还是小宝宝"
可爱度最高的宠物当选冠军
输入年龄时打 abc,程序会报错退出(还记得 .expect 的脾气吗?)
试试给 Pet 加上 #[derive(Debug)],用 {:?} 打印一只宠物,看看它长什么样