16|数据结构:Vec<T>、&[T]、Box<[T]> ,你真的了解集合容器么?

630 阅读6分钟

正式开始

主要的数据结构

image.png

集合容器

  1. 集合容器就是把一系列拥有相同类型的数据放在一起,统一处理.
  2. 比如字符串 String、数组 [T; n]、列表 Vec 和哈希表 HashMap、切片slice、循环缓冲区 VecDeque、双向列表 LinkedList

切片

  1. 切片是描述一组属于同一类型、长度不确定的、在内存中连续存放的数据结构

  2. 用 [T] 来表述,因为长度不确定,所以切片是个 DST(Dynamically Sized Type)

  3. 切片一般只出现在数据结构的定义中,不能直接访问,在使用中主要用以下形式

    a. &[T]:表示一个只读的切片引用

    b. &mut [T]:表示一个可写的切片引用

    c. Box<[T]>:一个在堆上分配的切片

fn main() {
    // 数组,存放在栈上
    let arr = [1, 2, 3, 4, 5];
    // 向量,存放在堆上
    let vec = vec![1, 2, 3, 4, 5];
    // s1和s2切片相似,对于相同内容数据的相同切片是等价的
    let s1 = &arr[..2];
    let s2 = &vec[..2];
    println!("s1: {:?}, s2: {:?}", s1, s2);

    // &[T] 和 &[T] 是否相等取决于长度和内容是否相等
    assert_eq!(s1, s2);
    // &[T] 可以和 Vec<T>/[T;n] 比较,也会看长度和内容
    assert_eq!(&arr[..], vec);
    assert_eq!(&vec[..], arr);
}

image.png

&[T] 和 &Vec 的区别

image.png

  1. 在使用的时候,可以根据需要将支持切片的具体数据类型,解引用转换成切片类型

  2. 比如 Vec 和 [T; n] 会转化成为 &[T]

    a. Vec 实现了 Deref trait

    b. array 内建了到 &[T] 的解引用


use std::fmt;
fn main() {
    let v = vec![1, 2, 3, 4];

    // Vec 实现了 Deref,&Vec<T> 会被自动解引用为 &[T],符合接口定义
    print_slice(&v);
    // 直接是 &[T],符合接口定义
    print_slice(&v[..]);

    // &Vec<T> 支持 AsRef<[T]>
    print_slice1(&v);
    // &[T] 支持 AsRef<[T]>
    print_slice1(&v[..]);
    // Vec<T> 也支持 AsRef<[T]>
    print_slice1(v);

    let arr = [1, 2, 3, 4];
    // 数组虽没有实现 Deref,但它的解引用就是 &[T]
    print_slice(&arr);
    print_slice(&arr[..]);
    print_slice1(&arr);
    print_slice1(&arr[..]);
    print_slice1(arr);
}

// 注意下面的泛型函数的使用
fn print_slice<T: fmt::Debug>(s: &[T]) {
    println!("{:?}", s);
}

fn print_slice1<T, U>(s: T)
where
    T: AsRef<[U]>,
    U: fmt::Debug,
{
    println!("{:?}", s.as_ref());
}

