Rust 之五 所有权、.. 和 _ 语法、引用和切片、Vec<T>、HashMap<K, V>

概述

Rust 的基本语法对于从事底层 C/C++ 开发的人来说多少有些难以理解,虽然官方有详细的文档来介绍,不过内容是相当的多,看起来也费劲。本文通过将每个知识点简化为 一个 DEMO + 每种特性各用一句话描述的形式来简化学习过程,提高学习效率。

所有 DEMO 可以从 https://gitee.com/itexp 获取

所有权

所有权(ownership)是 Rust 用于如何管理内存的一组规则。它决定了数据的生命周期、如何管理内存,并且确保数据在使用过程中不发生数据竞争。所有权系统可以有效地避免内存泄漏和悬挂指针(dangling pointers)等问题,从而保证程序的安全性和高效性。

  1. Rust 中的每一个值都有一个 所有者(owner)

    • 每个值(数据)都有一个与之相关联的变量,称为所有者(owner)。
    • 每当值被创建时,它有一个明确的所有者。
  2. 值在任一时刻有且只有一个所有者

    • 变量的所有者是唯一的,这意味着当一个值的所有者发生转移时,原所有者将不再拥有对该值的访问权限。
  3. 当所有者(变量)离开作用域,这个值将被丢弃

    • 当一个变量超出其作用域时,Rust 会自动释放该变量占用的内存。这个过程被称为"销毁"或"释放"(drop)。
  4. 编译器在编译时会根据一系列的规则进行检查,如果违反了任何这些规则,程序都不能编译。

转移(Move)

当将一个变量赋值给另一个变量、作为函数参数传递或作为返回值时,如果类型未实现 Copy trait,则 Rust 会执行转移操作。此时,原变量失去所有权,无法再被使用。

rust 复制代码
fn main() {
    let s1 = String::from("Hello"); // s1 是 String 类型的所有者
    let s2 = s1;  // s1 的所有权被转移给 s2

    // println!("{}", s1); // 错误!s1 的所有权已转移,不能再使用 s1
    println!("{}", s2); // 正确!s2 是所有者
}

克隆(Clone)

如果需要在转移所有权时保留原始值,可以通过 克隆(clone)来创建一个值的副本,这样两个变量可以各自拥有一份数据。新的数据与原数据是没有任何关系两个独立值,他们的生命周期、作用域等都可以不同。

rust 复制代码
fn main() {
    let s1 = String::from("Hello");
    let s2 = s1.clone(); // 克隆 s1,s2 拥有一份新副本

    println!("{}", s1); // 正常工作!s1 仍然有效
    println!("{}", s2); // 正常工作!s2 是 s1 的副本
}

.. 语法

在 Rust 中,.. 语法主要用于表示范围或忽略某些内容。

表示范围

.. 可以作为范围操作符。基本格式有 start_index..end_index 是前闭后开区间;start_index..=end_index 是闭区间;start_index.. 是从 start_index 开始的所有内容;..end_index 是表示到 end_index 结束的所有内容;只写 .. 则表示全范围。

其中,start_indexend_index 可以是任意整数类型(i32、u32、i64 等),并且可以省略其一或全部。如果要用负数,则前提是我们的范围本身支持使用负数索引!

  1. 如果将 start_indexend_index 指定为浮点数,则编译会报错

  2. start_index >= end_index 时,则表示为空范围

  3. start_index..end_indexstd::ops::Range<Idx> 类型;start_index..=end_indexstd::ops::RangeInclusive<Idx> 类型

定义范围变量

.. 可以作为范围操作符用来定义范围变量。

rust 复制代码
fn main() {
    /* ========================= 闭区间 ============================ */
    let range = -5..=5;     // 从 -5 到 5(包括 5)
    for i in range {
        println!("{}", i);  // 输出 -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5
    }

    /* ========================= 前闭后开区间 ============================ */
    let range = 1..5;       // 包含 1 到 4 的数字,不含 5
    for i in range {
        println!("{}", i);  // 输出 1, 2, 3, 4
    }

    /* ========================= 只有开始区间 ============================ */
    let range = 3..;            // 从 3 开始,直到无限大
    for i in range.take(5) {    // take(5) 限制最多打印 5 个数字
        println!("{}", i);      // 输出 3, 4, 5, 6, 7
    }

    /* ========================= 只有结束区间 ============================ */
    // let range = ..5;  // 包含 0 到 4 的数字
    // for i in range {
    //     println!("{}", i);
    // }
}

定义切片

.. 可以作为范围操作符可以用来定义切片。

rust 复制代码
    let arr = [0, 1, 2, 3, 4];
    let slice = &arr[1..4]; // [1, 2, 3]
    println!("Slice: {:?}", slice);

模式匹配

.. 可以用在模式匹配中以匹配某个范围。

rust 复制代码
let num = 42;
match num {
    1..=10 => println!("1到10"),
    11..50 => println!("11到49"), // 不包含50
    _ => println!("其他"),
}

忽略某些内容

_ 忽略单个值,.. 忽略剩余所有值。

解构操作

在解构结构体或者元组时,.. 语法用于表示忽略某些字段。它是解构模式的一部分,可以让你在解构时不关心结构体的某些字段,只关心你需要的字段。

