Go语言学习5-切片类型

2024-08-07 14:32:48 浏览数 (2)

引言

上篇我们介绍了 Go 语言的 《数组类型》,本篇将介绍 Go 语言的切片类型。主要如下:

主要内容

切片可以看作是对数组的一种包装形式。切片包装的数组称为该切片的底层数组。切片是针对其底层数组中某个连续片段的描述符。

1. 类型表示法

对于一个元素类型为T的切片类型来说,它的类型字面量就是:

代码语言:go复制
[]T

可以看出,长度并不是切片类型的一部分(即它不会出现在表示切片类型的类型字面量中)。另外,切片的长度是可变的。相同类型的切片值可能会有不同的长度。

切片类型声明中的元素类型也可以是任意一个有效的 Go 语言数据类型。例如:

代码语言:go复制
[]rune

如上用于表示元素类型为 rune 的切片类型。

同样可以把一个匿名结构体类型作为切片类型的元素类型。例如:

代码语言:go复制
[] struct {name, department string}

2. 值表示法

和数组类似,也是复合字面量中的一种,例如:

代码语言:go复制
[]string{"Go", "Python", "Java", "C", "C  ", "PHP"}

在切片值所属的类型中根本就没有关于长度的规定。以下切片是合法的:

代码语言:go复制
[]string{8: "Go", 2: "Swift", "Java", "C", "C  ", "PHP"}

上面的等同于下面的复合字面量:

代码语言:go复制
[]string{0: "", 1: "", 2: "Swift", 3: "Java", 4: "C", 5: "C  ", 6: "PHP", 7: "", 8: "Go"}

3. 属性和基本操作

切片类型的零值为 nil。在初始化之前,一个切片类型的变量值为 nil

切片类型中虽然没有关于长度的声明,但是值是有长度的,体现在它们所包含的元素值的实际数量。可以使用内建函数len来获取切片值的长度。例如:

代码语言:go复制
len([]string{8: "Go", 2: "Swift", "Java", "C", "C  ", "PHP"})

上面计算的结果值为9,这个切片值实际包含了6个被明确指定的string类型值和3个被填充的string类型的零值 ""

注意:在切片类型的零值(即nil)上应用内建函数len会得到0。

切片值的底层实现方式:

一个切片值总会持有一个对某个数组值的引用。一个切片值一旦被初始化,就会与一个包含了其中元素值的数组值相关联。这个数组值被称为引用他的切片值的底层数组。

多个切片值可能会共用一个底层数组。例如,如果把一个切片值复制成多个,或者针对其中的某个连续片段在切片成新的切片值,那么这些切片值所引用的都会是同一个底层数组。对切片值中的元素值的修改,实质上就是对其底层数组上的对应元素的修改。反过来讲,对作为底层元素的数组值中的元素值的改变,也会体现到引用该底层数组其包含该元素值的所有切片值上。

除了长度之外,切片值还有一个很重要的属性---容量。切片值的容量与它所持有的底层数组的长度有关。可以通过内建函数 cap 来获取它。例如:

代码语言:go复制
cap([]string{8: "Go", 2: "Swift", "Java", "C", "C  ", "PHP"})

该切片值的容量是 9 ,就等于它的长度。这是个特例,但很多情况下不是这样,且听慢慢道来。

切片值的底层数据结构:

一个切片值的底层数据结构包含了一个指向底层数组的指针类型值,一个代表了切片长度的 int 类型值和一个代表了切片容量的 int 类型值。

可以使用切片表达式从一个数组值或者切片值上 ”切” 出一个连续片段,并生成一个新的切片值。例如:

代码语言:go复制
array1 := [...]string{"Go", "Swift", "Java", "C", "C  ", "PHP"}
slice1 := array1[:4]

变量 slice1 的值的底层数组实际上就是变量 array1 的值,如下图:

经过上面的描述,大家可能认为一个切片的容量可能就是其底层数组的长度。但事实并非如此,这里再创建一个切片值。例如:

代码语言:go复制
slice2 := array1[3:]

变量 slice2 的值的底层数组也是变量 array1 的值,如下图:

如上所示 slice2 的值的容量与 array1 的值的长度并不相等。实际上,一个切片值的容量是从其中的指针指向的那个元素值到底层数组的最后一个元素值的计数值。切片值的容量的含义是其能够访问到的当前底层数组中的元素值的最大数量。

可以对切片值进行扩展,以查看更多底层数组元素。但是,并不能直接通过再切片的方式来扩展窗口。例如对于上面原始的 slice1 的值进行如下操作:

代码语言:go复制
slice1[4]

这会引起一个运行时恐慌,因为其中的索引值超出了这个切片值当前的长度,这是不允许的。正确拓展的方式如下:

代码语言:go复制
slice1 = slice1[:cap(slice1)]

通过再切片的方式把 slice1 扩展到了最大,可以看到最多的底层数组元素值了。这时 slice1 的值的长度等于其容量。

注意:一个切片值的容量是固定的。也就是说,能够看到的底层数组元素的最大数量是固定的。

不能把切片值扩展到其容量之外,例如:

代码语言:go复制
slice1 = slice1[:cap(slice1) 1] // 超出slice1容量的范围,这样会引起一个运行时恐慌

一个切片值只能向索引递增的方向扩展。例如:

代码语言:go复制
slice2 = slice2[-2:] // 这会引起一个运行时恐慌。另外,切片值不允许由负整数字面量代表。

使用 append 函数来扩展一开始的 slice1 的值:

