坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.3

前言

昨天我们学了package、crate、module相关的概念,也大致清楚了项目的结构。

坏蛋Dan:rust基础学习--day14

今天往下学第八章了


一些常用的集合(common collection)

这里我个人就冒然叫collections为集合了,如果有问题麻烦评论区里说一下,谢谢。。。

rust标准库中有很多这种集合。

rust中其他大多数据结构(一个变量只允许一种特定类型的值)不同的是,集合允许包含多个不同类型的数据。

当然,元组也可以。但是和数组/元组这俩原始复合类型不同的是,集合的数据是存放到heap也就是堆内存的。存放到heap上也就是说它的内存是动态分配的,也就意味着数据大小是可变的。这点和数组元组是完全不同的。

你应该还记得我们在介绍数组的时候还介绍了另一个和数组长得非常像的:vector。是的,它就是一个集合。

在这一章我们将学到三个集合这种数据结构的类型:

  1. vector
  2. String,是的,它是一个集合
  3. hash map,哈希map,一种特殊的map

不过在开始学习前,我们需要注意,它们都是放在standard library中的,并非核心代码。

如果还想了解其他标准库里的集合,可以访问这里

std::collections - Rustdoc.rust-lang.org/std/collections/index.html


vector

vector实际上是vector类型,是一个泛型。 前面说到过和数组一样,它允许存储多个数据,数据类型必须相同,以及在内存里是连续的。不同点在于内存存储方式(stack/heap)不同。

我们先开看下例子

vector的关键字是Vec,new是关联函数,前面说过了,会返回一个Vec实例。

注意,如果是::new这种方式创建vector,那么得声明数据的类型。

当然,也不是一定就要,比如后面再push个数据进去

之前说到过了,rust的编译器会自动推断类型,所以当你往里面存放一个或者一组类型相同的数据时就会自动推断出这个v变量的类型。

如果你还没想好准备放什么进去,这么写是最好的。但是如果当你已经知道要放啥进去了,这时候这么写就未免有些麻烦。rust提供了Vecmacro,也就是宏,可以理解为语法糖。

关键字是vec!,这里你应该已经知道了怎么知道是不是一个macro,带个!结尾的就是~。

这样你也没必要去声明类型了。

现在我们知道如何创建Vec实例了,接下来该学如何更新这个vector了。


更新vector

上面那个push实际上就是在更新v这个Vec实例。

和别的语言数组一样,都可以用pushlist尾部添加相同类型数据。

当然,这个vector必须是mut可变的。

类似数组的popinsert等方法也是有的,这个到时候用到再说。 我们先来学如何读取vector里的数据。


读取vector元素

既然长得像十分像数组,那自然也是可以通过下标读取的,不过它又是一个集合collection,那自然又可以用集合的方法get来读取元素

不过需要注意的一点是,get返回的是元素的引用。那么同样的,通过下标读取也尽量用引用方式。 因为vector里可以存放多种类型,如果是标量类型等存放到stack内存里的还好,但如果是需要存放到heap上的,很容易就会出问题。

好在rust防住了这种问题

来看个例子

完整的错误如下

这个时候改成引用就没问题了

我们再来看下get的使用

返回的是一个Option的枚举,并且可以看到T&String,是引用类型。

既然是Option,是一个枚举,那当然得配上match,就像下雨天要配德芙一样,这样才优雅丝滑~

用下标还是get取决于你自己,如果你能确定你所传入的下标值是一定有效的,那么你就没必要去选择get,毕竟还得写一个match匹配。

为什么这么说呢?我们来看个例子

我们声明了firstthird两个变量,一个用下标获取第100个,一个用get获取第100个。

还没开始run的时候其实是没报错的,为什么呢?因为vector是放在heap上的,是动态的,runtime的时候才会知道具体是多少。

我们来run一下。

可以看到通过下标的方式获取直接崩溃了。

我们再来看下通过get方法获取

可以看到程序并没有崩溃,而是走了matchNonearm,告知你这是一个无效的值。

所以什么时候用下标方式访问取决于你是否确定这个下标一定不会越界。

另外还有个点我们需要注意:获取引用元素之后并且未失效之前不允许改动原vector

之前学引用的时候我们学到过“一个scoped中不能同时存在不可变引用和可变引用”。

可以改成下面的样子

回到我们的例子,来看下相同问题

为啥我们改的是v却得到相同的报错?

因为push方法,我们看下push这个method的源码

是对v自己的可变引用,所以这里就出现了一个可变和不可变声明。

我们可以调整下代码

我们在声明前调用push就没问题了。

为什么会有这个问题呢????这里应该已经满头问号了。

其实这个和heap也就是堆内存分配的方式有关。

runtime时,在你push之前,内存分配器会根据v的大小去找一块闲置的大于当前v数据需要的空间存放这个v的数据,但是你现在又push了,之前的空间可能不满足需求了,所以这个时候内存分配器又得重新去找一块内存来存现在的v。而原来的空间就闲置了。这个时候如果你在push之前声明了一个不可变引用,它指向的地方还是之前的指针的位置,这个时候就会有问题了。

你可能又要问了,为啥这个可变引用没有失效?因为还没出作用域,只要在作用域内,它就是有效的。

所以说不允许同时有一个可变引用和不可变引用。

ok,我们来继续往下学


遍历vector

既然长得像数组,那自然会被用到遍历,我们来看下例子

和遍历array没啥大差别,你应该注意到了*i += 10这条语句的*符号,这个表示解引用(dereference),这个以后会说到,这里就暂时不说。


配合enum

前面说过vector是一个list,存放的是连续相同类型的数据。那么这些数据自然就是有关联的,那么有关联我们第一印象就是strcut,但是enum枚举类型也是可以的,比如下面这个例子

一个单元格可以存放三种类型的内容:数字、文本和小数。

然后又存在一组单元格,那自然就可以用vector将他们组装到一起。

rust需要在编译的时候就确定好vector的泛型是啥类型,因为这样才能知道如何分配内存给这个vector,如果类型都不清楚,那自然就不知道如何分配了。

声明好类型可以让编译器在编译的时候就知道要分配多少内存,这是一种优化。

另外,vector出了作用域就会失效了,包括它里面的所有元素。


总结

今天我们学了vector的相关知识。

本来打算今天写完整个第八章的,但怎奈内容有些多,尤其是String。所以还是老规矩拆分成三天或者四天内容。

最后如果觉得这篇文章对你有帮助的话,请务必点个赞,谢谢~

参考

  1. ^rust-common-collection https://doc.rust-lang.org/book/ch08-00-common-collections.html
  2. ^rust-store-list-with-vector https://doc.rust-lang.org/book/ch08-01-vectors.html#storing-lists-of-values-with-vectors
  3. ^rust-update-vector https://doc.rust-lang.org/book/ch08-01-vectors.html#updating-a-vector
  4. ^rust-read-element-of-vector https://doc.rust-lang.org/book/ch08-01-vectors.html#reading-elements-of-vectors
  5. ^rust-iterate-vector https://doc.rust-lang.org/book/ch08-01-vectors.html#iterating-over-the-values-in-a-vector
  6. ^rust-use-enum-to-store-multiple-types https://doc.rust-lang.org/book/ch08-01-vectors.html#using-an-enum-to-store-multiple-types

发布于 2022-12-10 13:24・IP 属地广东