010、切片

        除了引用,Rust还有另外一种不持有所有权的数据类型:切片(slice)。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合

        考虑这样一个小问题:编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回。

        如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可。让我们来看一下这个函数的签名应该如何设计:

fn first_word(s: &String) -> ?

        由于我们不需要获得传入值的所有权,所以这个函数 first_word 采用了 &String 作为参数。但它应该返回些什么呢?

        我们还没有一个获取部分字符串的方法。当然,你可以将首个单词结尾处的索引返回给调用者,如下代码所示: 

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

        这段代码首先使用 as_bytes 方法❶将 String 转换为字节数组,因为我们的算法需要依次检查 String 中的字节是否为空格。

        接着,我们通过 iter 方法❸创建了一个可以遍历字节数组的迭代器。我们会在后面文章中详细讨论这里新出现的迭代器。目前,你只需要知道 iter 方法会依次返回集合中的每一个元素即可。

        随后的 enumerate 则将 iter 的每个输出作为元素逐一封装在对应的元组中返回。元组的第一个元素是索引,第二个元素是指向集合中字节的引用。

        使用 enumerate 可以较为方便地获得迭代索引。既然 enumerate 方法返回的是一个元组,那么我们就可以使用模式匹配来解构它,就像Rust中其他使用元组的地方一样。

        在 for 循环的遍历语句中,我们指定了一个解构模式,其中 i 是元组中的索引部分,而 &item ❷则是元组中指向集合元素的引用。由于我们从 .iter().enumerate() 中获取的是产生引用元素的迭代器,所以我们在模式中使用了 &。 

        现在,我们初步实现了期望的功能,它能够成功地搜索并返回字符串中第一个单词结尾处的位置索引。但这里依然存在一个设计上的缺陷。

        我们将一个 usize 值作为索引独立地返回给调用者,但这个值在脱离了传入的 &String 的上下文之后便毫无意义。换句话说,由于这个值独立于String而存在,所以在函数返回值后,我们就再也无法保证它的有效性了。

        下面的代码示例中使用 first_word 函数演示了这种返回值失效的情形:

fn main() { let mut s = String::from("hello world"); let word = first_word(&s);  // 索引5会被绑定到变量word上 s.clear();    // 这里的clear方法会清空当前字符串,使之变为"" // 虽然word依然拥有5这个值,但因为我们用于搜索的字符串发生了改变, //所以这个索引也就没有任何意义了,word到这里便失去了有效性 
} 

        上面的程序在编译器看来没有任何问题,即便我们在调用 s.clear() 之后使用 word 变量也是没有问题的。同时由于 word 变量本身与 s 没有任何关联,所以 word 的值始终都是 5

        但当我们再次使用 5 去从变量 s 中提取单词时,一个 bug 就出现了:此时 s 中的内容早已在我们将 5 存入 word 后发生了改变。

        这种 API 的设计方式使我们需要随时关注 word 的有效性,确保它与 s 中的数据是一致的,类似的工作往往相当烦琐且易于出错。这种情况对于另一个函数 second_word 而言更加明显。

        这个函数被设计来搜索字符串中的第二个单词,它的签名也许会被设计为下面这样:

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

        现在,我们需要同时维护起始和结束两个位置的索引,这两个值基于数据的某个特定状态计算而来,却没有跟数据产生任何程度上的联系。

        于是我们有了 个彼此不相关的变量需要被同步,这可不妙。幸运的是,Rust为这个问题提供了解决方案:字符串切片。 

 

1. 字符串切片

        字符串切片是指向 String 对象中某个连续部分的引用,它的使用方式如下所示:

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

        我们可以在一对方括号中指定切片的范围区间 [starting_index.. ending_index],其中的 starting_index 是切片起始位置的索引值,ending_index 是切片终止位置的下一个索引值。

        切片数据结构在内部存储了指向起始位置的引用和一个描述切片长度的字段,这个描述切片长度的字段等价于 ending_index 减去 starting_index

        所以在上面示例的❶中,world 是一个指向变量 s 第七个字节并且长度为 5 的切片。下图中所展示的是字符串切片的图解:

        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: 

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 {

        由于编译器会确保指向 String 的引用持续有效,所以我们新设计的接口变得更加健壮且直观了。还记得在上面示例中故意构造出的错误吗?

        那段代码在搜索完成并保存索引后清空了字符串的内容,这使得我们存储的索引不再有效。它在逻辑上明显是有问题的,却不会触发任何编译错误,这个问题只会在我们使用第一个单词的索引去读取空字符串时暴露出来。

        切片的引入使我们可以在开发早期快速地发现此类错误。在上面示例中,新的 first_word 函数在编译时会抛出一个错误,尝试运行以下代码: 

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

        编译错误如下所示:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable--> src/main.rs:6:5|
4 |     let word = first_word(&s);|                            - immutable borrow occurs here
5 |
6 |     s.clear(); // 错误!|     ^ mutable borrow occurs here
7 | }| - immutable borrow ends here

        回忆一下借用规则,当我们拥有了某个变量的不可变引用时,我们就无法同时取得该变量的可变引用。

        由于 clear 需要截断当前的 String 实例,所以调用 clear 需要传入一个可变引用。这就是编译失败的原因。Rust不仅使我们的API更加易用,它还在编译过程中帮助我们避免了此类错误。 

