第四章 Rust 复杂数据结构

Rust 基础入门指南:为 Solana 合约学习铺路 https://learnblockchain.cn/column/113

一、字符串

动态字符串切片

字符串切片是引用类型,类型为 &str,它通过索引或范围来指定字符串的一部分,提供了对字符串的引用,而不引入额外的内存开销。切片并不拥有字符串的内容,因此不会消耗额外的内存。

fn main() {
    let s: String = String::from("Rust is powerful!");

    // 从索引 0 开始,获取前 4 个字节
    let slice1: &str = &s[0..4];

    // 默认从索引 0 开始,获取前 4 个字节
    let slice2: &str = &s[..4];

    let len: usize = s.len();

    // 从索引 5 开始,获取到字符串末尾
    let slice3: &str = &s[5..len];

    // 默认从索引 5 开始,获取到字符串末尾
    let slice4: &str = &s[5..];

    // 获取整个字符串的切片
    let slice5: &str = &s[0..len];

    // 同上,获取整个字符串的切片
    let slice6: &str = &s[..];

    // 输出结果
    println!("slice1: {}", slice1);  // 输出: Rust
    println!("slice2: {}", slice2);  // 输出: Rust
    println!("slice3: {}", slice3);  // 输出: is powerful!
    println!("slice4: {}", slice4);  // 输出: is powerful!
    println!("slice5: {}", slice5);  // 输出: Rust is powerful!
    println!("slice6: {}", slice6);  // 输出: Rust is powerful!

    let chinese_string = "编程语言";
    // 错误示例:试图获取 "编" 的前两个字节,但 "编" 占用 3 个字节
    // let wrong_slice = &chinese_string[0..2]; // 编译通过,但运行时会导致 panic

    // 正确示例:获取 "编" 的完整字节范围
    let correct_slice = &chinese_string[0..3];
    println!("正确的切片: {}", correct_slice); // 输出 "编"
}

索引范围是前闭后开的,即包含开始位置,但不包含结束位置。

字符串字面量与动态字符串

字符串字面量是在代码中直接写死的字符串,比如 "Rust is awesome!"。它们在程序编译时就已经确定并固定下来,类型为 &str。与动态字符串不同,字符串字面量是不可变的。

字符串字面量在编译时已知大小,因此它的生命周期与整个程序的运行期一致。这使得它们在内存中更加高效,而无需像动态字符串那样进行内存分配。

fn main() {
    // 字符串字面量转动态字符串
    let s1: String = "Rust is cool".to_string();
    let s2: String = String::from("Rust is powerful");

    // 动态字符串转字符串字面量
    let s3: &str = s1.as_str();

    println!("s1: {}", s1);  // 输出: Rust is cool
    println!("s2: {}", s2);  // 输出: Rust is powerful
    println!("s3: {}", s3);  // 输出: Rust is cool
}

动态字符串操作

fn main() {
    let mut s = String::from("Hello");

    // 追加字符串,修改原来的字符串
    s.push_str(", world");
    println!("追加字符串 push_str() -> {}", s);

    // 追加单个字符
    s.push('!');
    println!("追加字符 push() -> {}", s);

    // 在指定位置插入字符,修改原来的字符串
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);

    // 在指定位置插入字符串
    s.insert_str(6, " how are you?");
    println!("插入字符串 insert_str() -> {}", s);

    // 替换字符串中的某个子串,返回新字符串
    let str_old = String::from("I like rust, rust is great!");
    let str_new = str_old.replace("rust", "Rust");
    println!("原字符串:{}, 新字符串:{}", str_old, str_new);

    // pop 删除操作,修改原来的字符串
    let mut string_pop = String::from("删除操作,rust 中文!");
    // 删除末尾字符
    let p1 = string_pop.pop();
    println!("删除字符 pop() -> {:#?}", p1); // 输出:Some('!')
    println!("剩余字符串: {}", string_pop);
    
    // 删除末尾字符,再次 pop
    let p2 = string_pop.pop();
    println!("删除字符 pop() -> {:?}", p2); // 输出:Some('中')
    println!("剩余字符串: {}", string_pop);
}

二、元组

元组是一种复合类型,它将多个不同类型的值组合在一起,并且长度和顺序都是固定的。它们通过圆括号 ( ) 定义,元素之间用逗号 , 分隔。

想象你有一个旅行背包,里面装着不同种类的物品,比如护照(字符串)、现金(整数)、指南针(浮点数)。无论里面的物品是什么类型,它们都属于同一个背包(元组),并且位置固定。

定义元组

