今天来学习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的形式存在。
&str是String中一部分值的引用。
例1:
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:
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:
let hello :&str = "hello";上面代码中hello变量的类型是&str,它是一个指向二进制程序特定位置的字符串(程序加载到内的代码区一般在.rodata中的字符串),&str是一个不可变引用,字符串字面值是不可变的。
可以从一个slice创建另一个新的slice.
例4:
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:
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
之前我们学习过Deref和DerefMut两个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:
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:
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:
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>更加灵活。