rust 复制代码
let point = (3, 5, 10);
// 只取第一个值,忽略后续所有值
let (x, ..) = point;
println!("x = {}", x); // x = 3

struct Config {
    width: u32,
    height: u32,
    debug: bool,
}
fn init_config(Config { width, height, .. }: Config) {
    println!("初始化尺寸: {}x{}", width, height);
}

结构体更新语法

在结构体更新语法中,可以使用 ..结构体变量 的形式来用旧的结构体变量初始化新的结构体,而无需显示指明所有结构体成员挨个赋值。

rust 复制代码
    #[derive(Debug)]
    struct Point { x: i32, y: i32 }
    let p1 = Point { x: 10, y: 20 };
    let p2 = Point { x: 30, ..p1 };	// 其中的 ..p1 表示自动使用 p1 的其他值
    println!("p2: {:?}", p2); // Point { x: 30, y: 20 }

.. 必须放在结构体字面量的最后,且只能使用一次

模式匹配

在匹配匹配中,可以使用 .. 格式来忽略其他字段。

rust 复制代码
match point {
    (0, ..) => println!("x 为 0"),
    (.., 0) => println!("z 为 0"),
    _ => println!("其他情况"),
}

_ 语法

在 Rust 中,_ 通常用于表示忽略个元素或由编译器推断类型的占位符。

忽略某个元素

_ 忽略单个值,.. 忽略剩余所有值。

忽略未使用的变量

rust 复制代码
let _ = 42; // 明确忽略值,避免编译器警告
let s = String::from("hello");
let _ = s; // s 立即被 Drop
// let _x = s; // s 会在 _x 的作用域结束时 Drop

忽略函数参数和返回值

rust 复制代码
fn foo(_: i32) {
    println!("忽略参数");
}
foo(10); // 调用时传入的参数被忽略

let _ = setup_test_env(); // 忽略初始化返回值

忽略模式匹配的部分值

rust 复制代码
match Some(42) {
    Some(_) => println!("有值,但忽略具体内容"),
    None => println!("无值"),
    _ => println!("其他"),
}

忽略结构体字段

rust 复制代码
struct Point { x: i32, y: i32 }
let p = Point { x: 10, y: 20 };
let Point { x: _, y } = p; // 只绑定 y,忽略 x

忽略元组值

rust 复制代码
let (x, _) = (1, 2); // 只绑定元组的第一个元素,忽略第二个

类型推断占位符

泛型类型推断

rust 复制代码
let numbers: Vec<_> = vec![1, 2, 3].into_iter().collect(); 
// 编译器推断 `Vec<i32>`,等价于 `Vec<i32>`
use std::collections::HashMap;
let map: HashMap<_, _> = vec![("a", 1), ("b", 2)].into_iter().collect();
// 编译器推断为 `HashMap<&str, i32>`

闭包参数类型推断

rust 复制代码
let square = |x: _| x * x; // 错误:不能直接用于闭参
let square = |x| x * x; // 正确:编译器自动推断为 `i32`(根据上下文)

生命周期占位符

作为生命周期占位符时,需要符合生命周期标准的格式 '_。关于声明周期,我们将在 Rust 之六 语句和表达式、作用域、生命周期、变量与常量、控制流 中详细学习!

rust 复制代码
struct Parser<'a> {
    data: &'a str,
}

impl Parser<'_> { // 等价于 impl<'a> Parser<'a>
    fn parse(&self) -> &str {
        self.data
    }
}

数字字面量分隔符

rust 复制代码
let million = 1_000_000; // 提高可读性,等价于 1000000
let hex = 0xdead_beef;  // 十六进制分隔

集合类型

集合类型(Collections)这种数据结构是用于存储和管理多个数据的容器,Rust 标准库中提供了 Vec<T>HashMap<K, V>HashSet<T>BTreeMap<K, V>BTreeSet<T>LinkedList<T>VecDeque<T> 等集合类型。

不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的

Vec<T>

Vec<T>(动态数组)类型通常被称为 Vector(向量)类型,它允许我们用一个单独的数据结构中在堆内存中彼此相邻 地排列的储存多于一个相同类型的值。它是一个动态数组,可以根据需要自动增长和缩小。

  1. Vec 会根据需要自动调整其容量。当 Vec 的容量不够时,它会自动重新分配更多内存。

结构

Vec<T> 内部由一个指向堆内存中数据的起始地址(*mut T)的指针、 指示当前存储的元素数量的长度(len)以及表示分配的内存可容纳的元素总数(len ≤ capacity)的容量(capacity) 这三个部分组成。

rust 复制代码
pub struct Vec<T> {
    ptr: NonNull<T>, // 堆内存指针
    len: usize,      // 当前元素数量
    cap: usize,      // 分配的总容量
}

