今天来学习Rust中的slice类型。

为什么需要slice类型

为什么Rust会提供slice类型呢?

Rust中的借用(Borrow语义)可以将一个值在其所有权不发生转移的情况下,借给其他变量使用,借用通过创建引用来实现,Rust中创建引用的行为被称为借用。 但对于常用的集合类型例如Vec<T>, String(String的底层是Vec<u8>),这些集合类型的引用类型&Vec<T>, &String,引用的将是整个集合的内容。

在一些特定的场景中,我们需要只引用集合中一段连续的元素序列,而不是引用整个集合的内容。 因此Rust提供了一类slice类型,使用slice类型可以只引用集合中一部分连续的元素。

我们知道引用类型是没有所有权的类型,slice作为"部分引用"也是没有所有权的类型,所以slice通常以"借用"的形式存在。

rust中slice的语法是使用[start_index..end_index]指定的范围创建一个slice,包含start_index处的元素,而不包含end_index处的元素,rust中切片slice划得的元素个数是end_index - start_index(在这点上与go语言中的slice十分相似)。

例如有一个集合变量x,则创建一个基于x的slice的语法是&x[start_index..end_index],注意创建的slice是以"借用"形式存在,所以在x前面加了引用操作符&

字符串slice: str

str是rust核心中内置的字符串slice,它通常以借用&str的形式存在。

&strString中一部分值的引用。

例1:

1fn main() {
2    let s = String::from("hello world");
3    let hello: &str = &s[..5]; // hello变量的类型可以自动推断, 这里显式给出
4    let mid = &s[4..7];
5    let world = &s[6..];
6    println!("{}", hello); // "hello"
7    println!("{}", mid); // "o w"
8    println!("{}", world); // "world"
9}

需要注意字符串slice指定范围边界的索引必须是有效的UTF-8字符串边界,如果从一个多字节字符的中间位置创建字符串slice,程序将会panic(在这点上与go语言对string类型进行切片操作是不同的,在go中不会panic)。

例2:

1fn main() {
2   let s = String::from("你好 世界");
3   let hello = &s[..1]; // thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '你' (bytes 0..3) of `你好 世界`'
4   println!("{}", hello); 
5}

Rust中字符串字面量实际上就是字符串slice &str。

例3:

1let hello :&str = "hello";

上面代码中hello变量的类型是&str,它是一个指向二进制程序特定位置的字符串(程序加载到内的代码区一般在.rodata中的字符串),&str是一个不可变引用,字符串字面值是不可变的。

可以从一个slice创建另一个新的slice.

例4:

1fn main() {
2    let s = String::from("hello world");
3    let hello: &str = &s[..5];
4    let hel1: &str = &hello[..3];
5    let hel2: &str = &s[..3];
6    assert_eq!(hel1, hel2);
7}

更通用的slice类型

上面我们学习的字符串slice str是专门针对字符串的slice类型。Rust中还提供了更加通用的slice类型。

通用的slice类型表示为[T],一般以"借用"&[T]的形式存在。

我们可以从一个数组创建slice,只引用数组的一部分;还可以从Vec<T>创建slice,引用Vec集合的一部分元素。

例5:

1fn main() {
2    let arr: [i32; 3] = [1, 2, 3]; // arr是数组类型[i32; 3], 类型可自动推导, 这里显式给出
3    let vec: Vec<i32> = vec![2, 3, 4, 5]; // vec变量是Vec<i32>类型, 类型可自动推导, 这里显式给出
4    let s1: &[i32] = &arr[1..3]; // s1是类型为`&[i32]`的slice, 类型可自动推导, 这里显式给出
5    let s2: &[i32] = &vec[0..2]; // s2是类型为`&[i32]`的slice, 类型可自动推导, 这里显式给出
6    assert_eq!(s1, s2);
7    println!("{:?}, {:?}", s1, s2); // [2, 3], [2, 3]
8}

slice类型和Deref trait

之前我们学习过DerefDerefMut两个trait。

  • Deref用于不可变引用的解引用操作,如*v。 实现Deref trait允许我们重载不可变引用的解引用运算符*。实现了Deref trait的智能指针可以被当做常规引用来对待。
  • DerefMut用于可变引用的解引用操作,如*v = 1;。 实现DerefMut trait允许我们重载可变应用的解引用运算符*。实现了DerefMut trait的智能指针可以被当做常规可变引用来对待。

Rust中的String实现了Deref和DerefMut,String实现Deref中的关联类型Target是str。因此,如果对String类型进行解引用操作的话,就得到了引用整个String的str

例6:

 1fn main() {
 2    let word = String::from("hello!");
 3    let s: &str = &*word;
 4    println!("{}", s); // hello!
 5
 6    // 打印word变量的地址
 7    println!("{:p}", &word); // 0x7ffee56ebb98
 8    // 打印word变量拥有值的堆内存地址
 9    println!("{:p}", s); // 0x7f7f14c05d10
10}

Rust中的Vec<T>实现了Deref和DerefMut,Vec<T>实现Deref中的关联类型Target是[T]。因此,如果对Vec<T>类型进行解引用操作的话,就得到了引用整个String的[T]

例7:

 1fn main() {
 2    let v = vec![1, 2, 3];
 3    let s: &[i32] = &*v;
 4    println!("{:?}", s); // [1, 2, 3]
 5
 6    // 打印v变量的地址
 7    println!("{:p}", &v); // 0x7ffee9ce4b98
 8    // 打印v变量拥有值的堆内存地址
 9    println!("{:p}, {:p}", s, &v[0]); // 0x7f87b1d05c30, 0x7f87b1d05c30
10}

有了本节对slice类型的学习,再来复习一下之前学过的"传参时Deref强制转换",实现Deref的类型的引用,会自动隐式强制转换为通过Deref所能转换的类型。

例8:

 1fn main() {
 2    let arr = [1, 2, 3];
 3    let vec = vec![1, 2, 3];
 4
 5    foo(&vec); // Deref强制转换
 6    foo(&arr);
 7
 8    foo(&vec[..]);
 9    foo(&vec[..]);
10
11    foo(&*vec);
12}
13
14fn foo(s: &[i32]) {
15    println!("{:?}", s);
16}

例8中在设计foo函数签名时,设计参数为slice类型&[i32],能够同时适用于&Vec<i32>&[i32],比将foo函数的参数设计成&Vec<i32>更加灵活。

参考