【🔥前端学rust】复合数据类型

29 阅读10分钟

前言

这是 【🔥前端学rust】 系列文章的第三篇文章了。其他内容如下:

  1. 【🔥前端学rust】花式println和注释
  2. 【🔥前端学rust】基础数据类型

继续学习 rust 的数据类型,学习 rust 的复合数据类型。

字符串类型

rust 中,字符串有两种主要的类型:String 和字符串切片 &str 。这两种类型各有特点,适用于不同的场景。

String 类型

String 是一个可变的字符串类型,位于堆上,可以动态增长。它是 std::string::String 的实例。

  1. 可变String 是可变的,可以动态地改变其内容和大小。
  2. 堆分配String 存储在堆上,因此它的长度在运行时是可变的。
  3. UTF-8 编码String 是以UTF-8编码的,因此它可以存储任何Unicode字符。
  4. 所有权String 拥有其内容,负责内存的分配和释放。
let mut s = String::new(); // 创建一个空字符串
let s = String::from("Hello, Rust!"); // 从字面量创建字符串

let mut s = String::from("Hello");
s.push_str(", Rust!"); // 追加字符串
s.push('!'); // 追加字符

字符串切片 &str

&str 是一个不可变的字符串引用,通常被称为“字符串切片”。它是 rust 中最常见的字符串类型之一,因为它轻便且不需要分配堆内存。

  1. 不可变&str 是不可变的,表示一段字符串数据的引用。
  2. 堆或栈中&str 可以是堆分配(如引用String的内容)或栈上的字符串字面量。
  3. 通常用于借用&str 用于借用字符串数据而不被复制。
let s: &str = "Hello, Rust!"; // 字符串字面量
let s = String::from("Hello, Rust!");
let slice: &str = &s; // 从 String 创建 &str 切片

字符串转换

rust 中,经常需要在 &str 和 String 之间进行转换。

从 &str 到 String:

let slice: &str = "Hello, Rust!";
let s = String::from(slice);

从 String 到 &str:

let s = String::from("Hello, Rust!");
let slice: &str = &s; // 通过引用转换

字符串操作

rust 提供了许多有用的字符串方法,例如查找子字符串、替换字符等。

// 查找子字符串
fn main() {
    let s: String = String::from("Hello, world!");

    if let Some(pos) = s.find("world") {
        println!("Found at position: {}", pos);
    } else {
        println!("Not found.");
    }
}

// 替换字符
fn main() {
    let s: String = String::from("Hello, world!");
    let new_s = s.replace("world", "Rust");
    println!("{}", new_s);
}

// 字符串的连接
let s1 = String::from("Hello, ");
let s2 = String::from("Rust!");
let s3 = s1 + &s2; // 使用 + 操作符会转移拥有权

// 字符串切片
let s = String::from("Hello, Rust!");
let hello = &s[0..5]; // 获取切片

使用场景

使用 String 当你需要一个可变的、拥有其内容的字符串时,比如在需要频繁修改字符串内容的情况下。使用 &str 作为函数参数时,可以提高性能,因为它不需要额外的内存分配和复制。根据需要的可变性和所有权,可以选择适合的字符串类型来使用。

元组(Tuple)类型

rust 中,元组(Tuple)是一种有序的数据结构,可以包含任意数量和类型的值。元组是固定长度的,一旦创建,就不能改变其长度或其元素的位置。元组的基本语法是使用圆括号 () 将元素包围起来,元素之间用逗号 , 分隔。因此,元组具有如下的特性:

  1. 序列性:元组中的元素是有序的,可以通过索引访问。
  2. 不同类型:同一个元组可以包含不同类型的数据,如整数、浮点数和字符等。
  3. 解构:可以使用模式匹配解构元组,方便地提取出各个元素。
  4. 固定大小:元组的大小在编译时是固定的,不能动态改变。
fn main() {
    // 创建一个元组
    let my_tuple: (i32, f64, char) = (500, 6.4, 'a');

    // 访问元组元素
    let (x, y, z) = my_tuple;
    println!("x: {}, y: {}, z: {}", x, y, z);
    
    // 通过索引访问元素
    println!("First element: {}", my_tuple.0);
    println!("Second element: {}", my_tuple.1);
    println!("Third element: {}", my_tuple.2);
}

元组的解构

解构元组是指将元组中的各个元素分解到单独的变量中,这样可以更方便地使用这些值。