// 创建一个包含不同类型元素的元组
let info: (i32, bool, f64, &str) = (42, true, 3.14, "Rust");
// 元组可以嵌套
let nested_tuple: (u8, (char, &str)) = (1, ('A', "嵌套"));

访问元组元素

fn main() {
    let person: (&str, i32, f64) = ("Alice", 30, 1.65);
    
    // 解构方式获取元组元素
    let (name, age, height) = person;
    println!("{} is {} years old and {}m tall.", name, age, height);
    
    // 使用索引访问元组元素
    println!("Name: {}, Age: {}, Height: {}", person.0, person.1, person.2);
}

作为函数返回值

fn square_and_cube(n: i32) -> (i32, i32) {
    (n * n, n * n * n)
}

fn main() {
    let (square, cube) = square_and_cube(3);
    println!("Square: {}, Cube: {}", square, cube);
}

三、结构体

结构体是一种自定义数据类型,可以组织多个相关的字段,使代码更清晰易读。可以将结构体看作是人物档案,每个字段代表一个特定信息,比如姓名、年龄、职业等。

基础语法

// 定义结构体
struct Book {
    title: String,
    author: String,
    pages: u32,
    is_hardcover: bool,
}

fn main() {
    // 实例化
    let rust_book = Book {
        title: String::from("The Rust Programming Language"),
        author: String::from("Steve Klabnik and Carol Nichols"),
        pages: 552,
        is_hardcover: true,
    };
    println!("Book: {} by {}", rust_book.title, rust_book.author);

    // 通过已有结构体创建新实例
    let book2 = Book {
        title: String::from("Advanced Rust"),
        ..rust_book
    };

    println!("{} - {} pages", book2.title, book2.pages);
}

元组结构体(Tuple Struct)

如果你不需要字段名称,但仍然希望使用结构体的特性,可以使用元组结构体。

struct Coordinates(i32, i32, i32);

fn main() {
    let origin = Coordinates(0, 0, 0);
    println!("Origin is at ({}, {}, {})", origin.0, origin.1, origin.2);
}

四、枚举

枚举是一种用户自定义的数据类型,它允许一个类型有多个不同的可能值,每个值称为一个变体(variant)。

基础语法

enum CoffeeSize {
    Small,
    Medium,
    Large,
}

enum Message {
    Text(String),
    Move { x: i32, y: i32 },
    ChangeColor(i32, i32, i32),
}

fn main() {
    let my_coffee = CoffeeSize::Medium;
    match my_coffee {
        CoffeeSize::Small => println!("You chose a small coffee."),
        CoffeeSize::Medium => println!("You chose a medium coffee."),
        CoffeeSize::Large => println!("You chose a large coffee."),
    }

    let msg1 = Message::Text(String::from("Hello, Rust!"));
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::ChangeColor(255, 0, 0);
}

Option 枚举

Rust 没有 null,取而代之的是 Option<T> 枚举,用于表示可能为空的值。它的定义如下:

// 它有两个枚举值,Some(T): 包含一个具体的值 T,以及None: 表示没有值。
enum Option<T> {
    None,
    Some(T),
}
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    match divide(10.0, 2.0) {
        Some(result) => println!("Result: {}", result),
        None => println!("Cannot divide by zero!"),
    }
}

五、数组

数组是由多个相同类型的元素组合而成的集合。在 Rust 中,数组主要分为两类:

  • 静态数组 (array):分配在栈上,长度固定,访问速度快。
  • 动态数组 (Vector):分配在堆上,长度可变,但操作会带来额外的性能开销。

静态数组的特点类似于一个固定大小的书架,一旦确定其大小,就无法再改变。相比之下,动态数组更像是一个可以随时调整大小的抽屉,可以根据需要扩展或缩小。

静态数组

fn main() {
    // 由编译器推断类型
    let a = [10, 20, 30, 40, 50];

    // 显式指定类型和长度
    let b: [i32; 5] = [10, 20, 30, 40, 50];

    // 创建包含相同元素的数组
    let c = [7; 5]; // c = [7, 7, 7, 7, 7]

    // 静态数组:长度固定,存储在栈上
    let a = [5u8; 5]; // a = [5, 5, 5, 5, 5]

    // 非基础类型的数组需要使用 `std::array::from_fn` 来初始化
    let b: [String; 3] = std::array::from_fn(|_| "hello".to_string());
    // b = ["hello", "hello", "hello"]

    // 数组的元素访问
    let c: [u8; 5] = [15, 25, 35, 45, 55];
    let first = c[0]; // first = 15
    let second = c[1]; // second = 25

    // 访问数组越界元素(会导致编译错误)
    // let none_element = c[100];

    // 二维数组:每个元素都是一个 `[u8; 5]` 类型的数组
    let arrays: [[u8; 5]; 2] = [a, c];
}