定义

  1. Rust 为 Vector 提供了很多预定义的接口,可以调用 Vec::new() 函数来创建一个新的空 Vector。

    rust 复制代码
    let v: Vec<i32> = Vec::new();   // 显示的将 `Vec<T>` 中的 `T` 指定为了 `i32` 类型。
    println!("{:?}", v);            // {:?} 是调试格式化输出,输出 Vec 的内容
    let mut v: Vec<i32> = Vec::new();
    v.push(1);                  // 向向量中添加元素
    v.push(2);
    println!("{:?}", v);
  2. 最常用的方法是以 let 变量名 = vec![元素 1, 元素 2, 元素 n]; 这样的格式用初始值来创建一个 Vec<T> 而让 Rust 推断出储存值的类型。

    rust 复制代码
    let v = vec![1, 2, 3];          // vec! 宏创建一个 Vec,类型由编译器推断
    println!("{:?}", v);
    let mut v = vec![1, 2, 3];
    v[0] = 4;                       // 修改第一个元素
    v.push(5);                      // 在末尾添加元素
    println!("{:?}", v);

    其中,vec! 是一个宏,这个宏会根据我们提供的值来创建一个新的 Vector,Rust 可以推断出变量 v 的类型是 Vec<i32>

  3. 还可以用 Rust 为 Vector 提供预接口 with_capacity() 来预分配容量,然后后续补充值

    rust 复制代码
    let mut v = Vec::with_capacity(10); // 初始容量为 10,避免频繁扩容
    v.push(1);
    v.push(2);
    println!("{:?}", v);
  4. Vec 在 Rust 中是拥有所有权的,这意味着当 Vec 被移动到另一个变量时,原来的变量就不再可用。

    rust 复制代码
    let v1 = vec![1, 2, 3];
    let v2 = v1;  // v1 的所有权转移给 v2
    // println!("{:?}", v1);  // 错误,v1 不再有效
    println!("{:?}", v2);  // 输出 [1, 2, 3]

访问元素

无论 Vector 是否可变,都是可以使用 变量名[从 0 开始的索引] 来访问其中的元素。此外,还可以使用 Rust 还为 Vector 提供的一些常用的方法来访问。

rust 复制代码
    let mut v = vec![1, 2, 3];
    println!("v[0]: {}", v[0]); // 访问第一个元素
    println!("v[1]: {}", v[1]); // 访问第二个元素
    println!("v[2]: {}", v[2]); // 访问第三个元素
    println!("v.len(): {}", v.len()); // 获取向量长度
    println!("v.is_empty(): {}", v.is_empty()); // 判断向量是否为空
    println!("v.capacity(): {}", v.capacity()); // 获取向量容量
    println!("v.get(0): {:?}", v.get(0)); // 获取第一个元素,返回 Option<&i32>
    println!("v.get(3): {:?}", v.get(3)); // 获取第四个元素,返回 None
    println!("v.pop(): {:?}", v.pop()); // 弹出最后一个元素,返回 Option<i32>
    println!("v: {:?}", v); // 弹出后向量的内容
    println!("v.remove(0): {:?}", v.remove(0)); // 删除第一个元素,返回 i32
    println!("v: {:?}", v); // 删除后向量的内容
    println!("v.clear()"); // 清空向量
    v.clear(); // 清空向量
    println!("v: {:?}", v); // 清空后向量的内容
    println!("v.is_empty(): {}", v.is_empty()); // 判断向量是否为空
    println!("v.capacity(): {}", v.capacity()); // 获取向量容量
    println!("v.push(1)"); // 向向量中添加元素
    v.push(1); // 向向量中添加元素
    println!("v: {:?}", v); // 添加后向量的内容
    println!("v.len(): {}", v.len()); // 获取向量长度

迭代遍历

rust 复制代码
    let v = vec![1, 2, 3];
    for num in &v {          // 不可变迭代
        println!("{}", num);    // 1, 2, 3
    }
    let mut v = vec![1, 2, 3];
    for num in &mut v {   // 可变迭代
        *num *= 2;              // 修改元素值
    }
    println!("vec: {:?}", v);

HashMap<K, V>

HashMap<K, V> 是标准库提供的哈希表的实现,是一个存储键值对(key-value pair)的集合类型,。可以非常高效地进行查找、插入和删除操作。

  1. K 为键类型,V 为值类型
  2. 键 (K) 必须实现 Eq 和 Hash trait(如 String、i32 等),值 (V) 无特殊要求。
  3. HashMap 是基于哈希表实现的,所以查找操作的时间复杂度通常是 O(1),但在最坏情况下会退化为 O(n),具体取决于哈希冲突的情况。

定义

  1. Rust 为 HashMap 提供了很多预定义的接口,可以调用 HashMap::new() 函数来创建一个新的空 HashMap

    rust 复制代码
    let mut map = HashMap::new();       // 空 HashMap
    map.insert("key1".to_string(), 42); // 插入键值对
    println!("{:?}", map);              // {"key1": 42}
  2. 使用 HashMap::from() 或迭代器构建。

    rust 复制代码
    let mut map = HashMap::from([
        ("apple".to_string(), 3),
        ("banana".to_string(), 5),
    ]);
    map.insert("orange".to_string(), 2); // 插入键值对
    println!("{:?}", map);              // {"apple": 3, "banana": 5, "orange": 2}
    
    let teams = vec![("Alice", 100), ("Bob", 90)];
    let scores: HashMap<_, _> = teams.into_iter().collect();
    println!("{:?}", scores);           // {"Alice": 100, "Bob": 90}
  3. 还可以用 Rust 为 HashMap 提供接口 HashMap::with_capacity() 来预分配容量,然后后续补充值

    rust 复制代码
    let mut map = HashMap::with_capacity(100);
  4. HashMap 在 Rust 中是拥有所有权的,这意味着当 HashMap 被移动到另一个变量时,原来的变量就不再可用。

    rust 复制代码
    let mut map = HashMap::new();       // 空 HashMap
    map.insert("key1".to_string(), 42); // 插入键值对
    let map1 = map;                     // map 的所有权转移给 map1
    // println!("{:?}", map);           // 错误:使用了已转移所有权的变量
    println!("{:?}", map1);             // {"key1": 42}