字符串字面量就是切片

        还记得我们讲过字符串字面量被直接存储在了二进制程序中吗?在学习了切片之后,我们现在可以更恰当地理解字符串字面量了: 

let s = "Hello, world!";

        在这里,变量 s 的类型其实就是 &str:它是一个指向二进制程序特定位置的切片。正是由于 &str 是一个不可变的引用,所以字符串字面量自然才是不可变的。 

将字符串切片作为参数

        既然我们可以分别创建字符串字面量和String的切片,那么就能够进一步优化first_word函数的接口,下面是它目前的签名:

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

        比较有经验的Rust开发者往往会采用下面的写法,这种改进后的签名使函数可以同时处理 String &str: 

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

示例4-9:使用字符串切片作为参数s的类型来改进first_word函数

        当你持有字符串切片时,你可以直接调用这个函数。而当你持有 String 时,你可以创建一个完整 String 的切片来作为参数。

        在定义函数时使用字符串切片来代替字符串引用会使我们的 API 更加通用,且不会损失任何功能,尝试运行以下代码: 

fn main() { let my_string = String::from("hello world"); // first_word 可以接收String对象的切片作为参数 let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word 可以接收字符串字面量的切片作为参数 let word = first_word(&my_string_literal[..]); // 由于字符串字面量本身就是切片,所以我们可以在这里直接将它传入函数,// 而不需要使用额外的切片语法! let word = first_word(my_string_literal); 
} 

 

2. 其他类型的切片

        从名字上就可以看出来,字符串切片是专门用于字符串的。但实际上,Rust还有其他更加通用的切片类型,以下面的数组为例:

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

        就像我们想要引用字符串的某个部分一样,你也可能会希望引用数组的某个部分。这时,我们可以这样做: 

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

        这里的切片类型是 &[i32],它在内部存储了一个指向起始元素的引用及长度,这与字符串切片的工作机制完全一样。你将在各种各样的集合中接触到此类切片,而我们会在后面文章中讨论动态数组时再来介绍那些常用的集合。 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/590121.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp在Windows系统上打包IOS

获取UDID 使用苹果手机自带Safari浏览器可获取UDID :https://www.pgyer.com/udid 苹果开发者官网:https://developer.apple.com/ 生成开发证书 安装OpenSSL并配置环境变量:http://slproweb.com/products/Win32OpenSSL.html 管理员打开命令行 where …

12.29最小生成数K算法复习(注意输入输出格式),校园最短路径(通过PRE实现路径输出,以及输入输出格式注意)

7-2 最小生成树-kruskal算法 分数 15 const int maxn 1000; struct edge {int u, v, w; }e[maxn]; int n, m, f[30]; bool cmp(edge a, edge b) {return a.w < b.w; } int find(int x) {if (f[x] x) {return x;}else {f[x] find(f[x]);return f[x];} } //int arr[100…

vue脚手架安装

1、安装&#xff1a; npm i vue/cli -g(-g全局安装,全名global) vue --version 查看版本号 2、使用 vue create 项目名称 3、安装选择项 最后一个选N

【Redis-03】Redis数据结构与对象原理 -下篇

承接上篇【Redis-02】Redis数据结构与对象原理 -上篇 8. type-字符串string 8.1 字符串的三种encoding编码&#xff08;int embstr raw&#xff09; 如果保存的是整型&#xff0c;并且可以用long类型标识&#xff08;-9223372036854775808到9223372036854775807&#xff09…

【热学】欧阳欣院士北京大学

笔记正在逐渐完善Ing~博主主页还有其他上万字精品笔记欢迎自取 ​编辑P1第1讲 绪论 33:01 关于热学的基本概念和原理的讲解。热学是一门基础科学&#xff0c;它涉及到热运动和热力学的规律。热运动是指物体内部粒子的无序运动&#xff0c;热力学是研究热运动的宏观规律。视频介…

IO进程线程 day1 IO基础+标准IO

1、使用fgets统计一个文件的行号 #include <stdio.h> #include<string.h> #include<stdlib.h> int main(int argc, const char *argv[]) {FILE *fpNULL;if((fpfopen("1.c","r"))NULL){return -1;}int count0;char buf;while(buf!EOF){b…

C++多态性——(1)初识多态

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 苦难和幸福一样&#xff0c;都是生命盛…

