自定义类型,是由用户定义的类型。自定义类型可以由几种类型组成。它们可以是基元类型的包装起,也可以是多个自定义类型的组合。它们有3种形式:结构体、枚举及联合,或者被称为struct
、enum
及union
。它们允许你更轻松地表示自己的数据。自定义类型的命令规则遵循驼峰命名法,Rust 的结构体和枚举功能比 C 语言的结构体和枚举功能更强大,而Rust的联合非常类似于C语言的联合,主要用于与C语言代码库交互。
结构体
单元结构体
在Rust
中,结构体的声明形式有3种。其中最简单的是单元结构体(unit struct
),它使用关键字 struct 进行声明,随后是其名称,并用分号作为结尾。以下代码示例定义了一个单元结构体:
struct Dummy;
fn main() {
let value = Dummy;
}
在上述代码中定义了一个名为 Dummy
的单元结构体。在main
函数中,我们可以仅使用其名称初始化初始化此类型。value 下载包含一个 Dummy
实例,并且值为0.单元结构体在运行时不占用任何空间,因为没有与之关联的数据。
用到单元结构体的情况非常少。它们可用于对没有与之关联的数据或状态进行实体建模。也可用于表示错误类型,结构体本身足以表述错误,而不需要对其进行描述。还可用于表示状态机实现过程中的状态。接下来,结构体的第二种形式
元组结构体(tuple struct)
它具有关联数据。其中每个字段都没有命名,而是根据它们在定义中的位置进行引用。假定你正在编写用于图形应用程序的颜色转换/计算库,并希望在代码中表示 RGB 颜色值。可以用以下代码表示 Color 类型和相关元素:
struct Color(u8,u8,u8);
fn main() {
let white = Color(255,255,255);
//可以通过索引访问它们
let red = white.0;
let green = white.1;
let blue = white.2;
println!("Red value: {}",red);
println!("Green value:{}",green);
println!("Blue value:{}",blue);
let orange = Color(255,165,0);
//直接解构字段
let Color(r,g,b) = orange;
println!("R: {},G: {},B: {} (orange)",r,g,b);
//可以在解构时忽略字段
let Color(r,_,b) = orange;
}
上述代码中,Color(u8,u8,u8)
是创建和储存变量 white
的元组结构体。然后,我们使用white.0
语法访问white
中的单个颜色组件。元组结构体中的字段可以通过variable.<index>
这样的语法访问,其中索引会引用结构体中字短的位置,并且是以0
开头的。访问结构体中字短的另一种方法是使用let
语句对结构体进行解构。
后面,我们创建了一个颜色 orange
。随后我们编写了一条let
语句,并让Color(r,g,b)
位于等号左边,orange
位与等号右边。这使得orange
中的3
个字短分别存储到了变量r
、g
和b
中。系统会自动为我们判定r
、g
和b
的类型。
类C语言结构体
对于3个以下的属性进行数据建模时,元组结构体是理想的选择。除此之外的任何选择都会妨碍代码的可读性和我们的推理。对于具有3个以上字段的数据类型,建议使用类C语言的结构体,这是第3种形式,也是最常用的形式。
struct Player {
name: String,
iq: u8,
friends: u8,
score: u16
}
fn main() {
let name = "Alice".tostring();
let player = Player {
name,
iq: 171,
friends: 134,
score: 1129
};
bump_okayer_score(player,120)
}
fn bump_okayer_score(mut player: Player,score: u16) {
player.score += 120;
println!("Updated player stats:");
println!("Name: {}",player.name);
println!("IQ: {}",player.iq);
println!("Friends: {}",player.friends);
println!("Score: {}",player.score);
}
上述代码中,结构体的创建方式与元组结构体的相同,即通过指定关键字`struct`,随后定义结构体的名称。但是,结构体以花括号开头,并且声明了字段名称。在花括号内,我们可以将字段写成以都好分隔的`field:type`对。创建结构体的实例也很简单。我们只需编写 `Player`,随后跟一对花括号,花括号中包含以逗号分隔的字段。使用与字段具有相同名称的变量初始化字段时,我们可以使用字段初始化简化(field init shortland)特性,即前面代码中的 `name` 字段。然后我们可以使用 `struct.field_name`语法轻松地访问此前创建的实例中的字段。
上述代码中,我们还有一个名为bump_okayer_score
的函数,它将结构体Player
作为参数。默认情况下,函数参数是不可变的,所以当我们需要修改播放器中的分数 (score)使,需要将函数中的参数修改为 mut player
,以允许我们修改它的任何字段。在结构体上使用关键字mut
意味着它所有字段都是可以修改的。
使用结构体而不是元组结构体的优点在于,我们可以按任意顺序初始化字段,还可以 为字段提供有意义的名称。此外,结构体的大小只是其每个字段成员大小的总和,如有必 要,还包括任意数据对齐填充所需的空间大小。它没有任何额外的元数据尺寸的开销。
枚举
当你需要为不同类型的东西建模时,枚举可能是一种好办法。它是使用关键字 enum
创建的,之后跟着的是枚举名称和一对花括号。在花括号内部,我们可以编写所有可能的 类型,即变体。这些变体可以在包含或不包含数据的情况下定义,并且包含的数据可以是 任何基元类型、结构体、元组结构体,甚至是枚举类型。
不过,在递归的情况下,例如你有一个枚举 Foo 和一个引用枚举的变体,则该变体需 要在指针类型(Box、Rc 等)的后面,以避免类型无限递归定义。因为枚举也可以在堆栈 上创建,所以它们需要预先指定大小,而无限的类型定义使它无法在编译时确定大小。现 在,我们来看看如何创建一个枚举:
#[derive(Debug)]
enum Direction {
N,
E,
S,
w
}
#[derive(Debug)]
enum PlayerAction {
Move {
direction: Direction,
speed: u8
},
Wait,
Attack(Direction)
}
fn main() {
let simulated_player_action = PlayerAction::Move {
direction: Direction::N,
speed: 2,
};
match simulated_player_action {
PlayerAction::Wait => println!("Player wants to wait"),
PlayerAction::Move { direction,speed} => {
println!("Player wants to move in direction {:?} with speed {}",
direction,speed);
}
PlayerAction::Attack(direction) => {
println!("Player wants to attack direction {:?}",direction);
}
}
}
上述代码定义了两个枚举:Direction
和 PlayerAction
。然后我们通过选择任意变体来创 建它们的实例,其中枚举名和变体用双冒号分隔,例如 Direction::N
和 PlayerAction::Wait
。 注意,我们不能使用未初始化的枚举,它必须是变体之一。给定枚举值,要查看枚举实例 包含哪些变体,可以使用 match 表达式进行模式匹配。当我们在枚举上匹配时,我们可以 将变量放在 PlayerAction::Attack(direction)
中的direction
等字段中,从而直接解构变体中的 内容,反过来,这意味着我们可以在匹配臂中使用它们。
正如你在前面的 Direction
变体中看到的,我们有一个#[derive(Debug)]
注释。这是一个 属性,它允许用户在 println!()
中以{:?}
格式输出 Direction
实例。这是通过名为 Debug
的特 征生成方法来完成的。编译器告诉我们是否缺少 Debug
,并提供有关修复它的建议,因此 我们需要从那里获得该属性:
从函数式程序员的角度看,结构体和枚举也称为代数数据类型(Algebraic Data Type, ADT),因为可以使用代数规则来表示它们能够表达的值的取值区间。例如
- 枚举被称为求和类型,是因为它可以容纳的值的范围基本上是其变体的取值范围的总和;
- 枚举类型的一个值可以是变体中的任何一个。
- 枚举的值的”范围”是这三种可能性的总和,但它在任何给定时间只能代表其中一个可能性。
- 而结构体被称为乘积类型,是因为它的取值区间是其每个字段取值区间的笛卡儿积。
struct Player (u8,u8,u8)
- u8 是一个无符号8位整数,取值范围是 0 到 255(含),这意味着它有 256(即 2^8)个可能的值。
- 因为 Player 结构体有三个这样的字段,所以它的取值区间(或说可能的不同值的组合总数)是 256×256×256=2^24。
- 所以,Player 结构体可以表示 16,777,21616,777,216(即 224224)种不同的值组合。这里的计算是基于笛卡尔积的原理,即每个字段的可能值的总和相乘,因为结构体 Player 的每个实例都是由这三个 u8 字段的一个具体组合构成的。
- 在谈到它们时,我们有时会将它们称为 ADT。
我们用更简单的方式来解释这个概念。
想象一下,你有一些乐高积木。你可以用这些积木以不同的方式来构建各种东西。在编程中,特别是在 Rust 中,我们有两种特殊的积木(或说数据类型):结构体和枚举。它们就像是乐高积木,可以帮助我们构建更复杂的数据结构。
结构体(乘积类型)
结构体像是一块大的乐高板,你可以在上面放置不同的积木块。每块积木代表结构体中的一个字段。比如说,如果你要构建一个“汽车”模型,你可能需要一个“轮子”块、一个“车身”块和一个“引擎”块。在结构体中,你定义了这些部分(字段),并且每部分都是必须的。
结构体被称为乘积类型,因为你可以通过计算其字段的可能组合来确定结构体可以表示多少种不同的状态。如果“轮子”可以是两种类型、”车身”可以是三种类型,那么一共就有 2x3=6 种不同的组合方式。
枚举(和类型)
枚举则有点像是给你一系列完成的乐高模型,但你只能从中选择一个来展示。比如说,如果你有一个“交通工具”枚举,它可能包括“汽车”、“自行车”和“飞机”。在任何时候,你的“交通工具”只能是这三者之一。
枚举被称为和类型,因为它可以表示的值的范围是它所有变体的总和。如果你的枚举有 3 个变体,那么它就有 3 种可能的状态。
使用结构体和枚举
在 Rust 中,你会经常使用这两种类型。结构体非常适合当你需要多个部分共同定义一个事物时,比如一个人有名字、年龄和住址。而枚举则适合表示一些只有固定几个选项的情况,比如红绿灯的状态:红色、黄色或绿色。
举个例子
// 结构体:定义一个人
struct Person {
name: String,
age: u8,
}
// 枚举:定义红绿灯的状态
enum TrafficLight {
Red,
Yellow,
Green,
}