访问元素

无论 HashMap 是否可变,都是可以使用 变量名[KEY] 来访问其中的元素。此外,此外,还可以使用 Rust 还为 HashMap 提供的一些常用的方法来访问。

rust 复制代码
    let mut map = HashMap::new();       // 空 HashMap
    map.insert("key1".to_string(), 42); // 插入键值对
    map.insert("key2".to_string(), 43); // 插入键值对
    map.insert("key3".to_string(), 44); // 插入键值对
    let value = map["key2"];            // 如果键存在,否则会 panic
    println!("Value: {}", value);       // 输出 Value: 43
    println!("{:?}", map.get("key1"));  // Some(42)
    println!("{:?}", map.get("key4"));  // None
    println!("{:?}", map.get("key1").unwrap()); // 42
    let mut map = HashMap::new();       // 空 HashMap
    map.insert("key1".to_string(), 42); // 插入键值对
    map.insert("key2".to_string(), 43); // 插入键值对
    map.insert("key3".to_string(), 44); // 插入键值对
    println!("{:?}", map);              // {"key1": 42, "key2": 43, "key3": 44}
    map.remove("key1");                 // 删除键为 key1 的元素
    println!("{:?}", map);              // {"key2": 43, "key3": 44}
    map.clear();                        // 清空哈希表
    println!("{:?}", map);              // {}
    map.insert("key1".to_string(), 42); // 插入键值对
    println!("{:?}", map.len());        // 1
    println!("{:?}", map.is_empty());   // false
    map.remove("key1");                 // 删除键为 key1 的元素
    println!("{:?}", map.is_empty());   // true

迭代遍历

rust 复制代码
    let mut map = HashMap::new();       // 空 HashMap
    map.insert("key1".to_string(), 42); // 插入键值对
    map.insert("key2".to_string(), 43); // 插入键值对
    map.insert("key3".to_string(), 44); // 插入键值对
    for (key, value) in &map {          // 不可变遍历哈希表
        println!("{}: {}", key, value); // key1: 42
    }
    for (_, value) in &mut map {        // 可变遍历哈希表
        *value *= 2;
    }
    for key in map.keys() {             // 遍历键
        println!("{}", key);             // key1
    }
    for value in map.values() {         // 遍历值
        println!("{}", value);           // 42
    }
    for (key, value) in map.iter() {    // 遍历键值对
        println!("{}: {}", key, value); // key1: 42
    }

引用(借用)

在 Rust 中,引用(reference) 是对某个值的借用 ,它允许你在不获取值所有权的情况下访问该值。引用帮助我们管理内存,避免不必要的内存拷贝,同时保证内存安全。

引用(Reference)像一个指针,因为它代表了一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。与指针不同,引用可以确保指向某个特定类型的有效值。

Rust 中的引用分为用 &数据类型 表示的不可变引用和用 &mut 数据类型 表示的可变引用两种类型。其中的 数据类型 可以是标量类型、复合类型、结构体等类型 ,详见之前博文 Rust 之四 运算符、标量、元组、数组、字符串、结构体、枚举 中的介绍。

  1. 由于标量类型实现了 Copy trait,因此,不会进行所有权的转移。

  2. 数组的引用比较特殊,其格式为 &[T; N]

  3. 一个数据在同一时刻只能有一个可变引用,或者多个不可变引用,但不能同时拥有可变和不可变引用

不可变引用

不可变引用(Immutable Reference)允许你在不修改原始数据的情况下访问它。Rust 中使用 let 引用名: &数据类型 = &变量或值; 来创建不可变引用。允许同时创建同一值的多个不可变引用,因为它们不会改变数据。

rust 复制代码
/* ======================= 不可变引用作为函数的参数 =========================== */
fn calculate_length(s: &String, a: &i32) -> usize {
    println!("s: {}, a: {}", s, a);
    s.len()
}

fn main() {
    /* ======================== 标量类型不可变引用 =========================== */
    let x = 5;
    let y1: &i32 = &x;  // 显示指定类型。
    let y2 = &x;        // 自动推导类型。可以同时拥有多个不可变引用
    println!("x: {}, y1: {}, y2: {}", x, y1, y2);
    /* ========================= 字符串不可变引用 ============================ */
    let s = String::from("Hello, world!"); // 创建一个 String
    let r1: &String = &s;   // 显示指定类型。
    let r2 = &s;            // 自动推导类型。可以同时拥有多个不可变引用
    println!("r1: {}, r2: {}", r1, r2); // 可以同时拥有多个不可变引用
    /* ========================= 数组不可变引用 ============================ */
    let x = [1, 2, 3, 4, 5];
    let r1: &[i32; 5] = &x; // 显示指定类型。
    let r2 = &x;            // 自动推导类型。可以同时拥有多个不可变引用
    println!("r1: {:?}, r2: {:?}", r1, r2); // 可以同时拥有多个不可变引用
    /* ====================== 不可变引用作为函数参数 ========================== */
    let a = 10;
    let s = String::from("Hello, world!");
    let len = calculate_length(&s, &a);
    println!("The length of '{s}' is {len}");
}
  1. 不可变引用可以作为函数的参数。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!

