036-Slice

slice 是 Go 里非常强大的复合结构,简单的说,它就是一个动态数组。

1. slice 声明与定义

不同于数组的声明,slice 声明是不需要指定长度的。比如下面这样:

var a [5]int // 这是数组
var b []int  // 这是 slice

这样一来,数组与 slice 的区别就非常明显了。

下面是常见的声明和定义方式。

  • 例 1
package main

import "fmt"

func main() {
    var a []int
    var b []int = []int{1, 2, 3}
    var c = []string{"Allen", "Luffy"}
    d := []float64{3.4, 5.6, 0.9}
    e := make([]int, 10)           // 使用 make 函数创建并返回一个大小为 10 的 slice
    var f [5]int = [5]int{4, 5, 3} // 好像混入了点什么?^_^

    fmt.Printf("a = %v, type = %[1]T, len = %d, cap = %d\n", a, len(a), cap(a))
    fmt.Printf("b = %v, type = %[1]T, len = %d, cap = %d\n", b, len(b), cap(b))
    fmt.Printf("c = %v, type = %[1]T, len = %d, cap = %d\n", c, len(c), cap(c))
    fmt.Printf("d = %v, type = %[1]T, len = %d, cap = %d\n", d, len(d), cap(d))
    fmt.Printf("e = %v, type = %[1]T, len = %d, cap = %d\n", e, len(e), cap(e))
    fmt.Printf("f = %v, type = %[1]T, len = %d, cap = %d\n", f, len(f), cap(f))
}

输出为:

a = [], type = []int, len = 0, cap = 0
b = [1 2 3], type = []int, len = 3, cap = 3
c = [Allen Luffy], type = []string, len = 2, cap = 2
d = [3.4 5.6 0.9], type = []float64, len = 3, cap = 3
e = [0 0 0 0 0 0 0 0 0 0], type = []int, len = 10, cap = 10
f = [4 5 3 0 0], type = [5]int, len = 5, cap = 5

上面的例子除了 f 是一个 [5]int 数组外,其它的都是 slice. 函数 len 我们之前也见过了,它可以用来计算 string 的长度,在这里,也可以用来计算 array 和 slice 的长度。而 cap 函数,是我们第一次遇见,它表示数组或 slice 底层真正的容量大小。

2. 如何给 slice 追加元素?

先来看下面的例子。

  • 例 2
package main

import "fmt"

func main() {
    var a []int = []int{1, 2, 3}
    fmt.Printf("a = %v\n", a)
    a[3] = 1
    fmt.Printf("a = %v\n", a)
}

这段代码可以编译通过,但是运行时出错了:


这里写图片描述
图1 运行时异常

可以看到,第一个 Printf 正常输出了数组 a,执行到 a[3] = 1 时程序运行时异常了。原因是索引越界了。

不是说 slice 是动态数组吗,为什么添加个元素就不行了呢?在第 1 小节我们其实已经用过了 cap 函数了,它是用来计算 slice 底层真正的容量的。在这里 a 的 cap 容量刚好是 3,因此使用 a[3] = 1 一定是不能成功的。

Go 提供了一个内建函数 append 可以向 slice 进行追加元素,如果 slice 容量不够,它会自动的扩容。我们再修改一个版本,如例 3.

  • 例 3
package main

import "fmt"

func main() {
    var a []int = []int{1, 2, 3}
    fmt.Printf("a = %v\n", a)
    _ = append(a, 1) // append 会返回一个新的 slice,暂时忽略
    fmt.Printf("a = %v\n", a)
}


这里写图片描述
图2 追加元素失败?

从输出结果上看,好像并不正确?注意例 3 中的注释『append 会返回一个新的 slice』,这样吧,我们把返回的新 slice 的结果也打印出来看看。见例 4.

  • 例 4
package main

import "fmt"

func main() {
    var a []int = []int{1, 2, 3}
    fmt.Printf("a = %v\n", a)
    b := append(a, 1)
    fmt.Printf("a = %v\n", a)
    fmt.Printf("b = %v\n", b)
}


这里写图片描述
图3 slice b 才是那个被 append 的那个数组

这样你应该就能明白了,append 并不是真的把 a 给扩容了,它只是申请了一个容量更大的新 slice,然后把 a 里的元素复制到了新 slice,再追加一个元素。

append 的策略伪代码大概如下:

append(a []T, e T):
    l = len(a)
    c = cap(a)
    if l + 1 < c:
        a[l+1] = e
        return a
    else:
        b = make([]T, 2*c) // 申请一个以前容量 2 倍的 slice
        copy(b, a)         // 把 a 中的元素复制到 b
        b[l + 1] = e
        return b

我们验证一下例 4 中的容量变化,见例 5.

  • 例 5
package main

