过年放假,荒废了一段时间,接下来继续学习
切片类型
切片让你可以引用集合中连续的元素序列,而不是整个集合。这类似于 Python 中的切片或 C 中的指针运算。
切片由三个部分组成:
- 指向数据起始位置的指针
- 元素数量
- 元素类型
例如,假设我们有一个包含 10 个元素的数组:
fn main() {
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 引用前 5 个元素,这将创建一个新的切片,该切片包含指向数组前五个元素的指针、元素数量为 5 以及元素类型为i32
let slice = &array[0..5];
// 打印切片中的元素
for element in slice {
println!("{}", element);
}
}
切片语法
切片语法类似于数组语法,但有一些关键的区别:
- 数组使用方括号 (
[]
) 来指定元素索引,而切片使用两个点 (..
) 来指定元素范围。例如,array[0..5]
表示从数组的第一个元素开始,到第五个元素结束(不包括第六个元素 - 范围可以省略第一个或第二个元素。省略第一个元素表示从切片开始,省略第二个元素表示到切片结束。
- 范围可以使用负数来表示从切片末尾开始计数。
切片的创建
可以使用以下几种方法创建切片:
- 使用
..
运算符,表示从切片的开始到结束 - 使用
..=
运算符,表示从切片的开始到指定位置(包括该位置) - 使用
..n
运算符,表示从切片的开始到指定位置(不包括该位置) - 使用
n..
运算符,表示从指定位置(包括该位置)到切片的结束 - 使用
n..m
运算符,表示从指定位置(包括该位置)到另一个指定位置(不包括该位置)
例如,以下切片语法等效:
fn main() {
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 引用第 5 个元素之后的所有元素
let slice = &array[5..];
// 打印切片中的元素
for element in slice {
println!("{}", element);
}
// 引用前 5 个元素
let slice = &array[0..5];
// 打印切片中的元素
for element in slice {
println!("{}", element);
}
// 引用前 6 个元素
let slice = &array[0..=5];
// 打印切片中的元素
for element in slice {
println!("{}", element);
}
}
切片操作
切片支持许多与数组相同的操作,包括:
- 迭代
- 索引
- 赋值
- 比较
例如,我们可以使用以下代码迭代切片:
for element in slice {
println!("{}", element);
}
我们也可以使用索引来访问切片中的元素:
let element = slice[2];
切片和内存
切片是引用类型,这意味着它们不直接拥有它们引用的数据。相反,它们指向由其他对象拥有的数据。
这意味着切片可以比数组更有效地使用内存,因为它们可以共享底层数据。
切片和字符串
字符串在 Rust 中本质上是切片。这意味着您可以使用切片语法来操作字符串。
例如,我们可以使用以下代码获取字符串的第一个字符:
let string = "Hello, world!";
let first_char = &string[0..1];
我们来看另外一个例子,字符串切片是对 String
的一部分的引用,它看起来像这样:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{} {}", hello, world);
}
与引用整个String
不同,hello
引用的是String
的一部分,由额外的[0..5]
指定。我们通过指定[starting_index..ending_index]
在方括号内使用范围来创建切片,其中starting_index
是切片中的第一个位置,而ending_index
是切片中最后一个位置的下一个位置。在内部,切片数据结构存储切片的起始位置和长度,这对应于ending_index
减去 起始索引。因此,在let world = &s[6..11];
的情况下,world
将是一个切片,它包含指向字符串s
中索引为6
的字节的指针,长度值为5
。
图 6-1:引用String
的一部分的字符串切片
根据 Rust 的..
range 语法,如果您想从索引 0 开始,可以省略两个句点之前的数值。换句话说,以下内容是等价的:
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
这两个语句都会创建一个指向字符串 s
前 2 个字符的切片。省略起始索引 (0
) 是可选的,因为默认情况下它总是 0
。
同样地,如果您的切片包含String
的最后一个字节,您可以省略尾部数字。 这意味着以下内容是相等的:
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
这两个语句都会创建一个指向字符串 s
整个内容的切片,从第一个字符 (索引 0
) 到最后一个字符 (索引 s.len() - 1
)。省略结束索引 (s.len()
) 是可选的,因为默认情况下它会扩展到字符串的末尾,所以&s[3..]
等效&s[3..len]
。
时隔多日,回来继续学习
当然,我们结合上面的两部分代码,例如下面:
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
我们可以省略起始索引 (0
)和结束索引(s.len()
)来提取字符串的所有部分。
但是有一点:
切片范围必须与有效的 UTF-8 字符边界对齐
如果尝试在多字节字符中间对字符串进行切片,则会导致错误,因为该切片将无法表示有效的字符。本节所有切片中仅假设 ASCII;UTF-8 我们放在后续再详解。
我们现在基于切片再回馈下所有权部分的内容
下面的代码实现查找第一次出现的空格。当我们找到一个空格时,我们使用字符串的开头和空格的索引作为开始和结束索引返回一个字符串切片
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
时,我们会返回一个与基础数据相关的单个值。该值是对原始字符串一部分的引用,而不是新字符串本身和切片中元素的数量组成。这种方式内存使用效率高,但是要求原始字符串保持有效,这样切片才能继续使用。
接下来我们淘气一下,因为编译器将确保对 String
的引用保持有效,我们测试一下:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
我们来看看编译器给出的错误:
$ cargo run
...
error[E0425]: cannot find function `first_word` in this scope
--> src\main.rs:4:16
|
4 | let word = first_word(&s);
| ^^^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `guessing_game` (bin "guessing_game") due to previous error
···
Rust 的借用规则不允许同时存在指向相同数据的可变引用和不可变引用,这意味着在调用 clear
函数之前,我们必须确保不再使用 word
这个不可变引用。如果我们尝试在调用 clear
函数后继续使用 word
,Rust 的编译器会报错。(不得不赞这很棒的编译机制)
字符串文字作为切片
在 Rust 中,字符串字面量 (例如 "Hello, world!") 直接存储在程序的可执行文件中。这些字面量是不可变的,这意味着您无法更改它们的内容。
示例:将字符串字面量视为切片
let s = "Hello, world!";
在这行代码中,s
被声明为类型&str
。&str
表示对字符串的不可变引用。因此s
本质上是引用存储在程序二进制文件中的字符串字面量 "Hello, world!"。
由于s
是对字符串的不可变引用,因此它是不可变的。这与字符串字面量本身的不可变性相辅相成。您无法通过s
来修改原始的字符串字面量。
结语
切片 (slice) 是对序列 (例如字符串或向量) 的一部分的引用。它类似于从数组中提取的一部分子集。与基于索引的范围访问不同,切片在使用过程中不会失效。在运行时,切片以一种称为 "胖指针" 的形式表示。该指针包含指向序列起始位置的指针和序列的长度信息。
与基于索引的范围访问相比,切片的一个主要优势是它在使用过程中不会失效。这是因为切片不仅包含指向数据的位置,还包含数据的长度信息。
这使得 Rust 的编译器可以在编译时检查切片是否越界,从而防止程序出现访问无效内存的情况。
(下一节见