可变引用

可变引用(Mutable Reference)允许修改所引用的原始数据的值。Rust 中使用 let 引用名: &mut 数据类型 = &mut 变量或值; 来创建可变引用。在同一作用域内,只能有一个可变引用可以存在,也不能同时拥有可变引用和不可变引用 ,也不能同时拥有可变引用和对该变量的直接使用

rust 复制代码
// 可变引用作为函数的参数
fn change(s: &mut String, a: &mut i32) {
    s.push_str(", world!");
    *a += 1;
}

fn main() {
    /* ======================== 标量类型可变引用 =========================== */
    let mut x = 5;
    let y1: &mut i32 = &mut x;          // 指定类型的不可变引用 y1
    let y1 = &mut x;                    // 自动推导类型
    let y2 = &mut x;                    // 【错误】同一时刻不能有多个可变引用。
    let y3 = &x;                        // 【错误】不能同时拥有可变引用和不可变引用
    println!("x: {}, y1: {}", x, y1);   // 【错误】不能同时拥有可变引用和对该变量的直接使用
    let x = 6;
    let y1: &mut i32  = &mut x;         // 【错误】x 本身是不可变的,无法创建可变引用
    println!("y1 = {y1}");
    /* ================== 不可变引用和可变引用不能同时存在 ===================== */
    let mut x = String::from("Hello");
    let y1: &mut String = &mut x;       // 指定类型的不可变引用 y1
    let y1 = &mut x;                    // 自动推导类型
    let y2 = &mut x;                    // 【错误】同一时刻不能有多个可变引用。
    let y3 = &x;                        // 【错误】不能同时拥有可变引用和不可变引用
    println!("x: {}, y1: {}", x, y1);   // 【错误】不能同时拥有可变引用和对该变量的直接使用
    let x = String::from("Hello");
    let y1: &mut String  = &mut x;      // 【错误】x 本身是不可变的,无法创建可变引用
    println!("y1 = {y1}");
    /* ====================== 可变引用作为函数参数 ========================== */
    let mut s = String::from("Hello");
    let mut a = 1;
    change(&mut s, &mut a);
    println!("s = {s}, a = {a}");
}
  1. 可变引用对应的原数据也必须是可变的。

  2. 可变引用可以作为函数的参数。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!

  3. 注意 let r3 = &mut s;r3 是一个不可变的可变引用,也就意味着,我们不能更改 r3,例如修改 r3 = &mut s1 是不允许的

  4. 可变性必须从最外层开始显式声明

悬垂引用

悬垂引用(Dangling Reference)是指一个引用指向一个已经被销毁(例如,离开作用域)的内存位置。Rust 编译器会通过严格的所有权和生命周期机制,从根本上杜绝了悬垂引用的出现。悬垂引用主要有以下两种情况,Rust 编译器会直接报错,不允许编译。

  1. 引用一个已经被销毁的对象:例如,当一个变量在某个作用域结束后被销毁,但仍然有引用指向它时,就会出现悬垂引用。

    rust 复制代码
    let r;
    {
        let mut x = 5;
        r = &mut x;         // r 的生命周期绑定到 x
    }                       // x 在此处被释放,r 成为悬垂引用
    // println!("{}", r);   // 错误:使用悬垂引用
  2. 在栈上引用局部变量并返回该引用:例如,返回指向局部变量的引用,局部变量在函数结束时会被销毁。

    rust 复制代码
    fn dangle() -> &String { // 错误:缺失生命周期参数
        let s = String::from("hello");
        &s // s 在这里被销毁,返回的引用指向无效内存!
    }

