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

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()
}

列表4-7:返回 String 参数中字节索引值的 first_word 函数
因为需要逐个检查 String 的每个元素是否为空格,所以使用 as_bytes 方法将 String 转换为字节数组:

let bytes = s.as_bytes();

接着,用 iter 方法创建对字节数组的迭代器:

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

我们将在第13章详细讨论迭代器。目前只需知道 iter 是一种方法,它依次返回集合中的每个元素;而 enumerate 会包装 iter 的结果,将每个元素和其索引一起作为元组返回。元组中的第一个元素是索引,第二个是对该元素的引用。这比自己计算索引更方便。

由于 enumerate 返回的是元组,可以用模式匹配解构它。在 for 循环里,我们指定了模式,其中 i 是元组里的索引,&item 是元组里的字节本身。因为 .iter().enumerate() 返回的是对元素的引用,所以模式里用了 & 符号。

循环体内,通过字节字面量语法查找代表空格(b’ ')的字节,如果找到就返回当前位置,否则最后返回字符串长度:

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

现在我们能得到首个单词结尾的位置,但存在问题——仅仅返还 usize 索引是不够安全且易出错,因为这个数字只有结合原始 String 才有意义。换句话说,由于这是与 String 分离开的独立值,没有保证未来仍然有效。看下面列表4-8演示调用前述 first_word 函数后的情况。

文件名:src/main.rs

fn main() {let mut s = String::from("hello world");let word = first_word(&s); // word 得到值 5s.clear(); // 清空了 String,使其等于 ""// 此时 `word` 仍然是 `5` ,但 `s` 已无任何内容,// 因此不能再用 `word` 来合理地访问任何东西,// 所以 `word` 完全失效了!
}

列表4-8:保存调用 first_word 后结果,再修改 String 内容
程序可正常编译,也不会因之后使用 word 而报错。但由于 word 与 s 状态毫无关联,它依旧保持着值 5。如果尝试用这个值从变量 s 中提取首词,会导致错误,因为自从存储 5 到 word 后,s 的内容已被改变。

必须担心 word 中记录的位置与实际数据不同步既繁琐又容易出错!如果要实现 second_word 函数管理两个位置,就更加脆弱,其签名可能如下:

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

这意味着同时跟踪起始和结束两个索引,有更多基于特定状态计算出的、却未绑定该状态的数据变量,需要同步维护,非常麻烦。

幸运的是,Rust 提供了解决方案:字符串切片(string slices)。

字符串切片
字符串切片是对 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 的切片。

图 4-7 展示了这一点的示意图。
4-7
图 4-7:指向字符串部分的字符串切片

使用 Rust 的 … 范围语法,如果你想从索引 0 开始,可以省略两个点之前的值。换句话说,这两者是等价的:

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

同理,如果你的切片包含了字符串的最后一个字节,你可以省略末尾的数字。这意味着这两者是等价的:

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;关于 UTF-8 处理更详细内容,请参见第 8 章“用 Strings 存储 UTF-8 编码文本”一节。

了解以上信息后,我们重写 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[..]
}

我们通过查找第一个空格的位置获得单词结束索引,与清单 4-7 中的方法相同。当找到空格时,返回从起始位置到该空格索引之间的字符串切片。

现在调用 first_word 时,会得到与底层数据绑定在一起的单个值。该值由对起始点引用和长度组成。

返回一个切片同样适用于 second_word 函数:

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

这样我们就有了更简单且不易出错的 API,因为编译器会确保对 String 的引用保持有效。还记得清单 4-8 中那个 bug 吗?当我们拿到第一个单词结尾索引后,却清空了原串导致索引失效?那段代码逻辑错误但没有立即报错。如果继续用这个无效索引操作,就会出现问题。而使用切片则完全避免了此类错误,并能让我们更早发现代码中的问题。使用基于切片版本的 first_word 会产生编译时错误:

文件名:src/main.rs

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

编译器报错如下:

$ cargo runCompiling 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);|                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!|     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");|                                  ------ immutable borrow later used hereFor more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

根据借用规则,如果存在不可变引用,则不能再取得可变引用。clear 方法需要截断 String,因此需要可变引用。而 clear 调用之后 println! 使用了 word 中的不变引用,所以该不可变借用仍然活跃着。Rust 不允许同时存在可变和不可变借用,导致编译失败。不仅如此,Rust 除了简化我们的 API,还在编译阶段消除了整类潜在错误!


