07 切片
07 切片
在上一节中,我们提到长度也是数组的一部分,因此会带来诸多限制,比如我们写一个对数组求和的函数,参数要求长度为 10 的数组,那这个函数就只能用于长度为 10 的数组求和,这显然是不好的,因此就有了切片的概念。
切片(Slice)是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装,支持 自动扩容。
切片的本质
切片的本质是一个结构体,包含三个重要的字段:
type slice struct {
array unsafe.Pointer
len int
cap int
}
主要包含三个部分:
指向底层数组的指针: 会指向底层数组中的某个元素,因此切片是引用类型,对切片的操作会共享给所有引用这个数组的切片。
长度: 表示切片当前实际包含的元素个数,长度不能超过容量。
容量: 表示从指向的元素到数组的末尾的长度,如果在增加元素时容量不足,会自动扩容。
因此,对切片的操作是高效的,只需要修改这三个变量即可,而不需要重新分配内存用于新切片。
基本使用
切片的声明类似于数组,只不过不需要指定长度:
var slice_name []T
声明一个切片:
var string_slice []string
println(string_slice == nil) // true
也就是说,默认的切片是一个 nil,此时 对切片的任何读写操作都是违法的,任何一个引用类型都必须分配内存才能使用。
这里简单补充一下 nil,前面我们说过,在 Go 语言中,任何变量声明时如果不赋值都会被进行一个零值初始化,主要类型的零值如下:
bool -> false
number -> 0
string-> ""
pointer -> nil
slice -> nil
map -> nil
channel -> nil
function -> nil
interface -> nil
然后对切片的遍历操作和数组是保持完全一致的:
var a = []int32{1, 2, 3, 4}
// for 循环
for i := 0; i < len(a); i++ {
print(a[i], " ")
}
// range 循环
for _, v := range a {
print(v, " ")
}
其他方式定义切片
基于数组创建
由于切片的底层就是一个数组,所以我们可以基于数组定义切片,类似于左闭右开的操作:
var a = [...]int32{1, 2, 3, 4}
b := a[:2] // [1, 2]
c := a[2:] // [3, 4]
d := a[:] // [1, 2, 3, 4]
基于切片创建
也可以基于一个切片再获取一个切片:
a := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}
b := a[1:3]
c := b[1:5]
这里你可能会有疑问,b 的长度只有 2,创建 c 时怎么可以指定到索引 5 呢,原因在于基于切片构造一个切片时采用的是容量这个参数,前面我们也说了一个切片的容量是起始位置到底层数组的最后,切片 b 可用的元素只有两个,但是它的容量使指针可以遍历到原数组最后。
使用 make
同时还可以使用内置函数 make()
来构造切片,它可以为切片分配内存,因为引用类型的数据不分配内存是无法使用的:
// 语法
make([]T, size, cap) // 分别为元素类型、切片长度、切片容量
a := make([]int32, 2, 10) // 分配十个 int32 类型的内存,但是只对前两个做零值初始化
b := make([]int, 5) // 分配5个 int 类型的内存,并全部做初始化
对切片的操作
比较操作
首先,切片是不能用于比较操作的,唯一一个合法操作是与 nil 进行比较:
var s1 []int // len(s1)=0;cap(s1)=0;s1==nil
var s2 = []int{} // len(s2)=0;cap(s2)=0;s2!=nil
nil 表示这个切片没有指向任何一个数组,只要进行了初始化,哪怕底层数组为空,那也不等于 nil,因此判断一个切片是否为空要使用 len() 函数。
深浅拷贝
然后需要注意的是,多个切片会对同一个数组维持引用,因此对切片的修改会有传递性,也就是浅拷贝,可以看下面一个例子:
s1 := make([]int, 3) // [0 0 0]
s2 := s1
s2[0] = 100
fmt.Println(s1) // [100 0 0]
fmt.Println(s2) // [100 0 0]
同时,也有对切片的深拷贝操作,也就是 copy() 函数:
s2 := []int32{1, 2, 3}
s2Copy := make([]int32, len(s2))
copy(s2Copy, s2)
此时对两个切片的操作就不会共享了,因为这个函数会在堆区把原来的切片的数组重新复制一份,然后新切片指向的是这个新数组,自然不会共享切片的操作了。
增加元素
如果想给切片添加元素,应当采用的方法是 append()
,这是一个动态函数,如果底层数组的容量够用会直接添加元素,但是如果容量不足就会重新分配一个更大容量的新数组,然后再存入新元素,随后返回这个新的切片,这个操作和 cpp 中的 vector 很像。
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
删除元素
然后如果我们想删除切片中的一个元素,其实 Go 中并没有内置这个方法,但是我们可以用 append 来模拟这个操作:
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为 2 的元素
a = append(a[:2], a[3:]...)
也就是说,我们可以利用 a = append(a[:index], a[index+1:]...)
这种方法就可以删除索引为 index 的元素。
扩容操作原理
我们可以看一下 src/runtime/slice.go,里面就有与扩容相关的操作:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
这个部分还是比较简单的,可以自行阅读。