数组和切片

我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型。数组和切片有时候会让初学者感到困惑。

它们的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素)。

不过,它们最重要的不同是:数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。

数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,[1]string和[2]string就是两个不同的数组类型。

而切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。

image

我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用

也正因为如此,Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。注意,Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片。

我们通过调用内建函数len,得到数组和切片的长度。通过调用内建函数cap,我们可以得到它们的容量。

但要注意,数组的容量永远等于其长度,都是不可变的。切片的容量却不是这样,并且它的变化是有规律可寻的。

下面我们就通过一道题来了解一下。我们今天的问题就是:怎样正确估算切片的长度和容量?

为此,我编写了一个简单的命令源码文件.


package main

import "fmt"

func main() {
  // 示例1。
  s1 := make([]int, 5)
  fmt.Printf("The length of s1: %d\n", len(s1))
  fmt.Printf("The capacity of s1: %d\n", cap(s1))
  fmt.Printf("The value of s1: %d\n", s1)
  s2 := make([]int, 5, 8)
  fmt.Printf("The length of s2: %d\n", len(s2))
  fmt.Printf("The capacity of s2: %d\n", cap(s2))
  fmt.Printf("The value of s2: %d\n", s2)
}

我描述一下它所做的事情。

首先,我用内建函数make声明了一个[]int类型的变量s1。我传给make函数的第二个参数是5,从而指明了该切片的长度。我用几乎同样的方式声明了切片s2,只不过多传入了一个参数8以指明该切片的容量

现在,具体的问题是:切片s1和s2的容量都是多少?

这道题的典型回答:切片s1和s2的容量分别是5和8。


问题解析

解析一下这道题。s1的容量为什么是5呢?因为我在声明s1的时候把它的长度设置成了5。当我们用make函数初始化切片时,如果不指明其容量,那么它就会和长度一致。如果在初始化时指明了容量,那么切片的实际容量也就是它了。这也正是s2的容量是8的原因。

我们顺便通过s2再来明确下长度、容量以及它们的关系。我在初始化s2代表的切片时,同时也指定了它的长度和容量。

在刚才说过,可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

现在你需要跟着我一起想象:有一个窗口,你可以通过这个窗口看到一个数组,但是不一定能看到该数组中的所有元素,有时候只能看到连续的一部分元素。

现在,这个数组就是切片s2的底层数组,而这个窗口就是切片s2本身。s2的长度实际上指明的就是这个窗口的宽度,决定了你透过s2,可以看到其底层数组中的哪几个连续的元素。

由于s2的长度是5,所以你可以看到底层数组中的第 1 个元素到第 5 个元素,对应的底层数组的索引范围是[0, 4]。

切片代表的窗口也会被划分成一个一个的小格子,就像我们家里的窗户那样。每个小格子都对应着其底层数组中的某一个元素。

我们继续拿s2为例,这个窗口最左边的那个小格子对应的正好是其底层数组中的第一个元素,即索引为0的那个元素。因此可以说,s2中的索引从0到4所指向的元素恰恰就是其底层数组中索引从0到4代表的那 5 个元素。

请记住,当我们用make函数或切片值字面量(比如[]int{1, 2, 3})初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第 1 个元素。

但是当我们通过切片表达式基于某个数组或切片生成新切片的时候,情况就变得复杂起来了。

我们再来看一个例子:

s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6]
fmt.Printf("The length of s4: %d\n", len(s4))
fmt.Printf("The capacity of s4: %d\n", cap(s4))
fmt.Printf("The value of s4: %d\n", s4)

切片s3中有 8 个元素,分别是从1到8的整数。s3的长度和容量都是8。然后,我用切片表达式s3[3:6]初始化了切片s4。问题是,这个s4的长度和容量分别是多少?

这并不难,用减法就可以搞定。首先你要知道,切片表达式中的方括号里的那两个整数都代表什么。我换一种表达方式你也许就清楚了,即:[3, 6)。

[3:6]要表达的就是透过新窗口能看到的s3中元素的索引范围是从3到5(注意,不包括6)。

这里的3可被称为起始索引,6可被称为结束索引。那么s4的长度就是6减去3,即3。因此可以说,s4中的索引从0到2指向的元素对应的是s3及其底层数组中索引从3到5的那 3 个元素。

image

再来看容量。我在前面说过,切片的容量代表了它的底层数组的长度,但这仅限于使用make函数或者切片值字面量初始化切片的情况。

更通用的规则是:一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。

由于s4是通过在s3上施加切片操作得来的,所以s3的底层数组就是s4的底层数组。

又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。

所以,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5。

注意,切片代表的窗口是无法向左扩展的。也就是说,我们永远无法透过s4看到s3中最左边的那 3 个元素。

最后,顺便提一下把切片的窗口向右扩展到最大的方法。对于s4来说,切片表达式s4[0:cap(s4)]就可以做到。我想你应该能看懂。该表达式的结果值(即一个新的切片)会是[]int{4, 5, 6, 7, 8},其长度和容量都是5。

go中 make和new 有什么区别

make 是专门用来创建 slice、map、channel 的值的。它返回的是被创建的值,并且立即可用。
new 是申请一小块内存并标记它是用来存放某个值的。它返回的是指向这块内存的指针,而且这块内存并不会被初始化。或者说,对于一个引用类型的值,那块内存虽然已经有了,但还没法用(因为里面没有针对那个值的数据结构)。
所以,对于引用类型的值,不要用 new,能用 make 就用 make,不能用 make 就用复合字面量来创建。

posted @ 2021-07-27 15:39  hochan_100  阅读(95)  评论(0编辑  收藏  举报