代码语言:go复制
slice1 = append(slice1, "Ruby", "Erlang")

执行该语句后,切片类型变量 slice1 的值及其底层数组(数组变量 array1 的值)的状态,如下图:

可以看出,slice1 的值的长度已经由原来的 4 增长到了 6 ,与它的容量是相同的。但是由于这个值的长度还没有超出它的容量,所以没必要再创建出一个新的底层数组出来。

原来的 slice1 的值为:

代码语言:go复制
[]string{"Go", "Python", "Java", "C"}

现在的 slice1 的值为:

代码语言:go复制
[]string{"Go", "Python", "Java", "C", "Ruby", "Erlang"}

原来的 array1 的值为:

代码语言:go复制
[6]string{"Go", "Python", "Java", "C", "C  ", "PHP"}

现在的 array1 的值为:

代码语言:go复制
[6]string{"Go", "Python", "Java", "C", "Ruby", "Erlang"}

对现在的 slice1 再进行扩展,如下:

代码语言:go复制
slice1 = append(slice1, "Lisp")

执行这条语句后,变量 slice1 的值的长度就超出了它的容量。这时将会有一个新的数组值被创建并初始化。这个新的数组值将作为在 append 函数新创建的切片值的底层数组,并包含原切片值中的全部元素值以及作为扩展内容的所有元素值。新切片值中的指针将指向其底层数组的第一个元素值,且它长度和容量都与其底层数组的长度相同。最后,这个新的切片值会被赋给变量 slice1

可以使用 append 函数把两个元素类型相同的切片值连接起来。例如:

代码语言:go复制
slice1 = append(slice1, slice...)

当然也可以把数组值作为第二个参数传递给 append 函数。

即使切片类型的变量的值为零值 nil ,也会被看作是长度为 0 的切片值。例如:

代码语言:go复制
slice2 = nil
slice2 = append(slice2, slice1...)

或者如下:

代码语言:go复制
var slice4 []string
slice4 = append(slice4, slice...)

上面第一条语句用于声明(不包含初始化)一个变量。以关键字 var 作为开始,并后跟变量的名称和类型。未被初始化的切片变量的值为 nil

4. 切片使用的复杂用法

切片表达式中添加第三个索引---容量上界索引。如果被指定,那么切片表达式的求值结果的那个切片值的容量就不再是该切片表达式的操作对象的容量与该表达式中的元素下界索引之间的差值,而是容量上界索引与元素下界索引之间的差值。

指定容量上界索引的目的就是为了减小新切片值的容量,可以允许更加灵活的数据隔离策略。

代码语言:go复制
var array2 [10]int = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice5 := array2[2:6]

如上我们可以直接访问和修改 array2 中对应索引值在 [2,6) 范围之内的元素值。

代码语言:go复制
slice5 = slice5[:cap(slice5)]

如上再切片后,可以访问和修改 array2 的值中对应索引值在 [2,10) 范围之内的元素值。

如果 slice5 的值作为数据载体传递给了另一个程序,那么这个程序可以随意地更改 array2 的值中的某些元素值。这就等于暴露了程序中的部分实现细节,并公开了一个可以间接修改程序内部状态的方法,而往往这并不是我们想要的。

如果这样声明 slice5

代码语言:go复制
slice5 := array2[2:6:8]

这样 slice5 的持有者只能访问和修改 array2 的值中对应索引值在 [2,8) 范围之内的元素值。

代码语言:go复制
slice5 = slice5[:cap(slice5)]

即使将 slice5 扩展到最大,也不能通过它访问到 array2 的值中对应索引值大于等于 8 的那些元素。此时,slice5 的值的容量为 6(容量上界索引与元素下界索引的差值)。对于切片操作来说,被操作对象的容量是一个不可逾越的限制。slice5 的值对其底层数组( array2 的值)的 “访问权限” 得到了限制。

如果在 slice5 的值之上的扩展超出了它的容量:

代码语言:go复制
slice5 = append(slice5, []int{10, 11, 12, 13, 14, 15}…)

那么它原有的底层数组就会被替换。也就彻底切断了通过 slice5 访问和修改其原有底层数组中的元素值的途径。

有关切片表达式中的这3个索引的一个限制:当在切片表达式中指定容量上界索引的时候,元素上界索引是不能够省略。但是,在这种情况下元素下界索引却是可以省略的。例如:

代码语言:go复制
slice5[:3:5]//合法的切片表达式
slice5[0::5]//非法的切片表达式,会造成一个编译错误

批量复制切片值中的元素

代码语言:go复制
sliceA := []string{"Notepad", "UltraEdit", "Eclipse"}
sliceB := []string{"Vim", "Emacs", "LiteIDE", "IDEA"}

使用 Go 语言的内建函数 copy,将变量 sliceB 的值中的元素复制到 sliceA 的值中。例如:

代码语言:go复制
n1 := copy(sliceA,sliceB)

内建函数 copy 的作用是把源切片值(第二个参数值)中的元素值复制到目标切片值(第一个参数值)中,并且返回被复制的元素值的数量。copy 函数的两个参数的元素类型必须一致,且它实际复制的元素值的数量将等于长度较短的那个切片值的长度。

变量 n1 的值为 3 , 变量 sliceA 的值被修改为:

代码语言:go复制
[]string{"Vim", "Emacs", "LiteIDE"}

总结

本篇介绍了 Go 语言的 切片类型,下一篇介绍 Go 语言的字典类型,敬请期待!

0 人点赞