概述
Rust 的基本语法对于从事底层 C/C++ 开发的人来说多少有些难以理解,虽然官方有详细的文档来介绍,不过内容是相当的多,看起来也费劲。本文通过将每个知识点简化为 一个 DEMO + 每种特性各用一句话描述的形式来简化学习过程,提高学习效率。
所有 DEMO 可以从 https://gitee.com/itexp 获取
所有权
所有权(ownership)是 Rust 用于如何管理内存的一组规则。它决定了数据的生命周期、如何管理内存,并且确保数据在使用过程中不发生数据竞争。所有权系统可以有效地避免内存泄漏和悬挂指针(dangling pointers)等问题,从而保证程序的安全性和高效性。
-
Rust 中的每一个值都有一个 所有者(owner)
- 每个值(数据)都有一个与之相关联的变量,称为所有者(owner)。
- 每当值被创建时,它有一个明确的所有者。
-
值在任一时刻有且只有一个所有者
- 变量的所有者是唯一的,这意味着当一个值的所有者发生转移时,原所有者将不再拥有对该值的访问权限。
-
当所有者(变量)离开作用域,这个值将被丢弃
- 当一个变量超出其作用域时,Rust 会自动释放该变量占用的内存。这个过程被称为“销毁”或“释放”(drop)。
-
编译器在编译时会根据一系列的规则进行检查,如果违反了任何这些规则,程序都不能编译。
转移(Move)
当将一个变量赋值给另一个变量、作为函数参数传递或作为返回值时,如果类型未实现 Copy trait,则 Rust 会执行转移操作。此时,原变量失去所有权,无法再被使用。
fn main() {let s1 = String::from("Hello"); // s1 是 String 类型的所有者let s2 = s1; // s1 的所有权被转移给 s2// println!("{}", s1); // 错误!s1 的所有权已转移,不能再使用 s1println!("{}", s2); // 正确!s2 是所有者
}
克隆(Clone)
如果需要在转移所有权时保留原始值,可以通过 克隆(clone)来创建一个值的副本,这样两个变量可以各自拥有一份数据。新的数据与原数据是没有任何关系两个独立值,他们的生命周期、作用域等都可以不同。
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_index
和 end_index
可以是任意整数类型(i32、u32、i64 等),并且可以省略其一或全部。如果要用负数,则前提是我们的范围本身支持使用负数索引!
-
如果将
start_index
和end_index
指定为浮点数,则编译会报错 -
当
start_index >= end_index
时,则表示为空范围 -
start_index..end_index
是std::ops::Range<Idx>
类型;start_index..=end_index
是std::ops::RangeInclusive<Idx>
类型
定义范围变量
..
可以作为范围操作符用来定义范围变量。
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 的数字,不含 5for 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);// }
}
定义切片
..
可以作为范围操作符可以用来定义切片。
let arr = [0, 1, 2, 3, 4];let slice = &arr[1..4]; // [1, 2, 3]println!("Slice: {:?}", slice);
模式匹配
..
可以用在模式匹配中以匹配某个范围。
let num = 42;
match num {1..=10 => println!("1到10"),11..50 => println!("11到49"), // 不包含50_ => println!("其他"),
}
忽略某些内容
_
忽略单个值,..
忽略剩余所有值。
解构操作
在解构结构体或者元组时,..
语法用于表示忽略某些字段。它是解构模式的一部分,可以让你在解构时不关心结构体的某些字段,只关心你需要的字段。
let point = (3, 5, 10);
// 只取第一个值,忽略后续所有值
let (x, ..) = point;
println!("x = {}", x); // x = 3struct Config {width: u32,height: u32,debug: bool,
}
fn init_config(Config { width, height, .. }: Config) {println!("初始化尺寸: {}x{}", width, height);
}
结构体更新语法
在结构体更新语法中,可以使用 ..结构体变量
的形式来用旧的结构体变量初始化新的结构体,而无需显示指明所有结构体成员挨个赋值。
#[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 }
..
必须放在结构体字面量的最后,且只能使用一次
模式匹配
在匹配匹配中,可以使用 ..
格式来忽略其他字段。
match point {(0, ..) => println!("x 为 0"),(.., 0) => println!("z 为 0"),_ => println!("其他情况"),
}
_
语法
在 Rust 中,_
通常用于表示忽略个元素或由编译器推断类型的占位符。
忽略某个元素
_
忽略单个值,..
忽略剩余所有值。
忽略未使用的变量
let _ = 42; // 明确忽略值,避免编译器警告
let s = String::from("hello");
let _ = s; // s 立即被 Drop
// let _x = s; // s 会在 _x 的作用域结束时 Drop
忽略函数参数和返回值
fn foo(_: i32) {println!("忽略参数");
}
foo(10); // 调用时传入的参数被忽略let _ = setup_test_env(); // 忽略初始化返回值
忽略模式匹配的部分值
match Some(42) {Some(_) => println!("有值,但忽略具体内容"),None => println!("无值"),_ => println!("其他"),
}
忽略结构体字段
struct Point { x: i32, y: i32 }
let p = Point { x: 10, y: 20 };
let Point { x: _, y } = p; // 只绑定 y,忽略 x
忽略元组值
let (x, _) = (1, 2); // 只绑定元组的第一个元素,忽略第二个
类型推断占位符
泛型类型推断
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>`
闭包参数类型推断
let square = |x: _| x * x; // 错误:不能直接用于闭参
let square = |x| x * x; // 正确:编译器自动推断为 `i32`(根据上下文)
生命周期占位符
作为生命周期占位符时,需要符合生命周期标准的格式 '_
。关于声明周期,我们将在 Rust 之六 语句和表达式、作用域、生命周期、变量与常量、控制流 中详细学习!
struct Parser<'a> {data: &'a str,
}impl Parser<'_> { // 等价于 impl<'a> Parser<'a>fn parse(&self) -> &str {self.data}
}
数字字面量分隔符
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(向量)类型,它允许我们用一个单独的数据结构中在堆内存中彼此相邻地排列的储存多于一个相同类型的值。它是一个动态数组,可以根据需要自动增长和缩小。
- Vec 会根据需要自动调整其容量。当 Vec 的容量不够时,它会自动重新分配更多内存。
结构
Vec<T>
内部由一个指向堆内存中数据的起始地址(*mut T
)的指针、 指示当前存储的元素数量的长度(len)以及表示分配的内存可容纳的元素总数(len ≤ capacity)的容量(capacity) 这三个部分组成。
pub struct Vec<T> {ptr: NonNull<T>, // 堆内存指针len: usize, // 当前元素数量cap: usize, // 分配的总容量
}
定义
-
Rust 为 Vector 提供了很多预定义的接口,可以调用
Vec::new()
函数来创建一个新的空 Vector。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);
-
最常用的方法是以
let 变量名 = vec![元素 1, 元素 2, 元素 n];
这样的格式用初始值来创建一个Vec<T>
而让 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>
-
还可以用 Rust 为 Vector 提供预接口
with_capacity()
来预分配容量,然后后续补充值let mut v = Vec::with_capacity(10); // 初始容量为 10,避免频繁扩容 v.push(1); v.push(2); println!("{:?}", v);
-
Vec 在 Rust 中是拥有所有权的,这意味着当 Vec 被移动到另一个变量时,原来的变量就不再可用。
let v1 = vec![1, 2, 3]; let v2 = v1; // v1 的所有权转移给 v2 // println!("{:?}", v1); // 错误,v1 不再有效 println!("{:?}", v2); // 输出 [1, 2, 3]
访问元素
无论 Vector 是否可变,都是可以使用 变量名[从 0 开始的索引]
来访问其中的元素。此外,还可以使用 Rust 还为 Vector 提供的一些常用的方法来访问。
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)); // 获取第四个元素,返回 Noneprintln!("v.pop(): {:?}", v.pop()); // 弹出最后一个元素,返回 Option<i32>println!("v: {:?}", v); // 弹出后向量的内容println!("v.remove(0): {:?}", v.remove(0)); // 删除第一个元素,返回 i32println!("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()); // 获取向量长度
迭代遍历
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)的集合类型,。可以非常高效地进行查找、插入和删除操作。
- K 为键类型,V 为值类型
- 键 (K) 必须实现 Eq 和 Hash trait(如 String、i32 等),值 (V) 无特殊要求。
- HashMap 是基于哈希表实现的,所以查找操作的时间复杂度通常是 O(1),但在最坏情况下会退化为 O(n),具体取决于哈希冲突的情况。
定义
-
Rust 为 HashMap 提供了很多预定义的接口,可以调用
HashMap::new()
函数来创建一个新的空 HashMaplet mut map = HashMap::new(); // 空 HashMap map.insert("key1".to_string(), 42); // 插入键值对 println!("{:?}", map); // {"key1": 42}
-
使用
HashMap::from()
或迭代器构建。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}
-
还可以用 Rust 为 HashMap 提供接口
HashMap::with_capacity()
来预分配容量,然后后续补充值let mut map = HashMap::with_capacity(100);
-
HashMap 在 Rust 中是拥有所有权的,这意味着当 HashMap 被移动到另一个变量时,原来的变量就不再可用。
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 提供的一些常用的方法来访问。
let mut map = HashMap::new(); // 空 HashMapmap.insert("key1".to_string(), 42); // 插入键值对map.insert("key2".to_string(), 43); // 插入键值对map.insert("key3".to_string(), 44); // 插入键值对let value = map["key2"]; // 如果键存在,否则会 panicprintln!("Value: {}", value); // 输出 Value: 43println!("{:?}", map.get("key1")); // Some(42)println!("{:?}", map.get("key4")); // Noneprintln!("{:?}", map.get("key1").unwrap()); // 42let mut map = HashMap::new(); // 空 HashMapmap.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()); // 1println!("{:?}", map.is_empty()); // falsemap.remove("key1"); // 删除键为 key1 的元素println!("{:?}", map.is_empty()); // true
迭代遍历
let mut map = HashMap::new(); // 空 HashMapmap.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 之四 运算符、标量、元组、数组、字符串、结构体、枚举 中的介绍。
-
由于标量类型实现了 Copy trait,因此,不会进行所有权的转移。
-
数组的引用比较特殊,其格式为
&[T; N]
-
一个数据在同一时刻只能有一个可变引用,或者多个不可变引用,但不能同时拥有可变和不可变引用
不可变引用
不可变引用(Immutable Reference)允许你在不修改原始数据的情况下访问它。Rust 中使用 let 引用名: &数据类型 = &变量或值;
来创建不可变引用。允许同时创建同一值的多个不可变引用,因为它们不会改变数据。
/* ======================= 不可变引用作为函数的参数 =========================== */
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!"); // 创建一个 Stringlet 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}");
}
- 不可变引用可以作为函数的参数。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!
可变引用
可变引用(Mutable Reference)允许修改所引用的原始数据的值。Rust 中使用 let 引用名: &mut 数据类型 = &mut 变量或值;
来创建可变引用。在同一作用域内,只能有一个可变引用可以存在,也不能同时拥有可变引用和不可变引用,也不能同时拥有可变引用和对该变量的直接使用。
// 可变引用作为函数的参数
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; // 指定类型的不可变引用 y1let 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; // 指定类型的不可变引用 y1let 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}");
}
-
可变引用对应的原数据也必须是可变的。
-
可变引用可以作为函数的参数。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!
-
注意
let r3 = &mut s;
中r3
是一个不可变的可变引用,也就意味着,我们不能更改 r3,例如修改r3 = &mut s1
是不允许的 -
可变性必须从最外层开始显式声明
悬垂引用
悬垂引用(Dangling Reference)是指一个引用指向一个已经被销毁(例如,离开作用域)的内存位置。Rust 编译器会通过严格的所有权和生命周期机制,从根本上杜绝了悬垂引用的出现。悬垂引用主要有以下两种情况,Rust 编译器会直接报错,不允许编译。
- 引用一个已经被销毁的对象:例如,当一个变量在某个作用域结束后被销毁,但仍然有引用指向它时,就会出现悬垂引用。
let r; {let mut x = 5;r = &mut x; // r 的生命周期绑定到 x } // x 在此处被释放,r 成为悬垂引用 // println!("{}", r); // 错误:使用悬垂引用
- 在栈上引用局部变量并返回该引用:例如,返回指向局部变量的引用,局部变量在函数结束时会被销毁。
fn dangle() -> &String { // 错误:缺失生命周期参数let s = String::from("hello");&s // s 在这里被销毁,返回的引用指向无效内存! }
解引用
- 显式解引用:使用
*
操作符访问引用指向的值let x = 5; let r = &x; assert_eq!(*r, 5); // 显式解引用
- 隐式解引用:方法调用和部分场景(如
println!
)自动解引用let s = String::from("hello"); println!("Length: {}", s.len()); // 自动解引用 &s 为 &str
- Deref 强制转换:允许智能指针(如
String
)自动转换为底层引用(如&str
)fn print_str(s: &str) {println!("{}", s); } let s = String::from("hello"); print_str(&s); // &String 自动转换为 &str
切片类型
在 Rust 中,切片(Slice) 是一种引用类型,用于表示对数组、向量、字符串这三种类型中的一部分数据的引用。它允许我们操作其中的一部分数据,而无需复制整个数据,从而高效且安全地处理动态长度的数据。
-
切片本身不拥有数据(没有所有权),只是对现有数据的借用(引用)。
-
切片是一个指向数据起始位置的胖指针(fat pointer),其中包含了指向数据的指针(指向原数组或 Vec 的某个元素)和切片的长度(元素的个数)。
字符串切片
字符串切片(String slice)允许我们访问字符串中的部分数据,而无需复制数据。使用 &str
和 &mut str
分别表示不可变字符串切片类型和可变字符串切片类型。
-
字符串字面值默认的类型就是
&str
类型。 -
某些情况下,Rust 可以自动推到类型,因此定义时的
: &str
和: &mut str
可以省略 -
字符串切片类型
&str
和&mut str
可以作为函数的形参或者返回值,实参则是字符串的引用&字符串名
或&mut 字符串名
。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习!
定义
使用 let 切片名: &str = &字符串名[..语法]
和 let 切片名: &str = &"字符串字面值";
分别从字符串变量和字符串字面值创建不可变字符串切片;使用 let 切片名: &mut str = &mut 字符串名[..语法]
从字符串变量创建可变字符串切片(字符串字面值是不可变的,因此不能字符串字面值创建可变字符串切片)。
/* ========================= 不可变字符串切片 ============================ */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。
let s = "你好,世界!";let slice = &s[1..3]; // 运行出错,因为切片会从字符中间截断(一个汉字是2个字节)println!("slice: {slice}");
访问操作
无论字符串切片是否可变,都不能使用索引来直接访问元素。我们需要可以按字节索引访问或者转换为字符迭代器。此外,还可以使用 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 有效性
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 [数组类型]
来表示不可变数组切片类型和可变数组切片类型。
-
某些情况下,Rust 可以自动推到类型,因此定义时的
: &[数组类型]
或: &mut [数组类型]
可以省略 -
数组切片类型
&[数组类型]
和&mut [数组类型]
可以作为函数的形参或者返回值,实参则是数组的引用&数组名
或&mut 数组名
。我们将在 Rust 之六 语句和表达式、作用域、生命周期、函数 详细学习! -
对字节数组(
u8
类型的数组)的引用也叫 字节切片。字节切片是数组切片 的一种特殊形式
定义
使用 let 切片名: &[数组类型] = &数组名[..语法]
和 let 切片名: &[数组类型] = &[元素 1, 元素 1, 元素 n];
分别从数组变量和数组字面值创建不可变数组切片;使用 let 切片名: &mut [数组类型] = &mut 数组名[..语法]
和 let 切片名: &mut [数组类型] = &mut [元素 1, 元素 1, 元素 n];
分别从数组变量和数组字面值创建可变数组切片。
/* =========================== 创建不可变数组切片 ========================= */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 还为切片提供了一些常用的方法。
let arr = [1, 2, 3, 4, 5];let slice = &arr[..3]; // 自动推导类型,包含数组的第 0 个到第 3 个元素(不包括第 3 个元素)println!("{:?}", slice); // 输出: [1, 2, 3]println!("{:?}", slice.len()); // 输出: 3println!("{:?}", slice.is_empty()); // 输出: falseprintln!("{:?}", slice.first()); // 输出: Some(1)println!("{:?}", slice.last()); // 输出: Some(3)println!("{:?}", slice.get(1)); // 输出: Some(2)println!("{:?}", slice.get(3)); // 输出: None
迭代遍历
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 的借用和生命周期规则。
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); // 输出: []
与数组引用的互转
- 数组引用 ➜ 数组切片(隐式转换):数组引用可以自动转换为切片(零成本抽象)
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);
- 数组切片 ➜ 数组引用(需明确长度)
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>
切片。
/* ============================== 不可变切片 ============================= */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 还为切片提供了一些常用的方法。
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]}
迭代遍历
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);
与数组切片互转
- 普通数组切片 ➜
Vec<T>
切片let slice = &[1, 2, 3];let new_vec = slice.to_vec(); // 普通数组切片转 Vec<T> 切片,克隆数据生成新 Vecprintln!("new_vec: {:?}", new_vec);
Vec<T>
切片 ➜ 普通数组切片let vec = vec![4, 5, 6];let slice: &[i32] = &vec; // Vec<T> 切片转普通数组切片,零成本println!("slice: {:?}", slice);
与 Vec<T>
引用互转
Vec<T>
引用 ➜Vec<T>
切片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(); // 整个切片
Vec<T>
切片 ➜Vec<T>
引用let slice = &[1, 2, 3]; let new_vec = slice.to_vec(); // 拷贝数据创建新 Vec let new_vec: Vec<_> = slice.into(); // 同样创建新 Vec
参考
- https://kaisery.github.io/trpl-zh-cn/