3.1 为什么要切片slice
还有一个没有所有权的数据类型slice
。
他为什么叫切片?刚才我们已经了解了“引用”,而切片则允许你引用集合中的一段连续的元素序列,而不用引用整个集合。
举个例子,用一个函数获取字符串中的第一个单词,如果字符串中没有出现空格,则认为整个字符串就是一个单词。
// 部分方法可能暂时看不懂,会在后面的章节展开
fn first_word(s: &String) -> usize { //.. 返回一个独立的usize
let bytes = s.as_bytes(); // 将字符串转化为字节数组
for (i, &item) in bytes.iter().enumerate() {
// 创建了一个iter迭代器,enumerate返回元组
if item == b' ' {
return i;
}
}
s.len() // 返回单词结尾的索引
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word 的值为 5
s.clear(); // 这清空了字符串,使其等于 ""
// word 在此处的值仍然是 5,
}
这种情况下存在一个问题,就是实际的s
与获得的word
是分离的,也就是s
改变了之后,word
依然是原来的值,这可能导致事情的管理更加复杂、代码的运行容易出错。
如果再写一个second_word
函数,会更容易出错fn second_word(s: &String) -> (usize, usize) {
,因为你需要同时跟踪一个开始索引、一个结尾索引,并且都与输入的字符串有关,也就是说有三个分离的变量需要同步维护!
因此,需要使用字符串slice。
3.2 字符串slice
他看起来是这样的,而在具体数据结构上,他其实存储了开始位置与长度,比如 let world = &s[6..11];
,world
将是一个包含指向s
索引 6 的指针和长度值 5的slice。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// 可以省略头/尾
let slice = &s[0..2];
let slice = &s[..2];
let slice = &s[3..len];
let slice = &s[3..];
let slice = &s[0..len];
let slice = &s[..];
}
基于slice,改造一下上面的函数
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[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
这里rust就会正确地报错了,但为什么会报错呢?
还记得上文说的借用吗?
1、s.clear()
尝试清除一个不可变引用,这意味着clear
在尝试获取一个s
的可变引用然后再改变s
2、word
使用了&s
也就是不可变引用
3、word
的作用域一直延伸到了最后一行
这意味着在s.clear()
的时候,同时出现了s
的可变引用、不可变引用!所以编译器就报错了。
另外,补充一下,现在我们的签名是这样的:fn first_word(s: &String) -> &str {
而既然能够获取字面量和String
,那我们也可以把签名写成这样,这样会更好:fn first_word(s: &str) -> &str {
当然,在调用的时候,也需要传入一个slice切片。
3.3 其他slice
上文的字符串slice是针对字符串的,但也有通用的slice。
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
你可以对数组进行切片引用,也可以对其他集合使用这类slice,在之后降到vector的章节时会继续展开。