跳转到内容

Rust 结构体

结构体(struct)把相关的值组合成一个有名字的类型。元组是一种快速、没有名字的打包方式;结构体则给每个字段都起了名字,用到它的代码读起来也就清楚得多。

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Alice"),
age: 30,
};
println!("{} 今年 {} 岁", alice.name, alice.age);
}

输出:

Alice 今年 30 岁
▶ 动手试一试

和元组 ("Alice", 30) 比一比:用结构体时,alice.name 和 alice.age 一看就懂,你不用去记第 0 个字段是名字、第 1 个字段是年龄。

和变量一样,结构体实例默认是不可变的。想在创建之后修改某个字段,就加上 mut:

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Alice"),
age: 30,
};
alice.age += 1;
println!("{} 现在 {} 岁了", alice.name, alice.age);
}
▶ 动手试一试

用 impl 块可以给结构体加上函数。在里面,&self 指的就是调用这个方法的那个实例:

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("你好,我是 {},今年 {} 岁", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
alice.greet();
}

输出:

你好,我是 Alice,今年 30 岁
▶ 动手试一试

一种常见的写法是在 impl 里放一个 new 函数,由它来创建实例,调用的人就不用把每个字段都亲手写一遍:

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Bob", 25);
println!("{} 今年 {} 岁", bob.name, bob.age);
}
▶ 动手试一试

注意 Person { name: name.to_string(), age } 里单独出现的 age:当变量名和字段名完全相同时,可以省掉 age: age,只写 age。Rust 把这叫作字段初始化简写(field init shorthand)。

对上面写的结构体试试 println!("{:?}", alice),会编译不过:除非你告诉 Rust,否则它不知道怎样用调试格式打印一个自定义类型。在结构体上方加上 #[derive(Debug)],就能免费获得这个功能:

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
println!("{:?}", alice);
}

输出:

Person { name: "Alice", age: 30 }
▶ 动手试一试

#[derive(...)] 是 Rust 为类型自动生成常用功能的方式。你会经常看到用这种方式加上 Debug,以后还有 Clone 等等。

创建一个 Rectangle 结构体,带有 width 和 height 两个字段(u32)。给它两个方法:area 返回面积,is_square 在两条边相等时返回 true。提示:这两个方法都只读取字段,所以参数是 &self。

查看答案
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("宽矩形:面积 {},是正方形吗?{}", wide.area(), wide.is_square());
println!("正方形:面积 {},是正方形吗?{}", square.area(), square.is_square());
}

输出:

宽矩形:面积 24,是正方形吗?false
正方形:面积 25,是正方形吗?true
▶ 动手试一试

创建一个 Account 结构体,带有 owner(String)和以分为单位的 balance(u32)。写一个 Account::new("Ana"),让余额从 0 开始。然后通过修改字段存入 50 分和 25 分,再用两种方式打印账户:一句普通的话,以及 {:?}。提示:变量必须是 let mut,才能修改 account.balance;而 {:?} 需要 #[derive(Debug)]。

查看答案
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Ana");
account.balance += 50;
account.balance += 25;
println!("{} 有 {} 分钱", account.owner, account.balance);
println!("{:?}", account);
}

输出:

Ana 有 75 分钱
Account { owner: "Ana", balance: 75 }
▶ 动手试一试

new 自己把 balance 设成了 0,调用的人就不会忘记这一步。构造函数之所以这么常见,很大程度上就是因为这一点。

把三个 Person 值放进一个 vector:Ana(34 岁)、Ben(16 岁)和 Chen(52 岁)。打印所有 18 岁及以上的人的名字,再打印谁年龄最大。提示:用 for person in &people 循环,并用 let mut oldest = &people[0]; 记住目前为止年龄最大的人。

查看答案
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Ana"), age: 34 },
Person { name: String::from("Ben"), age: 16 },
Person { name: String::from("Chen"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{} 已经成年", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("年龄最大:{}({} 岁)", oldest.name, oldest.age);
}

输出:

Ana 已经成年
Chen 已经成年
年龄最大:Chen(52 岁)
▶ 动手试一试

oldest 是指向 vector 内部的引用(&Person),不是副本,所以什么都没有被克隆。每次发现年龄更大的人,只要让 oldest 改为指向那个人就行。

  • 结构体把有名字的字段组合成一个类型:struct Person { name: String, age: u32 }。
  • 用 Person { name: ..., age: ... } 创建实例;以后要修改字段,就把它标记为 mut。
  • 用 impl 块添加行为;&self 指的是调用方法的那个实例。
  • 加上 #[derive(Debug)],结构体就能用 {:?} 打印。

小测验

1. 和元组相比,结构体的主要优点是什么?

2. 给结构体加上 #[derive(Debug)] 后,你可以做什么?

3. 在 Person { name: name.to_string(), age } 里,单独的 age 起什么作用?

得分: 0 / 3