fn main() {
    // 定义一个元组
    let tuple = (500, 6.4, "hello");

    // 解构元组
    let (integer, float, string) = tuple;

    println!("Integer: {}", integer);
    println!("Float: {}", float);
    println!("String: {}", string);
}

元组的嵌套

元组可以嵌套,即元组的某个元素本身也是一个元组。

fn main() {
    // 定义一个嵌套的元组
    let nested_tuple = ((1, 2), 3, "four");

    // 访问嵌套元组中的元素
    println!("First element of the inner tuple: {}", nested_tuple.0 .0);
    println!("Second element of the inner tuple: {}", nested_tuple.0 .1);
    println!("Third element: {}", nested_tuple.1);
    println!("Fourth element: {}", nested_tuple.2);
}

元组作为函数返回值

元组也可以作为函数的返回值,这对于需要同时返回多个值的情况特别有用。

// 返回一个元组
fn calculate(x: i32, y: i32) -> (i32, i32) {
    (x + y, x * y)
}

fn main() {
    let result = calculate(5, 10);
    println!("Sum: {}, Product: {}", result.0, result.1);
}

使用场景

元组在以下场景中非常有用:

  • 当你需要返回多个值时,可以使用元组返回。
  • 当你需要在函数内部组合不同类型的数据时,元组是一种简单的解决方案。

数组(Array)类型

rust 中,数组(Array)是另一种定长的、不可变的数据结构。数组是一种固定大小的连续内存区域,用于存储相同类型的元素。数组非常适合于存储固定数量的元素,并且可以在编译时确定其大小,且不可改变,这意味着你不能添加或删除元素。

数组的声明

数组的声明使用方括号 [ ],并且后面需要指定数组长度和数据类型。即 let array_name: [type; size] = [value1, value2, ..., valueN];,其中 type 表示元素的类型,size 表示数组的大小(元素的数量),values 表示初始化数组的值列表。

let a: [i32; 5] = [1, 2, 3, 4, 5]; // 声明一个包含 5 个 i32 类型元素的数组
let b = [0; 10]; // 声明一个包含 10 个元素且都是 0 的数组

数组元素的访问

你可以使用索引来访问数组中的元素,索引从 0 开始。

let a = [1, 2, 3, 4, 5];
let first = a[0]; // first 为 1
let second = a[1]; // second 为 2

数组的长度

可以使用 len 方法获取数组的长度。

let a = [1, 2, 3, 4, 5];
let length = a.len(); // length 为 5

数组的遍历

可以使用 for 循环遍历数组中的每个元素。

let a = [1, 2, 3, 4, 5];

for element in a.iter() {
    println!("{}", element);
}

多维数组

rust 允许你创建多维数组。声明方式与一维数组类似,只是需要嵌套方括号。

let matrix: [[i32; 3]; 2] = [[1, 2, 3], [4, 5, 6]]; // 2 x 3 的二维数组

数组的切片

数组切片是对数组的引用,它可以让你动态访问数组的一部分。用 & 符号创建一个切片。

let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..4]; // 切片包含元素 2, 3, 4

数组传递

在函数中传递数组时,可以传递数组的引用,以避免复制数组。

fn print_array(arr: &[i32]) {
    for &item in arr.iter() {
        println!("{}", item);
    }
}

let a = [1, 2, 3, 4, 5];
print_array(&a);

若需在函数中接收固定大小的数组,可以这样定义:

fn print_fixed_array(arr: &[i32; 5]) {
    for &item in arr.iter() {
        println!("{}", item);
    }
}

let a = [1, 2, 3, 4, 5];
print_fixed_array(&a);

向量(Vector)类型

rust 中,向量(Vec<T>)是一种动态数组,它可以在运行时动态地扩展或收缩。与固定大小的数组不同,向量的长度可以在程序运行时增加或减少。Vec 是 Rust 标准库 std::vec 中的一部分。向量具有以下特点:

  1. 动态大小: 向量的大小可以在运行时动态调整,可以进行添加、删除等操作。
  2. 连续内存: 向量中的元素在内存中是连续存储的,这使得它具有良好的缓存友好性。
  3. 所有权: Rust的所有权系统促使你对向量及其元素的所有权进行明确控制。
// 创建一个空的向量
let mut vec: Vec<i32> = Vec::new();

