当前位置: 首页 > news >正文

Rust 切片类型(slice type)

文章目录

  • 切片类型
    • 字符串切片
    • 字符串字面量也是切片
    • 用切片作为参数
    • 小结

切片类型

Rust

切片允许你引用集合中一段连续的元素,而不是整个集合。切片是一种引用,因此它不拥有所有权。

这里有一个小编程问题:编写一个函数,接收一个由空格分隔的单词字符串,并返回该字符串中的第一个单词。如果字符串中没有空格,则整个字符串就是一个单词,应返回整个字符串。

让我们先不使用切片,来写这个函数的签名,以理解切片要解决的问题:

fn first_word(s: &String) -> ?

first_word 函数的参数是 &String。我们不需要所有权,所以这样没问题。(在惯用的 Rust 中,函数不会获取参数的所有权,除非确实需要,随着学习你会明白原因!)但我们应该返回什么呢?我们实际上没有办法只描述字符串的一部分。不过,我们可以返回单词结尾的索引,也就是空格的位置。让我们试试,如清单 4-7 所示。

文件名:src/main.rs

fn first_word(s: &String) -> usize {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return i;}}s.len()
}

以上函数返回 String 参数字节索引值的 first_word 函数
因为我们需要逐个检查 String 的元素是否为空格,所以我们用 as_bytes 方法将 String 转换为字节数组。

    let bytes = s.as_bytes();

接下来,我们用 iter 方法创建字节数组的迭代器:

    for (i, &item) in bytes.iter().enumerate() {

iter 方法会返回集合中的每个元素,而 enumerate 会包装 iter 的结果,并将每个元素作为元组的一部分返回。元组的第一个元素是索引,第二个元素是元素的引用。这样比我们自己计算索引更方便。

因为 enumerate 返回的是元组,我们可以用模式解构这个元组。在 for 循环中,我们指定了 i 作为元组中的索引,&item 作为单个字节。由于 .iter().enumerate() 返回的是元素的引用,所以我们在模式中使用 &。

for 循环内部,我们用字节字面量语法查找表示空格的字节。如果找到空格,就返回该位置。否则,返回字符串的长度 s.len()

        if item == b' ' {return i;}}s.len()

现在我们可以找到字符串中第一个单词结尾的索引了,但这里有个问题。我们返回的是 usize,但它只有在 &String 的上下文中才有意义。换句话说,因为它是与 String 分离的值,无法保证它在将来仍然有效。

文件名:src/main.rs

fn main() {let mut s = String::from("hello world");let word = first_word(&s); // word 得到值 5s.clear(); // 清空 String,变为 ""// 此时 word 仍然是 5,但 s 已经没有任何内容// 用 5 去索引 s 已经毫无意义,word 现在是无效的!
}

上述代码调用 first_word 后再修改 String 内容
这个程序可以编译通过,即使在调用 s.clear() 后再用 word 也不会报错。因为 words 的状态完全无关,word 仍然是 5。我们可以用 5 去索引 s 提取第一个单词,但这会出 bug,因为 s 的内容已经变了。

需要担心 word 的索引和 s 的数据不同步很麻烦且容易出错!如果我们再写一个 second_word 函数,管理这些索引会更脆弱。它的签名可能是:

fn second_word(s: &String) -> (usize, usize) {

现在我们要跟踪起始和结束索引,有更多的值需要和数据保持同步。我们有三个互不关联的变量需要同步。

幸运的是,Rust 有解决方案——字符串切片。

字符串切片

字符串切片是对 String 部分内容的引用,形式如下:

    let s = String::from("hello world");let hello = &s[0..5];let world = &s[6..11];

hello 不是对整个 String 的引用,而是对其中一部分的引用,通过 [0…5] 指定。我们用中括号加范围 [起始索引…结束索引] 创建切片,起始索引是切片的第一个位置,结束索引是最后一个位置加一。内部,切片结构存储起始位置和长度(结束索引减起始索引)。比如 let world = &s[6..11];world 是指向 s 第 6 个字节、长度为 5 的切片。

三张表:一张表示 s 的栈数据,指向堆上 “hello world” 字节数据表的第 0 个字节。第三张表表示 world 切片的栈数据,长度为 5,指向堆数据表的第 6 个字节。

字符串切片引用 String 的一部分
用 Rust 的 … 范围语法,如果从索引 0 开始,可以省略前面的值。也就是说,这两种写法等价:

let s = String::from("hello");let slice = &s[0..2];
let slice = &s[..2];

同理,如果切片包含 String 的最后一个字节,可以省略后面的数字。这两种写法等价:

let s = String::from("hello");let len = s.len();let slice = &s[3..len];
let slice = &s[3..];

也可以都省略,获取整个字符串的切片。如下等价:

let s = String::from("hello");let len = s.len();let slice = &s[0..len];
let slice = &s[..];

注意:字符串切片的范围索引必须在有效的 UTF-8 字符边界上。如果在多字节字符中间创建切片,程序会报错退出。为介绍切片,这里假设只用 ASCII。

了解这些后,让我们重写 first_word,让它返回切片。字符串切片类型写作 &str:

文件名:src/main.rs

fn first_word(s: &String) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[0..i];}}&s[..]
}

当找到空格时,返回从字符串开头到空格索引的切片。

现在调用 first_word 时,返回的是和底层数据关联的单一值。它由切片起始点的引用和切片长度组成。

second_word 也可以返回切片:

