4.5.0. 写在正文之前
这是第四章的最后一篇文章了,在这里也顺便对这章做一个总结:
所有权、借用和切片的概念确保 Rust 程序在编译时的内存安全。 Rust语言让程序员能够以与其他系统编程语言相同的方式控制内存使用情况,但是当数据所有者超出范围时,让数据所有者自动清理该数据意味着您无需编写和调试额外的代码来获得这个控制权。
看完这篇文章,相信你会由衷的感叹Rust所有权机制到底有多么神奇和先进。
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
4.5.1. 切片的特性
-
1. 类型和结构
- 切片类型的表示方式是:
&[T]
或&mut [T]
,其中 T 是切片中元素的类型。 - 不可变切片:
&[T]
,只允许读取操作。 - 可变切片:
&mut [T]
,允许修改操作。
- 切片类型的表示方式是:
-
2. 不拥有数据
- 切片本质上是对底层数据的引用,因此它不拥有数据。
- 切片的生命周期与底层数据一致,当底层数据被销毁时,切片也失效。
4.5.2. 字符串切片
以一道题为例:
编写一个函数,它接受字符串作为参数,它返回它在这个字符串中找到的第一个单词,如果函数没找到任何空格,那么整个字符串就被返回。
fn main() {let s = String::from("Hello world");let word_index = first_word(&s);println!("{}", word_index);
}
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()
}
- 因为需要逐个元素地遍历
String
并检查值是否为空格,所以使用as_bytes
方法将String
转换为字节数组. - 迭代器在以后会讲到,现在只需要知道
iter
是一个方法,用来逐一获取集合中的每个元素。enumerate
是一个工具,它在iter
的基础上,为每个元素附加一个索引,并将结果作为元组返回。返回元组的第一个元素是索引,第二个元素是对该元素的引用。
程序成功编译,输出是5。也就是Hello
后边的空格的索引位置
我们现在有办法找出字符串中第一个单词末尾的索引,但是有一个问题。我们自己返回一个usize
,但它只是&String
上下文中的一个有意义的数字。换句话说,因为它是与String
不同的值,所以不能保证它在将来仍然有效。
比如因为某些原因代码在调用first_word
之后写了s.clean();
这行来清空s
,此时的word_index
这个变量就没有意义了;也可以说,Rust编译器发现不了代码使用了s.clean()
但word_index
仍然存在的错误,如果你在之后的代码中还使用了word_index
去打印字符,那显然就会发生错误。
这类的API(或者叫函数设计)要求随时关注word_index
的有效性,确保这个索引和这个String
变量s
它们之间的同步性。偏偏这类工作往往相当繁琐而且特别容易出错,所以针对这类问题Rust提供了字符串切片。
字符串切片是指向字符串中一部分内容的引用。
在原字符串名前加上&
代表对它的引用,在后加上[开始索引..结束索引]
,表示引用这个字符串的一部分。注意,[]
内的区间是左闭右开,所以结束索引是切片终止位的下一个索引值。顺口溜:包左不包右。
fn main() {let s = String::from("hello world");let hello = &s[0..5];let world = &s[6..11];
}
在这个例子中把s
从0到5的索引区间(包括0不包括5),也就是"Hello"这部分赋给了hello
这个变量;把从6到11的索引区间(包括6不包括11),也就是"world"这个部分赋给了world
这个变量
由图可见,world
这个变量并不会独立于s
而存在,这样使得编译器能够在编译过程中就发现许多潜在的问题。
当然,对于索引的写法,还有几种省略的方式:
let hello = &s[0..5];
这个变量是从索引0开始截取的,Rust允许这样的等价写法:
let hello = &s[..5];
let world = &s[6..11];
这个变量截取到了s
的最后一个元素,Rust允许这样的等价写法:
let world = &s[6..];
如果想截取整个字符串,那就可以:
let whole = &s[..];
注意事项
- 字符串切片的范围索引必须发生在有效的
utf-8
边界内 - 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
重写代码
学了切片之后,就可以修改文章开头的代码来进一步优化了:
fn main() {let s = String::from("Hello world");let word = first_word(&s);println!("{}", word);
}
fn first_word(s:&String) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];} }&s[..]
}
&str
表示字符串切片
这个时候如果在word = first_word(&s);
这一行之后加上s.clean();
,Rust就能够发现错误并报错:
error[E0502]:cannot borrow `s` as mutable because it is also borrowed as immutable
因为在同一个作用域中出现了可变引用s.clean()
和不可变引用&s
,违反了借用规则
PS:s.clean()
等价于clean(&mut s)
4.5.3. 字符串字面值就是切片
字符串字面值被直接存储在二进制程序之中,在程序运行时会被放入静态内存里
let s = "Hello, World!";
变量s
的类型是&str
,它是一个指向二进制程序特定位置的切片。&str
不可用,所以字符串字面值也是不可变的。
4.5.4. 将字符串切片作为参数传递
fn first_word(s:&String) -> &str {
这是刚刚优化过的代码中声明函数的那一行,这种写法本身完全没有任何问题。但有经验的Rust开发者会使用&str
作为s
的参数类型,因为这样就可以同时接收String
和&str
类型的参数了:
- 如果你传入的的值是字符串切片,那么直接调用即可
- 如果值类型是
String
,那么可以传入&String
类型的实参,当函数参数需要&str
而你传递的是&String
时,Rust
会隐式调用Deref
,将&String
转换为&str
。
定义函数时使用字符串切片来代替字符串引用会使APU更加通用,且不会损失任何功能。
根据它,还可以再进一步地优化之前的代码:
fn main() {let s = String::from("Hello world");let word = first_word(&s);println!("{}", word);
}
fn first_word(s:&str) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];} }&s[..]
}
这行:
let word = first_word(&s);
也可以写成:
let word = first_word(&s[..]);
对于前者,Rust
会隐式调用Deref
,将&String
转换为&str
;后者是手动转换为&str
类型
4.5.5. 其他类型的切片
fn main() { let number = [1, 2, 3, 4, 5]; let num = &number[1..3]; println!("{:?}", num);
}
数组也可以使用切片。num
这个切片的本质就是存储了指向number
中切片截取的起始点(这个例子中是索引为1的位置)的指针与长度的信息。
其输出是:
[2, 3]