www.zhblog.net

底层为数组的 slice


在〈身为复合值的数组〉中看过数组,有的场合需要数组,然而,若只想处理数组中某片局部,或者以更高阶的观点看待一片数据(而不是从固定长度的数组观点),那么可以使用 slice。

创建一个 slice

如果需要一个 slice,可以使用make函数,举个例子来说,可以如下创建一个长度与容量皆为 5 的 slice,并返回slice的参考,类型为[]int

package main

import "fmt"

func main() {
    s1 := make([]int, 5)
    s2 := s1
    fmt.Println(s1) // [0 0 0 0 0]
    fmt.Println(s2) // [0 0 0 0 0]
    s1[0] = 1
    fmt.Println(s1) // [1 0 0 0 0]
    fmt.Println(s2) // [1 0 0 0 0]
    s2[1] = 2
    fmt.Println(s1) // [1 2 0 0 0]
    fmt.Println(s2) // [1 2 0 0 0]
}

如上所示,s1s2会是个参考(Reference),类型是[]int,参考至同一个 slice 实例。

透过s1s2操作时,操作的对象是变量参考之实例,就底层来说,make([]int, 5)在内存某位置创建了 slice 实例,而s1存储了该位置,如果改变了s1存储的地址值,那透过s1操作时,就会是另一个 slice 实例了。

将变量的参考对象指定给另一个变量时,底层是将存储的地址值指定给该变量,在上例中,s2 := s1,就是将s1存储的地址值,指定给s2,因此透过s2操作的对象,与s1操作的对象是相同的,透过其中一个名称来改变 slice 的元素内容,透过另一个名称获取 slice 的元素值,就会是改变后的值。

上例也可以写为:

package main

import "fmt"

func main() {
    var s1 []int = make([]int, 5)
    var s2 []int    // s2 这时是 nil
    s2 = s1         // 将 s1 的参考对象指定给 s2
    fmt.Println(s1) // [0 0 0 0 0]
    fmt.Println(s2) // [0 0 0 0 0]
    s1[0] = 1
    fmt.Println(s1) // [1 0 0 0 0]
    fmt.Println(s2) // [1 0 0 0 0]
    s2[1] = 2
    fmt.Println(s1) // [1 2 0 0 0]
    fmt.Println(s2) // [1 2 0 0 0]
}

在 Go 中,参考的默认空值都是nil。slice 无法进行==比较,slice 唯一可以用==比较的对象是nil,存储 slice 参考的变量也无法进行==比较,若真想知道两个变量参考的是否同一 slice,可以如下透过反射机制来得知:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    s1 := make([]int, 5)
    s2 := s1
    fmt.Println(reflect.ValueOf(s1).Pointer() == reflect.ValueOf(s2).Pointer())
}

若事先知道 slice 的值,也可以使用 slice 字面常量:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    s1 := []int{1, 2, 3, 4, 5}
    a1 := [...]int{1, 2, 3, 4, 5}
    fmt.Println(reflect.TypeOf(s1)) // []int
    fmt.Println(reflect.TypeOf(a1)) // [5]int
}

注意到,创建 slice 时,方括号中是没有...的,如果方括号中有...,那会是个数组,而不是个 slice,如上可看到的,s1的类型会是[]int,然而,a1的类型会是[5]ints1是个参考,可以指向某个 slice 实例,s1本身存储的地址值可以改变,而a1本身就是数组,从a1的位置开始,有连续 5 个int空间可用来存储int值,a1本身的位置是固定的,无法改变。

使用 slice 字面常量时,还可以初始化特定索引处的值。例如:

slice := []int{10, 20, 30, 10: 100, 20: 200}
// 显示 [10 20 30 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 0 0 200]
fmt.Println(slice)

在上面的例子中,索引 0、1、2 被初始化为 10、20、30,之后指定索引 10 为 100,索引 20 为 200,其余未指定处初始化为int空值 0。

从数组或 slice 创建 slice

如果有个现成的数组,可以从数组中创建 slice,例如,从数组的索引 1 到 4(不包括)创建一个 slice 的话,可以如下:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice := arr[1:4]
    fmt.Println(reflect.TypeOf(arr))   // [10]int
    fmt.Println(reflect.TypeOf(slice)) // []int
    fmt.Println(len(slice))            // 3
    fmt.Println(cap(slice))            // 9

    fmt.Println(slice)   // [2 3 4]
    fmt.Println(arr)     // [1 2 3 4 5 6 7 8 9 10]

    slice[0] = 20
    fmt.Println(slice)   // [20 3 4]
    fmt.Println(arr)     // [1 20 3 4 5 6 7 8 9 10]
}

在这边可以看到,slice 的长度可以使用len得知,而容量可以使用cap函数得知,如果从数组中切出 slice,长度是 slice 可参考的元素长度,而容量默认为从 slice 索引 0 处起算的底层数组元素长度,如图所示:

slice 与数组

是的!slice 底层实际上还是个数组,若两个 slice 底层是共用同一个数组,从一个 slice 操作,另一个 slice 获取的值也就会反映变化,也因此在上面的例子中,你透过slice[0]设定值为 20,底层的数组也会因而反映出变化,透过 slice 指定索引获取元素值时,不能超出 slice 的长度,不然会出现 index out of range 的错误。

注意,单是定义var slice []int的话,slice默认空值会是nil,也就是相当于var slice []int = nil,也就是slice参考至nil,此时len(slice)cap(slice)的结果都会是 0,fmt.Println的显示会是 [],==用于 slice 时,唯一能用来比较的就是nil

