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.hqwc.cn/news/314167.html

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

相关文章

TecoGAN视频超分辨率算法

1. 摘要 对抗训练在单图像超分辨率任务中非常成功,因为它可以获得逼真、高度细致的输出结果。因此,当前最优的视频超分辨率方法仍然支持较简单的范数(如 L2)作为对抗损失函数。直接向量范数作损失函数求平均的本质可以轻松带来时…

【Unity入门】RequireComponent的使用

目录 RequireComponent的作用构造函数 RequireComponent的作用 RequireComponent 属性自动将所需的组件添加为依赖项。 当某个脚本必须依赖其他脚本或者组件共同使用时,为了避免人为添加过程的操作失误,可以在代码中使用RequireComponent,它…

CSS-4

平面转换 整体认识 div {margin: 100px 0;width: 100px;height: 100px;background-color: pink;/* 过渡效果 */transition: all 1s;}/* 当鼠标悬停到div时,进行平面转换 */div:hover {transform: translate(800px) rotate(360deg) scale(2) skew(180deg);}作用&…

单字符检测模型charnet使用方法,极简

Git链接 安装按照上面的说明,说下使用。 把tools下面的test做了一点修改,可以读取一张图片,把里面的单个字符都检测和识别出来。 然后绘制到屏幕上。 import torch from charnet.modeling.model import CharNet import cv2, os import num…

HarmonyOS 实践之应用状态变量共享

平时在开发的过程中,我们会在应用中共享数据,在不同的页面间共享信息。虽然常用的共享信息,也可以通过不同页面中组件间信息共享的方式,但有时使用应用级别的状态管理会让开发工作变得简单。 根据不同的使用场景,ArkTS…

Javaweb之JDBC的详细解析

2. JDBC介绍(了解) 2.1 介绍 通过Mybatis的快速入门,我们明白了,通过Mybatis可以很方便的进行数据库的访问操作。但是大家要明白,其实java语言操作数据库呢,只能通过一种方式:使用sun公司提供的 JDBC 规范。 Mybatis…

neovim调试linux内核过程中索引不到对应头文件问题

大家好,我叫徐锦桐,个人博客地址为www.xujintong.com,github地址为https://github.com/jintongxu。平时记录一下学习计算机过程中获取的知识,还有日常折腾的经验,欢迎大家访问。 一、环境 neovim–0.9.4 mason的clang…

牛客网SQL训练5—SQL大厂面试真题

文章目录 一、某音短视频1.各个视频的平均完播率2.平均播放进度大于60%的视频类别3.每类视频近一个月的转发量/率4.每个创作者每月的涨粉率及截止当前的总粉丝量5.国庆期间每类视频点赞量和转发量6.近一个月发布的视频中热度最高的top3视频 二、用户增长场景(某度信…

2 - 表结构 | MySQL键值

表结构 | MySQL键值 表管理1. 库的操作2. 表的操作表的创建与删除表的修改复制表 3. 管理表记录 数据类型数值类型字符类型(汉字或者英文字母)日期时间类型 表头存储与日期时间格式的数据枚举类型 数据批量处理 表管理 客户端把数据存储到数据库服务器上…

12. 整数转罗马数字

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 …

Android 反编译处理Dex

前言 当我们将Android项目打包上架的时候,为了提高被人反编译代码的可能性可以提取 dex 文件对代码进一步做混淆处理。 本文不对相关工具做过多的解释,不了解的可以先熟悉相关工具的使用。 相关工具(点击直接下载) jadx-gui&a…

假两件风衣外套,你喜欢吗?

这款假两件设计的连帽风衣外套,,姐妹们喜欢吗? 宽松版型对身材包容性很强,韩系慵懒风颜色很舒服 时尚百搭怎么穿都好看系列 做了腰部可调节抽绳,想要修身一点的可以自己调节哈 袖口处也做了金属按扣调节,防风保暖 这件风衣也很好搭配&a…