今天来学习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:

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

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

例2:

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

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

例3:

1
let hello :&str = "hello";

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

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

例4:

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

更通用的slice类型

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

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

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

例5:

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

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let word = String::from("hello!");
    let s: &str = &*word;
    println!("{}", s); // hello!

    // 打印word变量的地址
    println!("{:p}", &word); // 0x7ffee56ebb98
    // 打印word变量拥有值的堆内存地址
    println!("{:p}", s); // 0x7f7f14c05d10
}

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

例7:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let v = vec![1, 2, 3];
    let s: &[i32] = &*v;
    println!("{:?}", s); // [1, 2, 3]

    // 打印v变量的地址
    println!("{:p}", &v); // 0x7ffee9ce4b98
    // 打印v变量拥有值的堆内存地址
    println!("{:p}, {:p}", s, &v[0]); // 0x7f87b1d05c30, 0x7f87b1d05c30
}

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

例8:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn main() {
    let arr = [1, 2, 3];
    let vec = vec![1, 2, 3];

    foo(&vec); // Deref强制转换
    foo(&arr);

    foo(&vec[..]);
    foo(&vec[..]);

    foo(&*vec);
}

fn foo(s: &[i32]) {
    println!("{:?}", s);
}

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

参考