import "fmt"

func main() {
    var a []int = []int{1, 2, 3}
    fmt.Printf("a = %v, len = %d, cap = %d\n", a, len(a), cap(a))
    b := append(a, 1)
    fmt.Printf("a = %v, len = %d, cap = %d\n", a, len(a), cap(a))
    fmt.Printf("b = %v, len = %d, cap = %d\n", b, len(b), cap(b))
}


这里写图片描述
图4 apend 返回的新 slice 容量翻倍

当然了,如果 a 的 cap 足够的话,append 是不会申请新的 slice 的。

3. 切片操作和slice 底层原理

这应该是大家最关心的事情了。实际上,slice 如果用 C 里的 struct 来描述的话,应该像这样:

template <typename T>
struct Slice {
    T* data; // 一个指向数据域的指针,指向数组某个元素的地址,不一定非得是首元素。
    int len; // 元素的个数
    int cap; // 容量大小
};

再来看一新的例子,见例 6.

  • 例 6
package main

import "fmt"

func main() {
    a := [4]int{0, 1, 2, 3} // a 是一个数组
    x := a[0:2]             // x 是一个 slice,通过对数组执行切片获得
    y := a[0:3]             // y 是一个 slice
    fmt.Printf("a = %v, len = %d, cap = %d\n", a, len(a), cap(a))
    fmt.Printf("x = %v, len = %d, cap = %d\n", x, len(x), cap(x))
    fmt.Printf("y = %v, len = %d, cap = %d\n", y, len(y), cap(y))
}

例 6 是另一种创建 slice 的方法,即通过一个已经存在的数组创建出来的。结合刚刚 Slice 结构的定义,x 和 y 的底层结构实际上像图 5 所示。


这里写图片描述
图5 slice 底层结构

x 和 y,它们指向了相同的底层数组。唯一的区别是,x 和 y 的 len 不一样。不难想象,如果修改了 a 或者 x 或者 y 中的任何一个元素,都会互相产生影响。

3.1 切片操作

上面说对数组执行切片操作,可以生成一个 slice。实际上,对 slice 也可以执行切片操作,它们的语法是相同的。对 slice 做切片,返回的仍然是 slice.

这里使用 gopl 中的例子做说明。见例 7.

  • 例 7
package main

import "fmt"

func main() {
    months := [...]string{
        1:  "January",
        2:  "February",
        3:  "March",
        4:  "April",
        5:  "May",
        6:  "June",
        7:  "July",
        8:  "August",
        9:  "September",
        10: "October",
        11: "November",
        12: "December", // 后面的逗号不能省略
    }

    fmt.Printf("months = %v, type = %[1]T, len = %d, cap = %d\n", months, len(months), cap(months))

    Q2 := months[4:7]
    summer := months[6:9]

    fmt.Printf("Q2 = %v, type = %[1]T, len = %d, cap = %d\n", Q2, len(Q2), cap(Q2))
    fmt.Printf("summer = %v, type = %[1]T, len = %d, cap = %d\n", summer, len(summer), cap(summer))
}


这里写图片描述
图6 运行结果

来看一下 Q2 和 summer 这两个 slice 的底层结构:


这里写图片描述
图7 Q2 和 summer 底层结构

4. 空 slice

空 slice 有两种情况,第一种 slice 变量的值为 nil,第二种,变量的值不为 nil,但是长度为空。

  • 例 8
package main

import "fmt"

func main() {
    var s1 []int
    var s2 []int = nil
    var s3 = []int(nil)
    var s4 = []int{}

    fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s1, s1 == nil, len(s1), cap(s1))  // []int:[], true, len = 0, cap = 0
    fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s2, s2 == nil, len(s2), cap(s2))  // []int:[], true, len = 0, cap = 0
    fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s3, s3 == nil, len(s3), cap(s3))  // []int:[], true, len = 0, cap = 0
    fmt.Printf("%T:%[1]v, %t, len = %d, cap = %d\n", s4, s4 == nil, len(s4), cap(s4))  // []int:[], false, len = 0, cap = 0
}
  • s1, s2, s3 虽然都是 nil,但是仍然可以使用 len 和 cap 对其进行求值。
  • 可以看到,s4 虽然不等于 nil,但是 len 和 cap 的结果也是 0.

因此,要判断一个 slice 是否为空的最好方法是使用 len 计算其长度,而不是判断其是否为 nil。

5. 总结

  • 掌握 slice 声明和定义
  • 掌握创建 slice 的几种方法(make,切片)
  • 掌握数组与 slice 之间的区别
  • 掌握 slice 底层原理
  • 掌握判断 slice 为空的方法
  • 掌握 append 函数底层原理

猜你喜欢

转载自blog.csdn.net/q1007729991/article/details/79904331