解引用

  1. 显式解引用:使用 * 操作符访问引用指向的值

    rust 复制代码
    let x = 5;
    let r = &x;
    assert_eq!(*r, 5); // 显式解引用
  2. 隐式解引用:方法调用和部分场景(如 println!)自动解引用

    rust 复制代码
    let s = String::from("hello");
    println!("Length: {}", s.len()); // 自动解引用 &s 为 &str
  3. Deref 强制转换:允许智能指针(如 String)自动转换为底层引用(如 &str

    rust 复制代码
    fn print_str(s: &str) {
        println!("{}", s);
    }
    let s = String::from("hello");
    print_str(&s); // &String 自动转换为 &str

切片类型

在 Rust 中,切片(Slice) 是一种引用类型,用于表示对数组、向量、字符串这三种类型中的一部分数据的引用。它允许我们操作其中的一部分数据,而无需复制整个数据,从而高效且安全地处理动态长度的数据。

  1. 切片本身不拥有数据(没有所有权),只是对现有数据的借用(引用)。

  2. 切片是一个指向数据起始位置的胖指针(fat pointer),其中包含了指向数据的指针(指向原数组或 Vec 的某个元素)和切片的长度(元素的个数)。

字符串切片

字符串切片(String slice)允许我们访问字符串中的部分数据,而无需复制数据。使用 &str&mut str 分别表示不可变字符串切片类型和可变字符串切片类型。

  1. 字符串字面值默认的类型就是 &str 类型。

  2. 某些情况下,Rust 可以自动推到类型,因此定义时的 : &str: &mut str可以省略

  3. 字符串切片类型 &str&mut str 可以作为函数的形参或者返回值,实参则是字符串的引用 &字符串名&mut 字符串名。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!

定义

使用 let 切片名: &str = &字符串名[..语法]let 切片名: &str = &"字符串字面值"; 分别从字符串变量和字符串字面值创建不可变字符串切片;使用 let 切片名: &mut str = &mut 字符串名[..语法] 从字符串变量创建可变字符串切片(字符串字面值是不可变的,因此不能字符串字面值创建可变字符串切片)。

rust 复制代码
    /* ========================= 不可变字符串切片 ============================ */
    let s = String::from("Hello World!");
    let str_slice: &str = &s[0..5];     // 显示指定类型。"Hello"
    println!("str_slice: {str_slice}");
    let str_slice = &s[0..5];           // 自动推导类型。"Hello"
    println!("str_slice: {str_slice}");
    let literal_slice = &"hello";  // 从字面值直接创建
    println!("literal_slice: {literal_slice}");
    /* ========================= 可变字符串切片 ============================== */
    let mut s = String::from("Hello World!");
    let str_slice: &mut str = &mut s[0..5]; // 显示指定类型。"Hello"
    str_slice.make_ascii_uppercase();
    println!("str_slice: {str_slice}");
    let str_slice = &mut s[0..5];           // 自动推导类型。"Hello"
    str_slice.make_ascii_lowercase();
    println!("str_slice: {str_slice}");

由于字符串字面值默认的类型就是 &str 类型,因此,从字符串字面值创建的字符串切片可以是 let literal_slice: &str = "hello"; 这个各种格式,也可以是 let literal_slice = "hello"; 这种格式

UTF-8 安全约束

Rust 的字符串是基于 UTF-8 编码的,因此切片的索引是以字节为单位的,而不是字符。因此,切片的操作需要注意字符边界(切片范围必须落在 UTF-8 字符边界),否则会导致 panic。

rust 复制代码
    let s = "你好,世界!";
    let slice = &s[1..3];  // 运行出错,因为切片会从字符中间截断(一个汉字是2个字节)
    println!("slice: {slice}");

访问操作

无论字符串切片是否可变,都不能使用索引来直接访问元素。我们需要可以按字节索引访问或者转换为字符迭代器。此外,还可以使用 Rust 还为切片提供了一些常用的方法。

rust 复制代码
    let s = "hello";
    let first_byte = s.as_bytes()[0];   // 按字节索引访问(需确保是 UTF-8 边界)104('h' 的 ASCII 码)
    println!("first_byte: {}", first_byte);
    for c in s.chars() {    // 转换为字符迭代器
        println!("{}", c);  // h, e, l, l, o
    }
    let s = "hello, world";
    let (hello, world) = s.split_at(7);
    println!("s.split_at: left: {} right: {}", hello, world);

与字节切片的转换

字符串切片可安全转换为字节切片(&[u8]),但反向转换需确保 UTF-8 有效性

rust 复制代码
    let bytes: &[u8] = "hello".as_bytes();// &str → &[u8]
    let raw = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello" 的 ASCII 码
    let s = std::str::from_utf8(raw).unwrap(); // &[u8] → &str(需 UTF-8 校验) "Hello" 

数组切片

数组切片(Array Slice)是对固定大小数组([T; N])中连续元素的引用,它提供了对数组部分或全部元素的安全访问,而无需复制数据。分别使用 &[数组类型]&mut [数组类型] 来表示不可变数组切片类型和可变数组切片类型。

  1. 某些情况下,Rust 可以自动推到类型,因此定义时的 : &[数组类型]: &mut [数组类型] 可以省略

  2. 数组切片类型 &[数组类型]&mut [数组类型] 可以作为函数的形参或者返回值,实参则是数组的引用 &数组名&mut 数组名。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!

  3. 对字节数组(u8 类型的数组)的引用也叫 字节切片。字节切片是数组切片 的一种特殊形式

定义

使用 let 切片名: &[数组类型] = &数组名[..语法]let 切片名: &[数组类型] = &[元素 1, 元素 1, 元素 n]; 分别从数组变量和数组字面值创建不可变数组切片;使用 let 切片名: &mut [数组类型] = &mut 数组名[..语法]let 切片名: &mut [数组类型] = &mut [元素 1, 元素 1, 元素 n]; 分别从数组变量和数组字面值创建可变数组切片。

rust 复制代码
    /* =========================== 创建不可变数组切片 ========================= */
    let arr = [1, 2, 3, 4, 5];
    let slice: &[i32] = &arr[1..3]; // 显式指定类型。包含数组的第 1 个到第 3 个元素(不包括第 3 个元素)
    println!("slice: {:?}", slice); // 输出: [1, 2, 3]
    let slice = &arr[..3];          // 自动推导类型。包含数组的第 0 个到第 3 个元素(不包括第 3 个元素)
    println!("slice: {:?}", slice); // 输出: [1, 2, 3]
    let slice = &[1, 2, 3, 4, 5];   // 直接从数组字面值创建
    println!("slice: {:?}", slice); // 输出: [1, 2, 3, 4, 5]
    /* =========================== 创建可变数组切片 ========================= */
    let mut arr = [1, 2, 3, 4, 5];
    let slice: &mut [i32] = &mut arr[..3];  // 显式指定 &mut [i32] 类型,包含数组的第 0 个到第 3 个元素(不包括第 3 个元素)
    slice[0] = 9;                   // 修改数组的第 0 个元素
    println!("slice: {:?}", slice); // 输出: [0, 2, 3]
    let slice = &mut arr[..3];      // 自动推导类型,包含数组的第 0 个到第 3 个元素(不包括第 3 个元素)
    slice[0] = 8;                   // 修改数组的第 0 个元素
    println!("slice: {:?}", slice); // 输出: [0, 2, 3]
    let slice = &mut [1, 2, 3, 4, 5];   // 直接从数组字面值创建
    slice[0] = 8;
    println!("slice: {:?}", slice); // 输出: [1, 2, 3, 4, 5]

访问操作

无论数组切片是否可变,都是可以使用 切片名[从 0 开始的索引] 来访问其中的元素。此外,还可以使用 Rust 还为切片提供了一些常用的方法。

rust 复制代码
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[..3];          // 自动推导类型,包含数组的第 0 个到第 3 个元素(不包括第 3 个元素)
    println!("{:?}", slice);        // 输出: [1, 2, 3]
    println!("{:?}", slice.len());  // 输出: 3
    println!("{:?}", slice.is_empty()); // 输出: false
    println!("{:?}", slice.first()); // 输出: Some(1)
    println!("{:?}", slice.last());  // 输出: Some(3)
    println!("{:?}", slice.get(1));  // 输出: Some(2)
    println!("{:?}", slice.get(3));  // 输出: None

迭代遍历

rust 复制代码
    let arr = [10, 20, 30];
    let slice = &arr[..];
    for num in slice {          // 不可变迭代
        println!("{}", num);    // 10, 20, 30
    }
    let mut arr = [1, 2, 3];
    for num in &mut arr[..] {   // 可变迭代
        *num *= 2;              // 修改元素值
    }
    println!("arr: {:?}", arr);

空数组切片

在 Rust 中,空数组切片(Empty Slice)是一个长度为 0 的切片,它不包含任何实际数据元素,但依然遵循 Rust 的借用和生命周期规则。

rust 复制代码
    let b: &[i32] = &[];                // 直接从字面值创建空切片
    println!("{:?}", b);                // 输出: []
    let arr = [1, 2, 3];
    let empty_from_range = &arr[0..0];  // 从范围 0..0 生成空切片
    println!("empty_from_range: {:?}", empty_from_range);                // 输出: []
    let empty_arr_slice: &[i32] = &[][..]; // 显式范围语法
    println!("empty_arr_slice: {:?}", empty_arr_slice);                // 输出: []
    let mut vec_empty = Vec::<i32>::new();
    let empty_slice_mut: &mut [i32] = &mut vec_empty[..];
    println!("empty_slice_mut: {:?}", empty_slice_mut);                // 输出: []

与数组引用的互转

  1. 数组引用 ➜ 数组切片(隐式转换):数组引用可以自动转换为切片(零成本抽象)

    rust 复制代码
        let arr_ref: &[i32; 5] = &[1, 2, 3, 4, 5];
        let slice: &[i32] = arr_ref;    // 隐式转换为整个数组的切片
        let sub_slice = &arr_ref[1..4]; // 显式生成子切片 [2, 3, 4]
        println!("slice: {:?} sub_slice: {:?}", slice, sub_slice);
  2. 数组切片 ➜ 数组引用(需明确长度)

    rust 复制代码
        let slice = &[1, 2, 3, 4, 5][..];
        use std::convert::TryInto;// 方法 1: 使用 try_into(需导入 std::convert::TryInto)
        let arr_ref: Option<&[i32; 5]> = slice.try_into().ok();
        println!("arr_ref: {:?}", arr_ref);
        if let [a, b, c, d, e] = slice {    // 方法 2: 模式匹配
            // 在此作用域内,a~e 对应各元素
            println!("{a} {b} {c} {d} {e}");
        }

Vec<T> 切片

在 Rust 中,Vec<T> 的切片是一种引用类型,它允许我们以引用形式安全地访问 Vec 中连续的元素序列,而无需复制数据。使用 &[数组类型]&mut [数组类型] 分别表示不可变 Vec<T> 切片类型和可变 Vec<T> 切片类型,与普通数组切片类型一致!

定义

使用 let 切片名: &[数组类型] = &数组名[..语法]let 切片名: &[数组类型] = &vec![元素 1, 元素 1, 元素 n]; 分别从数组变量和 Vec<T> 字面值创建不可变 Vec<T> 切片;使用 let 切片名: &mut [数组类型] = &mut 数组名[..语法]let 切片名: &mut [数组类型] = &mut vec![元素 1, 元素 1, 元素 n]; 分别从数组变量和 Vec<T> 字面值创建可变 Vec<T> 切片。

rust 复制代码
    /* ============================== 不可变切片 ============================= */
    let vec = vec![1, 2, 3, 4, 5];
    let slice_vec: &[i32] = &vec[1..4];     // 显示指定类型。索引 1 到 3(左闭右开)
    println!("slice_vec: {:?}", slice_vec);
    println!("vec: {:?}", vec);
    let slice_vec = &vec[1..4];             // 自动推导类型。索引 1 到 3(左闭右开)
    println!("slice_vec: {:?}", slice_vec);
    println!("vec: {:?}", vec);
    let slice_vec = &vec![1, 2, 3];         // 直接从字面值创建
    println!("slice_vec: {:?}", slice_vec);
    /* =============================== 可变切片 ============================== */
    let mut vec = vec![1, 2, 3, 4, 5];
    let slice_vec: &mut [i32] = &mut vec[1..4];    // 显示指定类型。索引 1 到 3(左闭右开)
    slice_vec[0] = 6;
    println!("slice_vec: {:?}", slice_vec);
    println!("vec: {:?}", vec);
    let slice_vec = &mut vec[1..4];                // 自动推导类型。索引 1 到 3(左闭右开)
    slice_vec[0] = 7;
    println!("slice_vec: {:?}", slice_vec);
    println!("vec: {:?}", vec);
    let slice_vec: &mut [i32] = &mut vec![1, 2, 3];// 直接从字面值创建
    println!("slice_vec: {:?}", slice_vec);

访问操作

无论 Vec<T> 切片是否可变,都是可以使用 切片名[从 0 开始的索引] 来访问其中的元素。此外,还可以使用 Rust 还为切片提供了一些常用的方法。

rust 复制代码
    let slice = &vec![10, 20, 30];
    println!("slice[0] = {} slice[2] = {}", slice[0], slice[2]);
    println!("slice.len() = {}", slice.len());
    println!("slice.is_empty() = {}", slice.is_empty());
    let (left, right) = slice.split_at(1);
    println!("slice.split_at: slice_left: {:?} - slice_right: {:?}", left, right);
    if let Some((first, rest)) = slice.split_first() {
        println!("First: {}, Rest: {:?}", first, rest); // 10, [20, 30]
    }

迭代遍历

rust 复制代码
    let mut vec = vec![1, 2, 3, 4, 5];
    for num in &vec[1..3] {// 不可变迭代
        println!("{}", num); // 2, 3
    }
    for num in &mut vec[1..3] {// 可变迭代
        *num *= 2; // 修改元素
    }
    println!("vec: {:?}", vec);

与数组切片互转

  1. 普通数组切片 ➜ Vec<T> 切片

    rust 复制代码
        let slice = &[1, 2, 3];
        let new_vec = slice.to_vec();   // 普通数组切片转 Vec<T> 切片,克隆数据生成新 Vec
        println!("new_vec: {:?}", new_vec);
  2. Vec<T> 切片 ➜ 普通数组切片

    rust 复制代码
        let vec = vec![4, 5, 6];
        let slice: &[i32] = &vec;       // Vec<T> 切片转普通数组切片,零成本
        println!("slice: {:?}", slice);

Vec<T> 引用互转

  1. Vec<T> 引用 ➜ Vec<T> 切片

    rust 复制代码
    let vec = vec![1, 2, 3, 4];
    // 自动解引用转换(Deref Coercion)
    let slice: &[i32] = &vec;        // 隐式转换为整个 Vec 的切片
    let partial_slice = &vec[1..3];  // 显式获取子切片 [2, 3]
    // 等价于显式调用方法
    let slice = vec.as_slice();      // 整个切片
  2. Vec<T> 切片 ➜ Vec<T> 引用

    rust 复制代码
    let slice = &[1, 2, 3];
    let new_vec = slice.to_vec();    // 拷贝数据创建新 Vec
    let new_vec: Vec<_> = slice.into(); // 同样创建新 Vec

参考

  1. https://kaisery.github.io/trpl-zh-cn/
相关推荐
RustFS3 小时前
RustFS 如何实现对象存储的前端直传?
vue.js·docker·rust
沐森7 小时前
使用rust打开node的libuv实现多线程调用三种模式
javascript·rust
苏近之8 小时前
Rust 基于 Tokio 实现任务管理器
后端·架构·rust
Source.Liu9 小时前
【Rust】方法重载
rust
QC七哥9 小时前
基于tauri构建全平台应用
rust·electron·nodejs·tauri
wadesir18 小时前
Rust中的条件变量详解(使用Condvar的wait方法实现线程同步)
开发语言·算法·rust
hans汉斯21 小时前
嵌入式操作系统技术发展趋势
大数据·数据库·物联网·rust·云计算·嵌入式实时数据库·汉斯出版社
王老师青少年编程1 天前
csp信奥赛C++标准模板库STL案例应用5
c++·stl·set·集合·标准模板库·csp·信奥赛
Source.Liu1 天前
【Rust】布尔类型详解
rust
清醒的土土土1 天前
Tokio 源码学习01——Mutex
rust