切片

切片Slice

一 切片介绍

​ 切片(Slice)顾名思义就是“把东西拿过来切一刀,得到一片,哈哈哈,话糙理不糙,只是注意一下此处的切一刀并不是切走了哈”。

​ 我们可以对数组、切片、指向数组或切片的指针、字符串进行切片操作,得到的是一个切片Slice或子字符串。

​ 简单切片操作:s[i:j]

//0、其中s可以是数组、切片、指向数组或切片的指针、字符串
//1、其中0 ≤ i≤ j≤ cap(s),引用s的从第i个元素开始到第j-1个元素的子序列,
//2、子序列是一个新的切片或者子字符串,新的slice将只有j-i个元素,
//3、如果i位置的索引被省略的话将使用0代替,即s[:j]与s[0:j]等价,
//4、如果j位置的索引被省略的话将使用len(s)代替,即s[1:j]与s[:j]等价。
//5、所以s[:]代表引用所有元素

​ 完整切片操作:s[i:j:max]

​ 切片Slice类型其实是一个元素类型相同且长度可变的序列。切片和数组有着紧密的联系,

  • 1、与数组相同的是:

    1、切片中的每个元素也都是相同的类型
    事实上,一个切片底层就是指向一个数组的,即切片就是基于数组类型做的一层封装,所以说切片内元素类型相同也不足为奇
    
    2、都是基于用索引访问元素
  • 2、与数组不同的是

    1、数组是长度不可变的序列,而切片则是长度可变的序列,
    数组类型是:[固定长度]T
    而切片类型一般写作是:[]T,所以说,切片的语法和数组很像,只是没有固定长度而已。
    2、数组是一个值类型,而切片是一个引用类型

​ 切片由三个部分构成:指针、长度、容量

//1、指针指向的是切片中的第一个元素所对应的底层数组元素的地址。
要注意的是:切片的第一个元素并不一定就是数组的第一个元素

//2、长度就是切片中包含的元素的个数
要注意的是:长度不能超过切片的容量

//3、长度指的是切片当前容纳的元素的个数,而切片的长度是可以变化的,容量指的则是切片未来可以容纳的元素的个数,即未来可以长度可以达到的范围
要注意的是:
    1、随着往切片中append元素,若底层数组不够容纳,切片便会自动扩容(指向新的数组)
    2、在没有扩容的情况下:容量指的是从切片的开始位置到底层数组的结尾位置
ps:内置的len和cap函数分别返回slice的长度和容量

二 创建切片

2.1 对数组进行切片操作得到切片类型

// 1、先创建一个数组,我们的索引从1开始,第0个元素会被自动初始化为空字符串。
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"}

 // 2、对数组months进行切片操作,结束位置不能超过数组的长度
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2)     // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]

fmt.Printf("%T\n",Q2) // []string
fmt.Printf("%T\n",summer) // []string

fmt.Println(Q2[1]) // 访问的是切片中的第2个元素,结果为:May
fmt.Println(summer[1]) // 访问的是切片中的第2个元素,结果为:July

多个slice之间可以共享底层的数据,例如上述Q2和summer,并且引用的数组部分区间可能重叠,如下图所示

// 切片为引用类型,修改切片的元素,肯定会影响到底层数据及相关切片
summer[0]="JUNE"

fmt.Println(Q2) // [April May JUNE]
fmt.Println(months) // [ ...... JUNE ......]

2.2 对切片进行切片操作得到切片类型

//1、以上述切片类型summer为例
summer := months[6:9],它的长度为3,容量为7,我们对其继续进行切片操作,得到仍然是一个切片类型

// 2、对切片类型再执行切片操作
// 2.1 结束位置可以超过原切片的长度3,
fmt.Println(summer[:7]) // 得到了一个更长的切片,元素为:[June July August September October November December]
fmt.Printf("%T",summer[:7]) // 类型为切片类型:[]string

// 2.2 但是结束位置绝对不能超过原切片的容量7,否则将会导致一个panic异常
fmt.Println(summer[:8]) // 异常panic: out of range

2.3 对指向数组或切片的指针进行切片操作来得到切片类型

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"}
x:=&months // x是指向数组的指针

summer := months[6:9]
y:=&summer // y是指向切片的指针,切片本身就是引用类型,取切片的指针可以用来不依赖变量summer操作原始切片,但其实本身意义并不大,了解即可

fmt.Println((*x)[0:3]) // [ January February]
fmt.Println((*y)[0:3]) // [June July August]

2.4 对字符串进行切片操作得到子字符串

字符串的切片操作和[]byte字节类型切片的切片操作是类似的。它们都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此切片操作对应常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。

msg := "hi吃鸡吗"

// 对字符串切片得到子字符串,结束位置不能超过字符串的长度
res:=msg[0:5] // 同样是以字节单位
fmt.Println(res) // hi吃
fmt.Printf("%T\n",res) // string