字符串字面量作为切片

回想一下,我们提到过字符串字面量存储在二进制文件中。有了对切片概念理解后,可以正确认识它们:

let s = "Hello, world!";

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


字符串切片作为参数

知道可以对字面量和 String 值取出子串,让我们改进一下first_word函数签名:

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

经验丰富 Rustacean 会采用下面这种签名(见清单4-9),因为它既支持传入 &String 类型,也支持传入直接为 &str 类型的数据:

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

清单4-9:通过将参数类型改为 string 切slice 改善first_word函数
如果已有 string 切slice ,可以直接传入;如果有完整或部分 String ,也能传入对应子串或整体。这种灵活性利用了解引用自动转换(deref coercions)特性,该特性将在第15章“函数与方法中的隐式解引用自动转换”一节讲解。

定义接受 string 切slice 而非对完整 String 引用,使得API更加通用且实用,同时功能不受影响:

文件名:src/main.rs

fn main() {  let my_string=String::from("hello world");  // 对部分或全部String进行first_word调用均有效  let word=first_word(&my_string[0..6]);  let word=first_word(&my_string[..]);  // 对整个String本身(即相当于全体子串)调用亦有效   let word=first_word(&my_string);  let my_string_literal="hello world";  // 对部分或全部string literal进行调用均生效    let word=first_word(&my_string_literal[0..6]);  let word=first_word(&my_string_literal[..]);  // 因为string literal本质上就是string slices,所以无需额外加[]即可直接调用!    let word=first_word(my_string_literal);  
} 

其他类型数组及其 Slice

顾名思义,string slices 专门针对 strings。但还有一种更通用形式叫做普通 slices 。比如考虑以下数组:

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

正如可能希望访问某个子串一样,也可能想访问数组的一部分,如下所示:

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

这种普通数组分割出的slice类型为 &[i32] 。工作机制类似于 string slices,通过保存首元素地址及长度实现。在实际开发中,各种集合都会大量应用此类Slice,第八章讲述向量(Vector)时会详细讨论相关内容。


总结

所有权、借用以及 Slice 概念共同保证 Rust 程序内存安全并发生于编译期控制之下。Rust 提供系统级语言般精细内存管理能力,但拥有者离开作用域时自动释放资源,无需手动写复杂销毁代码,从而减少调试负担。

所有权机制贯穿 Rust 多处设计理念,全书后续章节将持续深入探讨。本章至此结束,我们接下来进入第五章学习如何通过结构体(struct)组合数据块吧!

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

相关文章:

  • centos7中把nginx更新到1.26 版(centos7默认只能更新到1.20)
  • IROS-2025 | OIKG:基于观察-图交互与关键细节引导的视觉语言导航
  • 【LeetCode 热题 100】39. 组合总和——(解法一)选或不选
  • windwos11网页切换残留/卡屏/冻结/残影问题
  • Java学习---Spring及其衍生(下)
  • 基于SpringBoot+Vue的电脑维修管理系统(WebSocket实时聊天、Echarts图形化分析)
  • 类和包的可见性
  • 磁性材料如何破解服务器电源高频损耗难题?
  • Linux C 网络基础编程
  • Redis高可用架构演进面试笔记
  • 13-C语言:第13天笔记
  • mysql索引底层B+树
  • HTTP/1.0、HTTP/1.1 和 HTTP/2.0 主要区别
  • OpenLayers 综合案例-基础图层控制
  • 主要分布在背侧海马体(dHPC)CA1区域(dCA1)的位置细胞对NLP中的深层语义分析的积极影响和启示
  • 《Java语言程序设计》第2章复习题(3)
  • 高亮标题里的某个关键字正则表达式
  • JMeter 性能测试实战笔记
  • 云端哨兵的智慧觉醒:Deepoc具身智能如何重塑工业无人机的“火眼金睛”
  • 无人机正摄影像自动识别与矢量提取系统
  • 无人机保养指南
  • 无人机速度模块技术要点分析
  • 04.建造者模式的终极手册:从快餐定制到航天飞船的组装哲学
  • (LeetCode 面试经典 150 题) 56. 合并区间 (排序)
  • Flutter 主流 UI 框架总结归纳
  • 让UV管理一切!!!
  • Django实时通信实战:WebSocket与ASGI全解析(上)
  • 使用钉钉开源api发送钉钉工作消息
  • kafka的shell操作
  • kafka消费者组消费进度(Lag)深入理解