// 使用宏创建向量
let vec2 = vec![1, 2, 3, 4];

// 预留空间的向量
let mut vec3 = Vec::with_capacity(10);

向量的操作

rust 为向量提供了很多有用的方法。

// 添加元素:使用`push`方法添加元素到向量的末尾。
let mut vec = Vec::new();
vec.push(1);
vec.push(2);

// 获取元素:使用索引或者`get`方法访问元素。
let first = &vec[0]; // 直接索引
let second = vec.get(1); // 使用get,返回Option

// 修改元素:向量中的元素可以通过索引直接修改。
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
v[0] = 10; println!("{:?}", v);

// 删除元素: 使用pop方法从末尾移除元素,或使用remove从指定位置移除元素。
let last = vec.pop(); // 移除最后一个元素
if let Some(value) = vec.pop() {
    println!("Popped: {}", value);
}

// 遍历:可以使用for循环遍历向量的元素。
for value in &vec {
    println!("{}", value);
}

// 切片:向量可以通过切片(slice)来引用一个向量的部分。
let slice = &vec[1..3]; // 获取从索引1到2的切片

// 其他一些方法
1. len(): 获取向量中的元素数量。
2. is_empty(): 检查向量是否为空。
3. clear(): 清空向量中的所有元素。
4. extend(): 合并另一个可迭代对象的元素。

枚举(Enum)类型

rust 中的枚举(Enum)是一种非常强大和灵活的类型,允许你定义一组相关的值。枚举不仅能够定义一组离散的状态,还可以携带额外的数据。这种特性使得枚举非常适合用来表达复杂的业务逻辑和状态机。

枚举的声明

枚举由关键字 enum 定义,后面跟着枚举名和花括号 {},里面列出各个成员。每个成员可以有自己的名称,称为枚举的变体。成员可以携带数据,这些数据可以是任何类型。

enum EnumName {
    Variant1,
    Variant2(data_type),
    Variant3 { field1: data_type1, field2: data_type2 },
}

使用枚举

要使用枚举,可以使用枚举的成员名。

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

fn move_player(direction: Direction) {
    match direction {
        Direction::Up => println!("Moving up!"),
        Direction::Down => println!("Moving down!"),
        Direction::Left => println!("Moving left!"),
        Direction::Right => println!("Moving right!"),
    }
}

fn main() {
    let dir = Direction::Up;
    move_player(dir);
}

枚举带数据

rust 的枚举可以包含与它们的变体关联的数据。

enum Message {
    Quit,
    Write(String),
    Move { x: i32, y: i32 },
}

fn process_message(message: Message) {
    match message {
        Message::Quit => println!("Quitting!"),
        Message::Write(text) => println!("Writing: {}", text),
        Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
    }
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Write(String::from("Hello, World!"));
    let msg3 = Message::Move { x: 10, y: 20 };

    process_message(msg1);
    process_message(msg2);
    process_message(msg3);
}

rust 的枚举在很多地方都得到广泛应用,例如在错误处理(Result 枚举),可选值(Option 枚举)等场景。

结构体(Struct)类型

rust 中,结构体(Struct)是一种自定义的数据类型,用于组合多种类型的值。结构体(Struct)是 rust 中一个非常重要的复合数据类型,这里简单介绍一下,后续会专门开一篇文章介绍。

结构体的定义

结构体使用 struct 关键字定义。

struct StructName {
    field1: Type1,
    field2: Type2,
    // ...
}

创建结构体实例

可以通过使用构造函数语法来创建结构体的实例。

let person = Person {
    name: String::from("Alice"),
    age: 30,
};

访问属性

可以通过点操作符(.)来访问结构体的属性。

println!("Name: {}, Age: {}", person.name, person.age);

结构体方法

使用 impl 关键字可以为结构体定义方法。方法的第一个参数是 self,表示结构体的实例。

impl Person {
    fn introduce(&self) {
        println!("Hi, I'm {} and I'm {} years old.", self.name, self.age);
    }
}

person.introduce();

结构体在 rust 中是一个非常强大的特性,允许开发者定义复杂的数据类型,并为其提供方法以实现更复杂的业务逻辑。

总结

复合类型在 rust 中功能非常强大,它们允许开发者将多种数据组合在一起,并提供了一种有组织的方式来处理复杂的数据结构。在实际应用中,选择合适的复合类型可以提高代码的可读性和可维护性。