Rust Slice(切片)类型
切片(Slice)是对数据值的部分引用,是一种不持有所有权的数据类型。
切片这个名字往往出现在生物课上,我们做样本玻片的时候要从生物体上获取切片,以供在显微镜上观察。在 Rust 中,切片的意思大致也是这样,只不过它属于数据的取材引用。
一、编写处理字符串的函数
这里提供两种实现方法:一是通过以往的知识直接用String类型来解答,另一种是学习过本文的字符串切片类型后再去解答。
1、目标函数特点
接收字符串作为参数
返回它在这个字符串里找到的第一个单词
如果没有找到空格,将字符串全部返回
2、使用String粗略实现
既然是找到第一个单词,不妨找到第一个空格所在的索引,后续从头开始打印到索引位置即可:
fn main() { println!("切片的学习"); let mut s =String::from("hello world"); let index=first_world(&s); s.clear();//这里清空s字符串,但是仍然可以得到第一个空格的索引 println!("第一个空格出现的索引为:{}",index); } fn first_world(str:&String)->usize{ let bytes=str.as_bytes(); for(i,&item) in bytes.iter().enumerate(){ if item==b' '{ return i; } } str.len() }
运行结果:
这里创建String类型的s并赋值"hello world",第一个空格的索引应为5,接下来用first_world函数来实现。
函数形参的类型是String的引用,返回值是usize,str.as_bytes()含义为将str字符串转换为一个字节数组bytes,然后我们开始用for循环对字节数组遍历:
for(i,&item) in bytes.iter().enumerate() 中 (i,&iten) 是一个元组,i是元组的索引,即每个i对应着一个item,要注意它是一个引用,我们加上&之后解引用就成了一个u8类型的值了;然后bytes.iter()是该字节数组的一个迭代器,会依次返回字节数组里的元素;enumerate()方法会将迭代器遍历的元素包装成一个个的小元组,而刚开始for循环里的元组实际上是一个模式匹配,用来解构返回的小元组从而得到字节数组中每个索引和每个索引对应的元素值。
将每个元素值和b' '(b’A’是字节的数据类型' '内用来放置特定的字节,这里我直接填上一个空格)比较,如果相等就返回索引,也就是第一个空格的索引,如果遍历到最后都没有空格,那就返回整个字符串的长度。
在主函数中调用first_world方法,注意里面传入的是字符串的引用,而且必须加上&,这点是不同于C++的。并用index接收该函数的返回值。虽然程序写完了而且可以得到正确结果,但其实是有bug存在的:此时的字符串s和索引并没有关联,如果我把字符串清空,此时仍然可以输出索引位置,这很明显是不合理的,因此就要使用接下来分享的字符串切片来解决这个bug。
3、使用字符串切片完整实现
fn main() { println!("切片的学习"); let str=String::from("hello rust"); let new_str=first_world_slice(&str[..]); //str.clear();不可将变量同时借用为可变和不可变的状态 println!("字符串中第一个单词是:{}",new_str); } fn first_world_slice(s:&str) ->&str{ let bytes=s.as_bytes(); for(i,&it) in bytes.iter().enumerate(){ if it==b' '{ return &s[..i] } } &s[..] }
运行效果:
这里主函数调用函数时传参传入的是&str[. .],即为字符串str转换为完整的字符串切片类型;函数内的返回部分也做了修改,存在空格的话就直接返回其索引前的字符串切片,如果没有空格则返回整个字符串。如果这时候执行str.clear(),编译器就会提示错误,原因就是违反了变量租借的原则,由此可见Rust语言可以使一些API变得简单通用且可以在编译阶段就能阻止错误的发生。
细心观察的朋友可以看到函数的返回值被我改成了字符串切片类型,这样就可以传入字符串或者字符串切片两种类型的参数了:
传入的参数如果使字符串切片就直接写入
如果传入的是字符串类型,那么就创建一个完整的字符串切片即可
定义函数时使用字符串切片来代替字符串引用会使我们的API更加通用,且不会损失功能
二、字符串切片及其与字符串的区别
最简单、最常用的数据切片类型是字符串切片(String Slice)
例如:
fn main() { let s = String::from("broadcast"); let part1 = &s[0..5]; let part2 = &s[5..9]; println!("{}={}+{}", s, part1, part2); } //运行结果:broadcast=broad+cast
Rust 中的字符串类型实质上记录了字符在内存中的起始位置和其长度
part1在内存中的起始位置和字符串s一致
part2在内存中的起始位置指向字符c
s的长度为9,part1长度为5,part2长度为4
&s[x..y]就是字符串切片类型的格式,取值上是前开后闭的:[ x , y ) [x,y)[x,y)
. .y 等价于 0. .y
x. . 等价于位置 x 到数据结束
. . 等价于位置 0 到结束
注意事项
字符串切片的范围索引必须发生在有效的utf-8字符边界内
这是编码问题,后续文章会详细说明
如果尝试从一个多字节的字符串切片中创建字符串切片,程序会报错并退出
这是因为切片类型是没有所有权的
实际上,到目前为止你一定疑惑为什么每一次使用字符串都要这样写String::from("runoob") ,直接写 "runoob" 不行吗?
事已至此我们必须分辨这两者概念的区别了。在 Rust 中有两种常用的字符串类型:str 和 String :
str 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现(&str)。
凡是用双引号包括的字符串常量整体的类型性质都是 &str :
let s = "hello";
1
这里的 s 就是一个 &str 类型的变量。
String 和 str 除了同样拥有一个字符开始位置属性和一个字符串长度属性以外还有一个容量(capacity)属性。
String 和 str 都支持切片,切片的结果是 &str 类型的数据。
注意:切片结果必须是引用类型,但开发者必须自己明示这一点
三、非字符串切片的使用
除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组:
fn main() { let arr = [1, 3, 5, 7, 9]; let part = &arr[1..3]; for i in part.iter() { println!("{}", i); } } //运行结果为:3 5
这里的part是一个数组的切片,起始元素为arr数组的索引1的值,最后一个元素应为arr数组索引2的值
然后利用迭代器依次返回数组切片的元素值