结构体
结构体是一种用于组合多个不同类型的数据的自定义数据结构。它们允许您将不同的字段组合在一起,形成一个自定义的数据类型。结构体在Rust中非常常见,用于创建各种复杂的数据结构。
- 定义结构体
组成:
- 通过关键字
struct定义 - 结构体
名称 - n个有名字的结构体
字段
例如,定义一个名为Person的结构体 :
struct Person {
name: String, // 名字字段,类型为String
age: u32, // 年龄字段,类型为u32
height: f32, // 身高字段,类型为f32
address: String, // 地址字段,类型为String
}
该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,是一个可变的 String 类型。
- 创建结构体实例
- 初始化实例时,每个字段都需要进行初始化
- 初始化时的字段顺序不需要和结构体定义时的顺序一致
// 创建一个Person实例
let mut person = Person {
name: String::from("Alice"), // 名字为"Alice"
age: 25, // 年龄为25
height: 1.72, // 身高为1.72
address: String::from("BeiJing"), // 地址为"BeiJing"
};
- 访问/修改 结构体字段
通过 . 操作符即可访问结构体实例内部的字段值,也可以进行修改
println!("Name: {}", person.name); // 输出名字
println!("Age: {}", person.age); // 输出年龄
println!("Height: {}", person.height); // 输出身高
println!("Address: {}", person.address); // 输出地址
person.age = 26; // 修改年龄为26
通过解构访问结构体字段:
// 结构体可以像普通的变量一样使用
let mut vec = Vec::new(); // 创建一个空的Vec<T>
vec.push(person); // 将person实例添加到vec中
// 通过解构访问结构体的字段
for p in vec {
let Person { name, age, height,address } = p;
println!("Name: {}, Age: {}, Height: {}, Address: {}", name, age, height, address); // Name: Alice, Age: 26, Height: 1.72, Address: BeiJing
}
注意:必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。
- 简化结构体创建
函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化:
fn build_person(name: String, age: u32) -> Person {
Person {
name,
age,
height: 1.72,
address: String::from("BeiJing"),
}
}
- 结构体更新
根据已有的结构体实例,创建新的结构体实例
let person2 = Person {
name: String::from("xiaoming"),
..person
};
因为 person2 仅仅在 name 上与 person 不同,因此我们只需要对 name 进行赋值,剩下的通过结构体更新语法 ..person 即可完成。
.. 语法表明凡是没有显式声明的字段,全部从 person 中自动获取。需要注意的是 ..person 必须在结构体的尾部使用。
把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
上面代码中,
person的部分字段所有权被转移到person2中:age、height字段类型具有Copy特征,在赋值给person2时,仅仅发生了拷贝,而不是所有权转移。而address字段不具有Copy特征,所以只有address字段发生了所有权转移,由此,person无法再被使用。但是并不代表person内部的其它字段不能被继续使用,例如可以访问到其内部字段:person.age。
元组结构体
结构体有名称,但没有具体的字段名,只有字段的类型。这种结构体长得很像元组,因此被称为元组结构体,例如:
struct Color(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
}
类单元结构体(Unit-like Struct)
类单元结构体是一种特殊的结构体,它没有任何字段,只包含一个单元。类单元结构体可以用于实现一些特定的功能,例如作为函数返回值的容器或者作为特定类型的占位符。
定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 类单元结构体例如:
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为类单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
打印结构体信息
在结构体定义之前加上外部属性 #[derive(Debug)],然后使用{:?}来打印,如果想要更易读的展示,可以使用{:#?},例如:
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person { name: "Alice".to_string(), age: 30 };
println!("Person: {:?}", person);
println!("Person: {:#?}", person);
}
打印结果:
Person: Person { name: "Alice", age: 30 }
Person: Person {
name: "Alice",
age: 30,
}
另一种使用 Debug 格式打印数值的方法是使用dbg! 宏。dbg! 宏接收一个表达式的所有权(与 println! 宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
可以把 dbg! 放在表达式 30 * scale 周围,因为 dbg! 返回表达式的值的所有权,所以 width 字段将获得相同的值,就像在那里没有 dbg! 调用一样。我们不希望 dbg! 拥有 rect1 的所有权,所以我们在下一次调用 dbg! 时传递一个引用。下面是这个例子的输出结果:
[src\main.rs:10] 30 * scale = 60
[src\main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
方法
方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,,并且它们第一个参数总是 self,它代表调用该方法的结构体实例。
方法的语法如下:
impl 类型名 {
fn 方法名(&self, 参数列表) -> 返回类型 {
// 方法体
}
}
其中,
impl关键字用于实现特定类型的方法,类型名是你要为之实现方法的结构体或枚举的名称,方法名是你为该类型定义的方法的名称,&self是一个特殊的参数,表示该方法是针对结构体或枚举实例的,参数列表是方法的输入参数,返回类型是方法的返回值类型。
&self实际上是self: &Self的缩写。在一个impl块中,Self类型是impl块的类型的别名。方法的第一个参数必须有一个名为self的Self类型的参数,所以 Rust 让你在第一个参数位置上只用self这个名字来缩写。注意,我们仍然需要在self前面使用&来表示这个方法借用了Self实例。如果想要在方法中改变调用方法的实例,需要将第一个参数改为
&mut self。方法的名称可以与结构中的一个字段相同。名称加上括号时表示是方法,不使用括号时,指的是字段。
关联函数
所有在 impl 块中定义的函数被称为 关联函数(associated functions),它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。
关联函数通常用于返回一个结构体新实例的构造函数,通常为 new ,但 new 并不是一个关键字。例如:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle::new(3, 4);
println!("矩形的面积:{}", rect.area()); // 矩形的面积:12
}
多个 impl 块
每个结构体都允许拥有多个 impl 块。例如:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle::new(3, 4);
println!("矩形的面积:{}", rect.area()); // 矩形的面积:12
}
枚举
枚举(Enum)是一种自定义数据类型,它允许你定义一个取值范围有限的数据集合,并可以使用模式匹配(Pattern Matching)来处理不同的枚举成员。枚举和模式匹配是 Rust 的核心特性之一。
在 Rust 中,枚举(Enum)是一种自定义数据类型,它允许你定义一个取值范围有限的数据集合,并可以使用模式匹配(Pattern Matching)来处理不同的枚举成员。枚举和模式匹配是 Rust 的核心特性之一。
枚举的定义和使用语法
枚举的定义使用 enum 关键字,后跟枚举名称和枚举成员。每个枚举成员可以带有关联的数据。
语法示例:
enum EnumName {
Variant1,
Variant2,
Variant3,
// ...
}
示例代码:
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
}
fn main() {
let day = Weekday::Wednesday;
match day {
Weekday::Monday => println!("今天是星期一"),
Weekday::Tuesday => println!("今天是星期二"),
Weekday::Wednesday => println!("今天是星期三"),
Weekday::Thursday => println!("今天是星期四"),
Weekday::Friday => println!("今天是星期五"),
}
}
上面的代码定义了一个枚举 Weekday 表示工作日,并在 main 函数中使用模式匹配来处理不同的枚举成员。根据给定的枚举成员,将输出相应的信息。
枚举成员的数据
枚举成员可以携带关联的数据,在定义时可以指定每个成员的数据类型。
语法示例:
enum EnumName {
Variant1,
Variant2(u32, String),
Variant3(bool),
// ...
}
示例代码:
enum IpAddress {
V4(u8, u8, u8, u8),
V6(String),
}
fn print_ip(ip: IpAddress) {
match ip {
IpAddress::V4(a, b, c, d) => {
println!("IPv4 地址: {}.{}.{}.{}", a, b, c, d);
}
IpAddress::V6(s) => {
println!("IPv6 地址: {}", s);
}
}
}
fn main() {
let ipv4 = IpAddress::V4(127, 0, 0, 1);
let ipv6 = IpAddress::V6(String::from("::1"));
print_ip(ipv4); // IPv4 地址: 127.0.0.1
print_ip(ipv6); // IPv6 地址: ::1
}
上面的代码定义了一个枚举 IpAddress 表示 IP 地址,其中 V4 枚举成员携带四个字节的 IPv4 地址,而 V6 枚举成员携带一个字符串表示 IPv6 地址。在 print_ip 函数中,使用模式匹配来处理不同的枚举成员,并打印相应的信息。
Option 枚举用于处理空值
Option 是标准库定义的另一个枚举。Option 类型用来处理空值的情况。它有两个变体:Some 和 None。
Some变体表示一个包含值的Option。None变体表示一个空的Option。
使用 Option 可以避免使用空值(null)或空指针引发的问题。当你想表达一个值可能存在也可能不存在时,可以使用 Option 类型。
示例:
fn main() {
// 创建一个 Some 值
let some_value = Some(5);
println!("some_value = {:?}", some_value);
// 创建一个 None 值
let none_value: Option<i32> = None;
println!("none_value = {:?}", none_value);
// 使用 Option 类型的值
let optional_value: Option<i32> = Some(10);
match optional_value {
Some(value) => println!("value = {}", value),
None => println!("value is None"),
}
}
打印结果:
some_value = Some(5)
none_value = None
value = 10
在上面的示例中,我们首先创建了一个包含整数值 5 的 Some 值,并打印它。然后,我们创建了一个空的 Option 值 None,并打印它。最后,使用 match 语句来处理 Option 类型的值。根据 Option 类型的实际值,我们可以采取不同的操作。
使用 Option 类型可以帮助我们编写更安全、更健壮的代码,避免空指针引用等问题。
match 控制流结构
上面的例子中,我们已经使用到了match,它是一种非常强大的模式匹配工具,它可以用来对不同类型的值进行模式匹配。当match语句的表达式与某个模式的值匹配时,对应的代码块将被执行。
如下示例,展示了如何使用match来处理不同类型的值:
fn main() {
let value = 3;
match value {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Something else"),
}
}
// Three
在上面的示例中,会打印“Three”。最后的_模式是一个“catch-all”模式,它会匹配所有未在前面的模式中匹配到的值。在这个例子中,如果value的值是1到3之间的整数,那么将打印相应的字符串;否则,将打印"Something else"。
other
other 是一个变量模式,它可以匹配除了已经列出的模式之外的其他值,并将这个值绑定到 other 变量上。例如:
fn main() {
let value = 3;
match value {
1 => println!("One"),
2 => println!("Two"),
other => println!("Something else: {}", other),
}
}
// Something else: 3
在这个例子中,如果 value 的值是 1 或 2,那么对应的模式会匹配成功;否则,other 模式会匹配成功,并将 value 的值绑定到 other 变量上。然后打印 "Something else: 3" 。
if let 简洁控制流
与 match 表达式相比,if let 语法更简洁,特别是当你只关心一种特定的模式,而忽略其他所有情况时。
下面是一个 match 表达式的例子:
let some_value = Some(5);
match some_value {
Some(value) => println!("Got a value: {}", value),
None => println!("No value"),
}
使用 if let,你可以简化这个代码:
let some_value = Some(5);
if let Some(value) = some_value {
println!("Got a value: {}", value);
} else {
println!("No value");
}
if let语法的好处在于它的简洁性,但是这样会失去 match 强制要求的穷尽性检查。if let 更适合处理只有一种情况你真正关心,而其他情况你不需要处理的情况。在这种情况下,你不需要编写一个完整的match表达式,而可以使用if let 来简化代码。