写代码易错点整理

写代码易错点整理 变量重复声明引用的swap使用push_back更好用背包 变量重复声明 会导致越界错误&#xff0c;结果错误等。调试时会发现某个值突然很大&#xff0c;有可能是重复声明导致的。 引用的swap使用 比如一个函数中 bool func(vector<vector<int>>&…

openFeign调用接口时传递表单参数、Json参数、HttpServletRequest对象

目录 一、控制器二、Feign客户端 openFeign调用接口时传递Json参数以及httpservletrequest对象 一、控制器 RestController RequestMapping("test") public class TestController {/*** 用来测试表单参数传递&#xff1a;单个参数*/PostMapping(value "/formD…

modelsim安装使用

目录 modelsim 简介 modelsim 简介 ModelSim 是三大仿真器公司之一mentor的产品&#xff0c;他可以模拟行为、RTL 和门级代码 - 通过独立于平台的编译提高设计质量和调试效率。单内核模拟器技术可在一种设计中透明地混合 VHDL 和 Verilog&#xff0c;常用在fpga 的仿真中。 #…

PAT乙级1045 快速排序

著名的快速排序算法里有一个经典的划分过程&#xff1a;我们通常采用某种方法取一个元素作为主元&#xff0c;通过交换&#xff0c;把比主元小的元素放到它的左边&#xff0c;比主元大的元素放到它的右边。 给定划分后的 N 个互不相同的正整数的排列&#xff0c;请问有多少个元…

Pointnet++环境配置(Windows11和ubuntu)及训练教程

本文使用的是Pytorch版本的Pointnet代码&#xff0c;主要包含完整的环境配置及训练过程&#xff0c;包括在训练时遇到的坑。 目录 1.Windows11环境配置 2.Ubuntu环境配置 3.训练教程 3.1分类&#xff08;Classification&#xff09;训练 3.2零件分割&#xff08;Part Segme…

leetcode中shell题解

常见的shell awk用法 # grades.txt 如下&#xff1a;Alice 85 Bob 92 Charlie 78 Alice 90 Bob 88# awk程序 {name $1score $2sum[name] scorecount[name] }END {for (name in sum) {avg sum[name] / count[name]printf "学生 %s 的平均分是 %.2f\n", name, av…

中科亿海微UART协议

引言 在现代数字系统设计中&#xff0c;通信是一个至关重要的方面。而UART&#xff08;通用异步接收器/发送器&#xff09;协议作为一种常见的串行通信协议&#xff0c;被广泛应用于各种数字系统中。FPGA&#xff08;现场可编程门阵列&#xff09;作为一种灵活可编程的硬件平台…

个体诊所软件方案,农村医疗服务站社区门诊电子处方管理系统软件教程

个体诊所软件方案&#xff0c;农村医疗服务站社区门诊电子处方管理系统软件教程 一、软件程序问答 1、处方单软件有病历汇总吗 如下图&#xff0c;软件以 佳易王电子处方软件V17.2版本为例说明 点击 病历汇总统计 按钮&#xff0c; 可以按明细查询或病历汇总查询&#xf…

基于JavaWeb实验室预约管理系统(源码+数据库+文档)

一、项目简介 本项目是一套基于JavaWeb实验室预约管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;e…

rime中州韵 help lua Translator

lua 是 Rime中州韵/小狼毫输入法强大的武器&#xff0c;掌握如何在Rime中州韵/小狼毫中使用lua&#xff0c;你将体验到什么叫 随心所欲。 先看效果 在 rime中州韵 输入效果一览 中的 &#x1f447; help效果 一节中&#xff0c; 我们看到了在Rime中州韵/小狼毫输入法中输入 h…

cpu优化方法

如何看谁拉高了cpu&#xff1f; cpu高的时候有没有抓到进程threadtop&#xff0c;从threadtop找到top 3线程和正常场景对比一下就知道是否有异常对明显有异常的线程&#xff0c;看下是否抓到simplerperf trace或systrace&#xff0c;从trace中找到高频调用栈是否异常如果没有…

力扣133. 克隆图

深度优先遍历 思路&#xff1a; 使用一个哈希表存储已经被克隆过的节点&#xff0c;key 为原节点&#xff0c;value 为克隆的节点&#xff1b;从原节点开始遍历&#xff0c;如果已经被克隆过&#xff0c;则回到其克隆节点&#xff1b;否则&#xff0c;克隆该节点&#xff0c;并…

常见位运算模板方法总结(包含五道例题)

哈喽大家好&#xff0c;今天博主给大家带来算法基础常见位运算的模板&#xff0c;可以说大家遇到的百分之九十与位运算有关的题都可以用得上。话不多上我们上干货&#xff1a; 一.基础位运算符 << 左移运算符 >> 右移运算符 ~ 取反 & 与运算 | …