什么是Slice
我们知道每种语言的底层数据结构中都会有数组这一结构,数组由连续的内存空间组成,因此系统很容易通过基址寄存器和偏移量来控制索引,从而读到数组的内容。
那么在使用一个数组之前,首先需要创建这个数组结构。于是我们通常需要向内存申请这么一块连续的固定大小的空间,再将数组的起始位置的地址返回给变量,这样,这个变量便持有了对这个数组的引用。
到这里我们知道了,我们创建一个数组需要向内存申请一块指定大小的空间,因此我们必须在创建数组时明确表明所创建数组的大小,即容量。那么如果我们在创建时确实无法明确数组的具体容量怎么办呢?
动态数组解决了这一问题。
什么是动态数组呢?顾名思义,随着我们程序的需求能够进行动态容量调整的数组,就是动态数组。
在Java中,有ArrayList,也就是数组列表。它的底层结构同样是数组,通过初始化一个较小的初始容量来创建初始数组,当数组的大小超过一定阈值时,便以2倍大小进行自动扩容。
而在Go语言中,就是Slice(切片)。
Slice详解
Java中的ArrayList与普通数组array密不可分。slice同样离不开数组结构,slice的底层引用着一个数组对象。
它由3部分组成:
- 指针——指向第一个slice元素对应的底层数组元素的地址
- 长度
- 容量
多个slice可共享底层数据
slice可进行切片操作例如s[i:j],0<=i<=j<=cap(s)。创建一个新的slice,引用s切片的第i个元素到底j-1个元素。
若i或j被省略,则i索引以0代替,j索引以len(s)代替。
切片操作中,范围超过cap(s)上限将抛出panic异常,但超出len(s)则是拓展了slice。
由于我们创建的slice包含指向第一个slice元素的指针,它直接指向底层数组结构,在想函数传递slice时,复制该slice参数仅是对其底层数组创建一个新的别名,也就意味着我们能够在函数中直接操作原内存空间的数据。比如以下反转函数:
func reverse(a []int) {
for i, j := 0, len(a)-1;i<j;i,j = i+1,j-1 {
a[i],a[j] = a[j],a[i]
}
}
反转后:
a := [...]int{1,2,3,4,5}
reverse(a[:])
fmt.Println(a) //[5 4 3 2 1]
append函数
append函数用于向slice追加元素。
例如appendInt函数,调用前首先检查slice底层是否有足够容量保存新添加的元素,有的话,直接扩展slice,将新添加的元素复制到新扩展的空间,返回slice。
若空间不足,则先分配一个足够大的slice用于保存结果。扩展数组时通常直接将当前长度翻倍,演示如下:
func main() {
var x, y []int
for i := 0; i < 10; i++ {
y = appendInt(x, i)
fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
x = y
}
}
每次容量变化会导致重新分配内存和复制操作
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:
runes = append(runes, r)
我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。
var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
Slice内存技巧
- 原字符串切片内存上去除“”返回:
func nonEmpty(string []string) []string {
i := 0
for _,s := range string {
if s != "" {
string[i] = s
i++
}
}
return string[:i]
}
func main() {
data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonEmpty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data) // `["one" "three" "three"]`
}
nonempty函数也可以使用append函数实现:
func nonempty2(strings []string) []string {
out := strings[:0] // zero-length slice of original
for _, s := range strings {
if s != "" {
out = append(out, s)
}
}
return out
}
无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。
一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:
stack = append(stack, v) // push v
stack的顶部位置对应slice的最后一个元素:
top := stack[len(stack)-1] // top of stack
通过收缩stack可以弹出栈顶的元素
stack = stack[:len(stack)-1] // pop
要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}
如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
func remove(slice []int, i int) []int {
slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]
}
本文详细介绍了Go语言中的Slice,其作为动态数组,由指针、长度和容量三部分组成,允许灵活的容量调整。Slice可以进行切片操作,通过append函数可追加元素,并探讨了 Slice 的内存管理技巧,包括模拟栈操作和删除元素的方法。
4万+

被折叠的 条评论
为什么被折叠?