// 把字符串底层的byte存入切片:[]byte
x:=[]byte(msg) // [104 105 229 144 131 233 184 161 229 144 151]

x[8]='\xe5' // 替换为中文字符"吧"的第1个utf-8码点
x[9]='\x90' // 替换为中文字符"吧"的第2个utf-8码点
x[10]='\xa7' // 替换为中文字符"吧"的第3个utf-8码点
fmt.Println(x) // [104 105 229 144 131 233 184 161 229 144 167]
fmt.Println(string(x)) // hi吃鸡吧

// 把字符串底层的byte转成unicode码点存入切片:[]rune
y:=[]rune(msg) // [104 105 20320 22909 101 103 111 110]

y[4]='吧'
y[3]='\u5427'
fmt.Println(y)         // [104 105 21507 21543 21543]
fmt.Printf("%q\n", q)  // ['h' 'i' '吃' '吧' '吧']
fmt.Println(string(y)) // hi吃吧吧

var runes []rune
for _, r := range "Hello, 世界" {
    runes = append(runes, r)
}
fmt.Println(runes)         // [72 101 108 108 111 44 32 19990 30028]
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"

2.5 make创建切片类型,指向一个隐式的底层数组

​ 我们可以直接声明一个切片类型,但是它的零值为nil,一个零值的切片等于nil,nil代表空、没有分配内存,所以说一个nil值的切片并没有底层数组

var s []int
fmt.Println(s == nil) // true

​ 为了防止运行时申请内存带来的效率问题,我们可以用make函数为切片事先申请好了内存空间,内置的make函数创建一个指定元素类型长度容量的slice。容量部分可以省略,在这种情况下,容量将等于长度

make([]T, len)
make([]T, len, cap) // 等同于 make([]T, cap)[:len]

​ 在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量

在第一种语句中,创建了一个元素个数为len个的底层数组,然后得到的切片类型引用了整个数组,切片的长度等于切片的容量,这是切片操作能达到的最大长度了

在第二个语句中,创建了一个元素个数为cap个的底层数组,然后只引用了底层数组的前len个元素,若len<cap,那额外的元素就是留给未来的增长用的。

s:=make([]T, 0, 4) 
fmt.Println(s == nil) // false

2.6 声明并初始化切片,指向一个隐式的底层数组

​ slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素。

s1 := [5]int{1, 2, 3, 4, 5}
fmt.Printf("%T\n",s1) // 数组类型[5]int

s2 := []int{1, 2, 3, 4, 5}
fmt.Printf("%T\n",s2) // 切片类型[]int

但是对于slice并没有指明序列的长度,这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。

​ 像数组字面值一样,slice的字面值可以按顺序指定初始化值序列如上所示

​ 也可以通过索引和元素值指定,或者的两种风格的混合语法初始化,如下所示

s1 := []string{"aaa", "bbb",3:"ddd",}
fmt.Println(s1) // [aaa bbb  ddd]
fmt.Println(s1[0]) // aaa
fmt.Println(s1[1]) // bbb
fmt.Println(s1[2]) // 空
fmt.Println(s1[3]) // ddd

三 切片不能直接进行相等性比较

切片与数组还有一点不同的就是,slice彼此之间不能比较,因为切片是引用类型,它所包含的真实值都来自于或者说间接引用于底层数组中的元素,而我们想比较的都是值而不是引用,比较引用也没有意义,因为底层数组的元素是可以随时被修改的,

当然,我们自己取出切片中的元素逐一进行比较是可以的,如下所示

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false // 两个切片类型的长度不一样直接返回false,代表不相等
    }
    for i := range x {
        if x[i] != y[i] {
            return false // 两个切片类型但凡有一组对应值不相等就返回false,代表不相等
        }
    }
    return true // 如果能够执行该行代码,返回true,则代表两个切片是相等的,即底层元素一样
}

因为我们可以声明一个切片类型,它的零值为nil,如下所示

//1、一个nil值的slice的长度和容量都是0,如下所示
var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil // 基于类型转换表达式[]int(nil)生一个值为nil的切片

//2、当然也有非nil值的切片的长度和容量也是0,例如[]int{}或make([]int, 3)[3:]
s = []int{}    // len(s) == 0, s != nil
s = make([]int, 3)[3:] // 同上

所以说事实上切片slice唯一合法的比较操作就是和nil比较,例如:

if summer == nil { /* ... */ }

综上:如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。

除了和nil相等比较外,一个nil值的slice的行为和其它任意0产长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。

四 传递切片修改关联数组

​ 因为数组是值类型,传入函数后,是一个新的副本,在函数内修改并不会影响原数组,若想修改,那么必须传递数组的引用,我们可以传递数组指针

联系管理员微信tutu19192010,注册账号

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术