fn second_word(s: &String) -> &str {

现在 API 更简单,且更难出错,因为编译器会确保 String 的引用有效。如果我们拿到第一个单词的索引后清空字符串,索引就无效了。那段代码逻辑上有错,但不会立刻报错。如果继续用 first_word 索引空字符串,问题才会暴露。切片让这种 bug 不可能发生,能更早发现问题。用切片版 first_word 会在编译时报错:

文件名:src/main.rs
这段代码无法编译!

fn main() {let mut s = String::from("hello world");let word = first_word(&s);s.clear(); // 报错!println!("the first word is: {word}");
}

编译器报错如下:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow s as mutable because it is also borrowed as immutable
–> src/main.rs:18:5
|
16 | let word = first_word(&s);
| – 此处发生不可变借用
17 |
18 | s.clear(); // 报错!
| ^^^^^^^^^ 此处发生可变借用
19 |
20 | println!(“the first word is: {word}”);
| ------ 不可变借用稍后在此处使用

查看更多信息请运行 rustc --explain E0502
error: could not compile ownership (bin “ownership”) due to 1 previous error
回忆借用规则:有不可变引用时,不能再有可变引用。clear 需要截断 String,因此需要可变引用。clear 之后的 println! 用到了 word 的引用,所以不可变引用仍然有效。Rust 不允许 clear 的可变引用和 word 的不可变引用同时存在,编译失败。Rust 不仅让 API 更易用,还在编译期消除了整类错误!

字符串字面量也是切片

还记得我们说过字符串字面量存储在二进制文件中。现在了解切片后,可以更好地理解字符串字面量:

let s = "Hello, world!";

这里 s 的类型是 &str:它是指向二进制中特定位置的切片。这也是为什么字符串字面量是不可变的;&str 是不可变引用。

用切片作为参数

既然可以对字面量和 String 取切片,我们还可以改进 first_word 的签名:

fn first_word(s: &String) -> &str {

更有经验的 Rustacean 会写成下面这样,因为这样既能处理 &String,也能处理 &str

fn first_word(s: &str) -> &str {

如果有字符串切片,可以直接传递。如果有 String,可以传 String 的切片或引用。这种灵活性得益于 deref 强制转换。

让函数接收字符串切片而不是 &String,让 API 更通用且不损失功能:

文件名:src/main.rs

fn main() {let my_string = String::from("hello world");// `first_word` 可用于 String 的部分或全部切片let word = first_word(&my_string[0..6]);let word = first_word(&my_string[..]);// `first_word` 也可用于 String 的引用,相当于整个切片let word = first_word(&my_string);let my_string_literal = "hello world";// `first_word` 可用于字符串字面量的部分或全部切片let word = first_word(&my_string_literal[0..6]);let word = first_word(&my_string_literal[..]);// 因为字符串字面量本身就是切片,// 直接传递也可以!let word = first_word(my_string_literal);
}

其他切片
字符串切片只用于字符串。但还有更通用的切片类型。比如这个数组:

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

我们也可能想引用数组的一部分,可以这样做:

let a = [1, 2, 3, 4, 5];let slice = &a[1..3];assert_eq!(slice, &[2, 3]);

这个切片的类型是 &[i32]。它和字符串切片一样,存储对第一个元素的引用和长度。你会在各种集合中用到这种切片。

小结

所有权、借用和切片确保了 Rust 程序在编译期的内存安全。Rust 让你像其他系统编程语言一样控制内存,但数据所有者自动在作用域结束时清理数据,无需你手动写和调试额外的代码。

http://www.lryc.cn/news/574055.html

相关文章:

  • 关于华为Pura70Pro+升级鸿蒙NEXT和回退
  • 第三章---需求分析
  • JavaScript 中 async/await 的工作原理
  • Chromium 136 编译指南 macOS篇:编译优化技巧(六)
  • 【C++】C++中的虚函数和多态的定义与使用
  • 微软ASR与开源模型分析
  • 黑马python(十五)
  • C语言数组介绍 -- 一维数组和二维数组的创建、初始化、下标、遍历、存储,C99 变长数组
  • 三、kubectl使用详解
  • 安卓9.0系统修改定制化____如何编辑和修改安卓手机默认按键配置文件 改变按键功能 操作篇 九
  • LeetCode中K个链表的链接的解法
  • 区块链大讲堂 | 分布式隐私计算友好的零知识证明协议
  • 矩阵阶数(线性代数) vs. 张量维度(深度学习):线性代数与深度学习的基石辨析,再也不会被矩阵阶数给混淆了
  • Flink SQL执行流程深度剖析:从SQL语句到分布式执行
  • 机器学习基础:从概念到应用的全面解析
  • mac隐藏文件现身快捷键
  • Node.js 中的 JWT 认证:从生成到验证的完整指南
  • 深入浅出Node.js中间件机制
  • Apache SeaTunnel Spark引擎执行流程源码分析
  • 17、Rocket MQ快速实战以及核⼼概念详解
  • 更新麒麟连不上外网
  • 从理论到实践:Air8101外挂Air780EPM模块,实现4G联网能力!
  • 游戏盾:守护虚拟世界的坚固堡垒
  • 「Linux用户账号管理」组群管理
  • ActixWeb框架实战案例精萃
  • DAY 40 训练和测试的规范写法
  • 详解HarmonyOS NEXT仓颉开发语言中的全局弹窗
  • LED-Merging: 无需训练的模型合并框架,兼顾LLM安全和性能!!
  • Spring AI 项目实战(十二):Spring Boot +AI + DeepSeek + 百度OCR 公司发票智能处理系统的技术实践(附完整源码)
  • Maven 多模块项目调试与问题排查总结