rust -- 结构体与枚举

256 阅读11分钟

结构体

结构体是一种用于组合多个不同类型的数据的自定义数据结构。它们允许您将不同的字段组合在一起,形成一个自定义的数据类型。结构体在Rust中非常常见,用于创建各种复杂的数据结构。

  1. 定义结构体

组成:

  • 通过关键字 struct 定义
  • 结构体 名称
  • n个有名字的结构体 字段

例如,定义一个名为Person的结构体 :

struct Person {  
    name: String,  // 名字字段,类型为String  
    age: u32,      // 年龄字段,类型为u32  
    height: f32,   // 身高字段,类型为f32  
    address: String,   // 地址字段,类型为String 
}  

该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,是一个可变的 String 类型。

  1. 创建结构体实例
  • 初始化实例时,每个字段都需要进行初始化
  • 初始化时的字段顺序不需要和结构体定义时的顺序一致
    // 创建一个Person实例  
    let mut person = Person {  
        name: String::from("Alice"),  // 名字为"Alice"  
        age: 25,                      // 年龄为25  
        height: 1.72,                 // 身高为1.72  
        address: String::from("BeiJing"),   // 地址为"BeiJing"
    };
  1. 访问/修改 结构体字段

通过 . 操作符即可访问结构体实例内部的字段值,也可以进行修改

    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 不支持将某个结构体某个字段标记为可变。

  1. 简化结构体创建

函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化:

fn build_person(name: String, age: u32) -> Person {
    Person {
        name,
        age,
        height: 1.72,
        address: String::from("BeiJing"),
    }
}
  1. 结构体更新

根据已有的结构体实例,创建新的结构体实例

let person2 = Person { 
    name: String::from("xiaoming"),
    ..person
};

因为 person2 仅仅在 name 上与 person 不同,因此我们只需要对 name 进行赋值,剩下的通过结构体更新语法 ..person 即可完成。

.. 语法表明凡是没有显式声明的字段,全部从 person 中自动获取。需要注意的是 ..person 必须在结构体的尾部使用。

把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段

上面代码中,person 的部分字段所有权被转移到 person2 中:ageheight 字段类型具有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, 参数列表) -> 返回类型 {
        // 方法体 
    } 
}
  1. 其中,impl 关键字用于实现特定类型的方法,类型名 是你要为之实现方法的结构体或枚举的名称,方法名 是你为该类型定义的方法的名称,&self 是一个特殊的参数,表示该方法是针对结构体或枚举实例的,参数列表 是方法的输入参数,返回类型 是方法的返回值类型。

  2. &self 实际上是 self: &Self 的缩写。在一个 impl 块中,Self 类型是 impl 块的类型的别名。方法的第一个参数必须有一个名为 self 的Self 类型的参数,所以 Rust 让你在第一个参数位置上只用 self 这个名字来缩写。注意,我们仍然需要在 self 前面使用 & 来表示这个方法借用了 Self 实例。

  3. 如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self

  4. 方法的名称可以与结构中的一个字段相同。名称加上括号时表示是方法,不使用括号时,指的是字段。

关联函数

所有在 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 来简化代码。