rust学习笔记之基础:结构体和枚举
Rust 自定义数据类型主要是通过下面这两个关键字来创建:
- struct: 定义一个结构体(structure)
- enum: 定义一个枚举类型(enumeration)
结构体
struct,或者 structure,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。
结构体(structure,缩写成 struct)有 3 种类型,使用 struct 关键字来创建:
- 元组结构体(tuple struct),事实上就是具名元组而已。
- 经典的 C 语言风格结构体(C struct)。
- 单元结构体(unit struct),不带字段,在泛型中很有用。
结构体实例
结构体和元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
#[derive(Debug)]
struct User { // 结构体
active: bool, // 字段
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// 结构体实例
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
println!('{}',user1.email);
// 以 Debug 方式打印结构体
println!("{:?}", user1);
// 可变结构体实例,Rust 并不允许只将某个字段标记为可变。
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
// 变量与字段同名时的字段初始化简写语法
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
解构结构体
fn main() {
struct Foo { x: (u32, u32), y: u32 }
// 解构结构体的成员
let foo = Foo { x: (1, 2), y: 3 };
let Foo { x: (a, b), y } = foo;
println!("a = {}, b = {}, y = {} ", a, b, y);
// 可以解构结构体并重命名变量,成员顺序并不重要
let Foo { y: i, x: j } = foo;
println!("i = {:?}, j = {:?}", i, j);
// 也可以忽略某些变量
let Foo { y, .. } = foo;
println!("y = {}", y);
// 这将得到一个错误:模式中没有提及 `x` 字段
// let Foo { y } = foo;
}
结构体更新语法
使用结构体更新语法从其他实例创建实例
let user2 = User {
email: String::from("another@example.com"),
..user1
};
请注意,结构更新语法就像带有 = 的赋值,因为它移动了数据,在创建 user2 后不能再使用 user1,因为 user1 的 username 字段中的 String 被移到 user2 中。如果我们给 user2 的 email 和 username 都赋予新的 String 值,从而只使用 user1 的 active 和 sign_in_count 值,那么 user1 在创建 user2 后仍然有效。
元组结构体
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
要定义元组结构体,以 struct 关键字和结构体名开头并后跟元组中的类型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
// 实例化一个元组结构体
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
// 访问元组结构体的字段
println!("Point ({:?},{:?},{:?})", origin.0, origin.1, origin.2);
// 解构一个元组结构体
let Pair(x, y, z) = origin;
println!("Point ({:?},{:?},{:?})", x, y, z);
}
每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 . 后跟索引来访问单独的值
单元结构体
我们也可以定义一个没有任何字段的结构体!它们被称为单元结构体或者类单元结构体(unit-like structs)。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
struct AlwaysEqual;
fn main() {
// 实例化一个单元结构体
let subject = AlwaysEqual;
}
使用结构体
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("{}",area(&rect1));
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
方法
方法(method)是依附于对象的函数。这些方法通过关键字 self 来访问对象中的数据和其他。方法在 impl 代码块中定义。
方法与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是 self,它代表调用该方法的结构体实例。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// 实现的代码块,`Rectangle` 的所有方法都在这里给出
impl Rectangle {
// 实例方法。`&self` 是 `self: &Self` 的语法糖,其中 `Self` 是方法调用者的类型。在这个例子中 `Self` = `Rectangle`
fn area(&self) -> u32 {
// `self` 通过点运算符来访问结构体字段
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// 这个方法要求调用者是可变的。`&mut self` 为 `self: &mut Self` 的语法糖
fn change(&mut self, width: u32, height: u32) {
self.width += width;
self.height += height;
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("{}",rect1.area());
let mut rect1 = Rectangle {
width: 30,
height: 50,
};
rect1.change(10,10);
}
Rust 有一个叫 自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。
它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:
rect1.area();
(&rect1).distance();
这种自动引用的行为之所以有效,是因为方法有一个明确的接收者:self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
关联函数
所有在 impl 块中定义的不以 self 为第一参数(因此不是方法)的函数被称为关联函数(associated function),关联函数与 impl 后面命名的类型相关,但并不作用于一个结构体的实例。
Rust 没有 static 关键字修饰的静态方法,所有无 self 参数的 impl 函数都是关联函数。
关联函数经常被用作返回一个结构体新实例的构造函数。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3); // 调用关联函数
}
枚举
enum 关键字允许创建一个从数个不同取值中选其一的枚举类型(enumeration)。任何一个在 struct 中合法的取值在 enum 中也合法。
枚举允许你通过列举可能的 成员(variants) 来定义一个类型。
enum IpAddrKind {
V4, // 成员
V6, // 成员
}
可以像这样创建 IpAddrKind 两个不同成员的实例:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
枚举关联数据
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
枚举可以像结构体一样使用 impl 来在枚举上定义方法。
fn main() {
enum Message {
Quit, // 没有关联任何数据
Move { x: i32, y: i32 }, // 包含一个匿名结构体
Write(String), // 包含单独一个 String
ChangeColor(i32, i32, i32), // 包含三个 i32
}
impl Message {
fn call(&self) {
// 在这里定义方法体
match self {
Message::Quit => println!("退出消息"),
Message::Move { x, y } => println!("移动到 ({}, {})", x, y),
Message::Write(text) => println!("文本消息: {}", text),
Message::ChangeColor(r, g, b) => println!("颜色变为 RGB({}, {}, {})", r, g, b),
}
}
}
let m = Message::Write(String::from("hello"));
m.call();
}
类型别名
若使用类型别名,则可以通过其别名引用每个枚举变量。当枚举的名称太长或者太一般化,且你想要对其重命名,那么这对你会有所帮助。
enum VeryVerboseEnumOfThingsToDoWithNumbers {
Add,
Subtract,
}
// 创建一个类型别名
type Operations = VeryVerboseEnumOfThingsToDoWithNumbers;
fn main() {
// 通过类型别名引用枚举变量
let x = Operations::Add;
}
最常见的情况就是在 impl 块中使用 Self 别名。
enum VeryVerboseEnumOfThingsToDoWithNumbers {
Add,
Subtract,
}
impl VeryVerboseEnumOfThingsToDoWithNumbers {
fn run(&self, x: i32, y: i32) -> i32 {
match self {
Self::Add => x + y,
Self::Subtract => x - y,
}
}
}
使用 use
使用 use 声明的话,就可以不写出名称的完整路径了:
// 该属性用于隐藏对未使用代码的警告。
#![allow(dead_code)]
enum Status {
Rich,
Poor,
}
enum Work {
Civilian,
Soldier,
}
fn main() {
// 显式地 `use` 各个名称使他们直接可用,而不需要指定它们来自 `Status`。
use Status::{Poor, Rich};
// 自动地 `use` `Work` 内部的各个名称。
use Work::*;
// `Poor` 等价于 `Status::Poor`。
let status = Poor;
// `Civilian` 等价于 `Work::Civilian`。
let work = Civilian;
match status {
// 注意这里没有用完整路径,因为上面显式地使用了 `use`。
Rich => println!("The rich have lots of money!"),
Poor => println!("The poor have no money..."),
}
match work {
// 再次注意到没有用完整路径。
Civilian => println!("Civilians work!"),
Soldier => println!("Soldiers fight!"),
}
}
C 风格用法
enum 也可以像 C 语言风格的枚举类型那样使用。
// 该属性用于隐藏对未使用代码的警告。
#![allow(dead_code)]
// 拥有隐式辨别值(implicit discriminator,从 0 开始)的 enum
enum Number {
Zero,
One,
Two,
}
// 拥有显式辨别值(explicit discriminator)的 enum
enum Color {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff,
}
fn main() {
// `enum` 可以转成整型。
println!("zero is {}", Number::Zero as i32);
println!("one is {}", Number::One as i32);
println!("roses are #{:06x}", Color::Red as i32);
println!("violets are #{:06x}", Color::Blue as i32);
}
Option 枚举
Option 是标准库定义的另一个枚举。Option 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。问题不在于概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,而且它定义于标准库中,如下:
enum Option<T> {
Some(T),
None,
}
Option<T> 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option:: 前缀来直接使用 Some 和 None。
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
如果使用 None 而不是 Some,需要告诉 Rust Option<T> 是什么类型的,因为编译器只通过 None 值无法推断出 Some 成员保存的值的类型。
当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option<T> 为什么就比空值要好呢?简而言之,因为 Option<T> 和 T(这里 T 可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option<T>。
在对 Option<T> 进行 T 的运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
不再担心会错误的假设一个非空值,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option<T> 中。接着,当使用这个值时,必须明确地处理值为空的情况。只要一个值不是 Option<T> 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
那么当有一个 Option<T> 的值时,如何从 Some 成员中取出 T 的值来使用它呢?Option<T> 枚举拥有大量用于各种情况的方法。
总的来说,为了使用 Option<T> 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 Some(T) 值时运行,允许这些代码使用其中的 T。也希望一些代码在值为 None 时运行,这些代码并没有一个可用的 T 值。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match 控制流运算符
Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面量、变量、通配符和许多其他内容构成。match 的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
enum Status{
OK,
NG
}
fn get_status(st Status)->u8{
match st {
Status::OK=>1,
Status::NG=>{
println!("NG");
0
}
}
}
匹配 Option<T>
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
通配模式
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
2 => (), // 无事发生
other => move_player(other), // 匹配任意其它的值
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
浙公网安备 33010602011771号