动态数组

动态数组 Vec<T> 是 Rust 中可变长度的集合,允许在运行时动态调整大小。与 String 不同,Vec<T> 是通用的,可存储任意类型的元素。

fn main() {
    // 显式声明类型
    let v1: Vec<i32> = Vec::new();
    // v1.push(1); 编译错误

    // 让编译器推断类型。
    let mut v2 = Vec::new();
    v2.push(10);

    // 使用 vec! 宏创建并初始化
    let v3 = vec![10, 20, 30];

    // 使用 [初始值;长度] 来创建数组
    let v4 = vec![1; 3]; // v4 = [1, 1, 1]

    // 使用from语法创建数组
    let v5 = Vec::from([1, 1, 1]);

    let mut v = vec![1, 2, 3, 4, 5];

    // 访问元素
    let first: &i32 = &v[0];
    println!("第一个元素是 {}", first);

    // 通过 get 方法安全访问
    if let Some(last) = v.get(4) {
        println!("最后一个元素是 {}", last);
    }

    // 修改元素
    for i in &mut v {
        *i *= 2;
    }
    println!("修改后的 v: {:?}", v); // [2, 4, 6, 8, 10]

    // 删除元素
    v.pop();
    println!("删除最后一个元素后: {:?}", v); // [2, 4, 6, 8]

    v.remove(1);
    println!("删除索引 1 处的元素后: {:?}", v); // [2, 6, 8]

    /* 自动扩容 */
    let mut v = vec![1, 2, 3, 4];

    println!("初始容量: {}", v.capacity());
    println!("初始 v[0] 的地址: {:p}", &v[0]);

    v.push(5);
    println!("添加一个元素后,容量: {}", v.capacity());
    println!("添加后 v[0] 的地址: {:p}", &v[0]);

    v.push(6);
    println!("再次添加元素后,容量: {}", v.capacity());
    println!("再次添加后 v[0] 的地址: {:p}", &v[0]);
}

初始时 Vec 的容量可能是 4,存储在堆内存中,而栈上存储着指向堆的指针。当 push(5) 触发扩容时,Vec 会申请更大的空间(通常是当前容量的 2 倍),并将数据复制到新的地址。

六、hashmap

// 由于 HashMap 并没有包含在 Rust 的 prelude 库中,所以需要手动引入
use std::collections::HashMap;

fn main() {
    // 创建一个HashMap,用于存储学生姓名和他们的年龄
    let mut student_ages = HashMap::new();
    student_ages.insert("Alice", 20);
    student_ages.insert("Bob", 19);
    student_ages.insert("Charlie", 21);

    // 创建一个指定容量的 HashMap,用于存储学生姓名和他们的身高
    // 这样可以预先分配内存,减少后续插入时的内存分配开销
    let mut student_heights = HashMap::with_capacity(4);
    student_heights.insert("Alice", 165);
    student_heights.insert("Bob", 180);
    student_heights.insert("Charlie", 175);
    student_heights.insert("David", 170);

    // 打印 HashMap 的内容
    println!("学生年龄: {:?}", student_ages);
    println!("学生身高: {:?}", student_heights);
}

复杂一点的例子:

use std::collections::HashMap;

fn main() {
    // 创建一个包含用户姓名和积分的列表
    let user_list = vec![("Alice", 300), ("Bob", 250), ("Eve", 150), ("Mallory", 50)];

    // 将列表转换为 HashMap
    let mut user_map: HashMap<&str, i32> = user_list.into_iter().collect();
    println!("用户积分: {:?}", user_map);

    // 获取元素
    let alice_points = user_map["Alice"];
    println!("Alice 的积分: {}", alice_points);

    // 使用 get 方法安全获取元素
    let alice_points = user_map.get("Alice");
    println!("Alice 的积分: {:?}", alice_points);

    // 尝试获取不存在的键
    let trent_points = user_map.get("Trent");
    println!("Trent 的积分: {:?}", trent_points);

    // 覆盖已有的值,insert 会返回旧值
    let old = user_map.insert("Alice", 400); // 更新 Alice 的积分
    assert_eq!(old, Some(300)); // 确认旧值是 300

    // 使用 entry 方法插入或获取值
    let v = user_map.entry("Trent").or_insert(100); // Trent 不存在,插入 100
    assert_eq!(*v, 100);

    let v = user_map.entry("Trent").or_insert(200); // Trent 已存在,不会修改
    assert_eq!(*v, 100);

    // 打印更新后的 HashMap
    println!("更新后的用户积分: {:?}", user_map);
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容