5.3. 切片

切片类型的基本声明如下,T 为元素类型:

[]T

切片的第一印象与数组很相似:它们都是特定类型对象的序列,但它们的实际行为存在巨大区别,切片是数组的部分引用,它时常取自于数组,例如:

    arr := [...]i32{1, 2, 3, 4}
    sl: []i32 = arr[0:2]
    println(len(sl))      // 2
    println(sl[0], sl[1]) // 1 2

表达式 arr[m:n] 返回一个切片,切片始于数组 arr 的第 m 个元素,切片长度为 n-m,与字符串的类似语法相似,若省略 m,则表示始于数组首个元素;若省略 n,则终于数组最后一个元素。mn 不可超过数组实际范围,否则会触发异常。

切片中并不保存实际数据,通过 [] 访问到的对象位于它所引用的数组中,这意味着更改数组中的对象可能影响到切片,反之亦然,例如:

    arr := [...]i32{1, 2, 3, 4}
    sl := arr[0:2]
    println(sl[0]) // 1
    arr[0] = 13
    println(sl[0]) // 13
    sl[1] = 42
    println(arr[1]) // 42

内置函数 cap 可用于获取切片的可用容量——既切片所引用的数组的长度减去切片开始位置,例如:

    arr := [...]i32{11, 12, 13, 14}
    sl1 := arr[1:2]
    println(len(sl1), cap(sl1)) // 1 3

由定义可知,切片的容量恒大等于其长度。

一个数组可以被多个切片引用,如果引用的部分之间存在重叠,那么重叠部分的更改也会互相影响,例如:

    arr := [...]i32{1, 2, 3, 4}
    sl1 := arr[0:2]
    sl2 := arr[1:3]
    println(sl2[0]) // 2
    sl1[1] = 42
    println(sl2[0]) // 42

实际上,对切片使用[m:n]操作符也可以获得一个新的切片,新切片始于源切片的第 m 个元素,其余规则与从数组中获取一个切片类似。

获取切片的方法除了引用数组或已有切片外,还可以通过内置函数 make 直接创建,形式签名为:

make([]T, Len: int, Cap: int) => []T
make([]T, Len: int) => []T // 等价于 make([]T, Len, Len)

返回值是一个类型为 []T、长度为 Len、容量为 Cap 的切片,其中 Cap 可以省略,此时切片的容量为 Len,例如:

    sl1 := make([]i32, 3, 5)
    println(sl1[0], len(sl1), cap(sl1)) // 0 3 5

使用 make 函数创建切片时,隐式的创建了一个长度为 Cap 的数组,并将其引用为切片。

另一个与切片密切相关的内建函数是 append,它用于向切片中追加元素,形式签名为:

append(sl []T, e T) => []T

该函数将元素 e 追加至切片 s 的尾部,并返回一个新的切片。由于凹语言的函数调用使用值传递,追加行为不会影响源切片 s,因此实际常用的写法如下例:

    sl: []i32
    //...
    sl = append(sl, 42)

既将 append 返回的新切片赋值给源切片。append 不仅向切片中追加元素,还可以追加另一个切片,例如:

    sl1 := []i32{13, 42}
    sl2 := []i32{9527, 1024}
    sl1 = append(sl1, sl2...)
    println(sl1[0], sl1[1], sl1[2], sl1[3]) // 13 42 9527 1024

当被追加对象是切片时,应在变量名后添加 ...

由于切片底层引用了一个长度固定的数组,如果使用 append 追加元素后,切片的长度未超过数组可用容量,那么数组对应元素的内容将被替换为追加元素,例如:

    arr := [...]i32{1, 2, 3}
    sl1 := arr[0:2]
    sl1 = append(sl1, 5)
    println(arr[2]) // 5    

倘若新切片的长度超过原始数组容量,那么 append 函数会自动重新申请一个足够大的数组,将源切片的元素拷贝至新数组(既自动执行了一次深拷贝),然后再执行追加,这种情况我们称为切片扩容,例如:

    arr := [...]i32{1, 2, 3}
    sl1 := arr[:]
    sl2 := append(sl1, 4)
    sl2[0] = 42
    println(sl1[0]) // 1

显然,如果发生了切片扩容,新切片与源切片的相互引用关系就切断了。

与前面章节介绍过的数据类型不同,切片类型的变量不可比较,因为切片不是纯值类型,而是与底层数组甚至其他切片存在引用关系,这种关联使得切片在运行时无法保证其中的元素值不变。

切片只能与常量 nil 进行比较,用于判断切片是否为 0 值,例如:

    sl: []i32
    println(sl == nil, sl != nil) // true false

实际上,如果需要判断某个切片是否为空,不应将其与 nil 比较,而应判断其长度是否为 0,因为存在长度为 0,但不为 nil 的切片,例如:

    arr := [...]i32{1, 2, 3}
    sl1 := arr[0:0]
    println(sl1 == nil, len(sl1), cap(sl1)) // false 0 3

除特别说明外,凹语言程序应以相同的方式处理长度为 0 的切片,与 nil 值的切片。内置函数 append 既符合该要求,例如下列程序是合法的:

    sl: []i32 // sl == nil
    sl := append(sl, 5)
    println(sl[0]) // 5

已知问题:

  • 访问切片元素时,未执行边界检查。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
  • 使用 [] 从数组或切片中获取新切片时,未执行边界检查。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。