方才使用make([]int, 5)函数创建 slice 时,只指定了长度为 5,而容量就默认与长度相同,实际上,可以分别指定容量与长度,例如:

package main

import "fmt"

func main() {
    slice := make([]int, 5, 10)
    fmt.Println(slice)       // [0 0 0 0 0]
    fmt.Println(len(slice))  // 5
    fmt.Println(cap(slice))  // 10
}

指定索引从数组中产生 slice时,若省略冒号之后的数字,则创建的 slice,默认可获取至数组尾端的元素,也就是长度将等于容量,例如,若arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},那么arr[3:]的话,获取的 slice 可以访问的元素为 {4, 5, 6, 7, 8, 9, 10},长度与容量皆为 7;如果省略冒号之前的数字,默认从索引 0 开始,例如arr[:2]会获取 {1, 2},长度为 2,容量为 10;如果是arr[:],那么就是获取全部数组内容了,长度与容量皆为 10。

Go 1.2 开始,可以在[]中指定三个数字,以冒号区隔,第三个数字指定的是 slice 以原数组哪个索引作为边界。例如:

package main

import "fmt"

func main() {
    arr := [...]int{1, 2, 3, 4, 5}
    slice1 := arr[0:2:4]
    fmt.Println(slice1)      // [1 2]
    fmt.Println(len(slice1)) // 2
    fmt.Println(cap(slice1)) // 4
}

第三个数字指定的索引不能超过数组边界,不然会发生 invalid slice index 的错误。

也可以从 slice 中产生 slice,产生的 slice 底层还是同一个数组。例如:

package main

import "fmt"

func main() {
    arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice1 := arr[:5]
    slice2 := slice1[:3]

    fmt.Println(slice1) // [1 2 3 4 5]
    fmt.Println(slice2) // [1 2 3]

    slice2[0] = 10
    fmt.Println(slice1) // [10 2 3 4 5]
    fmt.Println(slice2) // [10 2 3]
    fmt.Println(arr)    // [10 2 3 4 5 6 7 8 9 10]
}

slice 的 append

可以使用append对 slice 附加元素,这会返回一个 slice 的参考:

package main

import "fmt"

func main() {
    arr := [...]int{1, 2, 3, 4, 5}
    slice1 := arr[:2]
    fmt.Println(slice1)      // [1 2]
    fmt.Println(len(slice1)) // 2
    fmt.Println(cap(slice1)) // 5

    slice2 := append(slice1, 6)
    fmt.Println(slice2)      // [1 2 6]
    fmt.Println(len(slice2)) // 3
    fmt.Println(cap(slice2)) // 5

    slice2[0] = 10
    fmt.Println(slice1) // [10 2]
    fmt.Println(slice2) // [10 2 6]
    fmt.Println(arr)    // [10 2 6 4 5]
}

只要附加的元素没有超出 slice 的容量,返回的 slice 参考就会是相同的,底层也是同一数组,因此,改变了slice2[0]的值,slice1arr获取结果都有了变化。

如果append的时候,附加元素超出了 slice 的容量,那么底层会创建一个新的数组,容量为原 slice 容量的两倍加 2,接着将旧数组内容复制到新数组,然后将指定的值附加上去,append的结果也会返回新的 slice 参考。例如:

package main

import "fmt"

func main() {
    arr := [...]int{1, 2, 3, 4, 5}
    slice1 := arr[:]
    fmt.Println(slice1)      // [1 2 3 4 5]
    fmt.Println(len(slice1)) // 5
    fmt.Println(cap(slice1)) // 5

    slice2 := append(slice1, 6)
    fmt.Println(slice2)      // [1 2 3 4 5 6]
    fmt.Println(len(slice2)) // 6
    fmt.Println(cap(slice2)) // 12

    slice2[0] = 10
    fmt.Println(slice1) // [1 2 3 4 5]
    fmt.Println(slice2) // [10 2 3 4 5 6]
    fmt.Println(arr)    // [1 2 3 4 5]
}

在上面的例子中,由于slice2底层的数组,与slice1无关了,因此,透过slice2[0]修改了值,并不会影响到透过slice1arr获取的值。

如果想用append来直接附加另一个 slice,可以使用...,将另一个 slice 扩展为一列实参,例如:

package main

import "fmt"

func main() {
    slice1 := []int{1, 2, 3}
    slice2 := []int{4, 5, 6}
    fmt.Println(append(slice1, slice2...))  // [1 2 3 4 5 6]
}

slice 的 copy

可以使用copy函数,将一个 slice 的内容,复制至另一个 slice:

package main

import "fmt"

func main() {
    src := []int{1, 2, 3, 4, 5}
    dest := make([]int, len(src), (cap(src)+1)*2)
    fmt.Println(copy(dest, src)) // 5
    fmt.Println(src)             // [1 2 3 4 5]
    fmt.Println(dest)            // [1 2 3 4 5]

    src[0] = 10
    fmt.Println(src)  // [10 2 3 4 5]
    fmt.Println(dest) // [1 2 3 4 5]
}

复制时,目的 slice 的容量必须足够,否则会发生 cap out of range 的错误,copy函数若执行成功,会返回复制的元素个数。

先前提到,可以从 slice 中产生 slice,然而,由于从 slice 中产生 slice,底层仍会是同一个数组,因此,要小心一些应用场合,对于一个很大的数组,若不断地切出新的 slice,底层参考的数组还是那么大,想避免这类问题,应自行使用make创建适当大小的 slice,然后从旧 slice 使用copy复制元素值,或者使用append,将旧 slice 的内容附加至新 slice,以避免这类问题。


展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情