切片和迭代器 Iterator

  1. 切片是集合数据的视图,而迭代器定义了对集合数据的各种各样的访问操作。
  2. 通过切片的 iter() 方法,我们可以生成一个迭代器,对切片进行迭代
  3. Rust 下的迭代器是个懒接口(lazy interface
举例
fn main() {
    // 这里 Vec<T> 在调用 iter() 时被解引用成 &[T],所以可以访问 iter()
    let result = vec![1, 2, 3, 4]
        .iter()
        .map(|v| v * v)
        .filter(|v| *v < 16)
        .take(1)
        .collect::<Vec<_>>();

    println!("{:?}", result);
}

整个过程是这样的

  1. 在 collect() 执行的时候,它实际试图使用 FromIterator 从迭代器中构建一个集合类型,这会不断调用 next() 获取下一个数据
  2. 此时的 Iterator 是 Take,Take 调自己的 next(),也就是它会调用 Filter 的 next()
  3. Filter 的 next() 实际上调用自己内部的 iter 的 find(),此时内部的 iter 是 Map,find() 会使用 try_fold(),它会继续调用 next(),也就是 Map 的 next()
  4. Map 的 next() 会调用其内部的 iter 取 next() 然后执行 map 函数。而此时内部的 iter 来自 Vec<i32>

只有在 collect() 时,才触发代码一层层调用下去,并且调用会根据需要随时结束

特殊的切片:&str

String 是一个特殊的 Vec,所以在 String 上做切片,也是一个特殊的结构 &str。

image.png String 在解引用时,会转换成 &str


use std::fmt;
fn main() {
    let s = String::from("hello");
    // &String 会被解引用成 &str
    print_slice(&s);
    // &s[..] 和 s.as_str() 一样,都会得到 &str
    print_slice(&s[..]);

    // String 支持 AsRef<str>
    print_slice1(&s);
    print_slice1(&s[..]);
    print_slice1(s.clone());

    // String 也实现了 AsRef<[u8]>,所以下面的代码成立
    // 打印出来是 [104, 101, 108, 108, 111]
    print_slice2(&s);
    print_slice2(&s[..]);
    print_slice2(s);
}

fn print_slice(s: &str) {
    println!("{:?}", s);
}

fn print_slice1<T: AsRef<str>>(s: T) {
    println!("{:?}", s.as_ref());
}

fn print_slice2<T, U>(s: T)
where
    T: AsRef<[U]>,
    U: fmt::Debug,
{
    println!("{:?}", s.as_ref());
}
字符的列表和字符串有什么关系和区别?

use std::iter::FromIterator;

fn main() {
    let arr = ['h', 'e', 'l', 'l', 'o'];
    let vec = vec!['h', 'e', 'l', 'l', 'o'];
    let s = String::from("hello");
    let s1 = &arr[1..3];
    let s2 = &vec[1..3];
    // &str 本身就是一个特殊的 slice
    let s3 = &s[1..3];
    println!("s1: {:?}, s2: {:?}, s3: {:?}", s1, s2, s3);

    // &[char] 和 &[char] 是否相等取决于长度和内容是否相等
    assert_eq!(s1, s2);
    // &[char] 和 &str 不能直接对比,我们把 s3 变成 Vec<char>
    assert_eq!(s2, s3.chars().collect::<Vec<_>>());
    // &[char] 可以通过迭代器转换成 String,String 和 &str 可以直接对比
    assert_eq!(String::from_iter(s2), s3);
}

字符列表可以通过迭代器转换成 String,String 也可以通过 chars() 函数转换成字符列表

image.png

切片的引用和堆上的切片,它们是一回事么?

Box<[T]>和Vec<T>的区别
  1. Vec<T> 有额外的 capacity,可以增长
  2. Box<[T]> 一旦生成就固定下来,没有 capacity,也无法增长
Box<[T]>和切片的引用&[T]的区别

它们都是在栈上有一个包含长度的胖指针,指向存储数据的内存位置

  1. Box<[T]> 只会指向堆
  2. &[T] 指向的位置可以是栈也可以是堆
  3. Box<[T]> 对数据具有所有权,而 &[T] 只是一个借用

image.png

如何产生Box<[T]>呢

从已有的 Vec<T> 中转换


use std::ops::Deref;

fn main() {
    let mut v1 = vec![1, 2, 3, 4];
    v1.push(5);
    println!("cap should be 8: {}", v1.capacity());

    // 从 Vec<T> 转换成 Box<[T]>,此时会丢弃多余的 capacity
    let b1 = v1.into_boxed_slice();
    let mut b2 = b1.clone();

    let v2 = b1.into_vec();
    println!("cap should be exactly 5: {}", v2.capacity());

    assert!(b2.deref() == v2);

    // Box<[T]> 可以更改其内部数据,但无法 push
    b2[0] = 2;
    // b2.push(6);
    println!("b2: {:?}", b2);

    // 注意 Box<[T]> 和 Box<[T; n]> 并不相同
    let b3 = Box::new([2, 2, 3, 4, 5]);
    println!("b3: {:?}", b3);

    // b2 和 b3 相等,但 b3.deref() 和 v2 无法比较
    assert!(b2 == b3);
    // assert!(b3.deref() == v2);
}
  1. Vec<T> 可以通过 into_boxed_slice() 转换成 Box<[T]>,Box<[T]> 也可以通过 into_vec() 转换回 Vec<T>
  2. 当 Vec<T> 转换成 Box<[T]> 时,没有使用到的容量就会被丢弃,整体Box<[T]> 有一个很好的特性是,不像 Box<[T;n]> 那样在编译时就要确定大小,它可以在运行期生成,以后大小不会再改变占用的内存可能会降低
  3. Box<[T]>不像 Box<[T;n]> 那样在编译时就要确定大小,它可以在运行期生成,以后大小不会再改变

当我们需要在堆上创建固定大小的集合数据,且不希望自动增长,那么,可以先创建 Vec,再转换成 Box<[T]>

小结

下图描述了切片和数组 [T;n]、列表 Vec<T>、切片引用 &[T] /&mut [T],以及在堆上分配的切片 Box<[T]> 之间的关系 image.png

链接

  1. PartialEq trait
  2. 切片文档
  3. 切片的iter方法
  4. Iterator Adaptor
  5. Iterator的Map
  6. FromIterator
  7. Iterator take
  8. Iterator Filter
  9. Iterator try_fold
  10. 扩展增强的Iterator itertools
  11. tokio broadcast channel

精选问答

  1. 为什么rust解引用是用&T 来表示,而不是用*T

    a. *&T 是引用,T 是解引用。比如你有一个 b = &mut u32,你可以 *b = 10 来解引用更改 b 指向的内存

    b. Rust 大部分情况下都会做自动解引用(使用 . 的时候)。所以你会感觉很少需要用 *。stackoverflow.com/questions/2…