17 复合数据类型之数组

复合数据类型之数组Array

一 数组介绍

数组是一组同一类数据的集合,数组包含的元素个数被称为数组的长度,可用len(数组)来统计。

特点如下

  • 1、数组内的元素可以被改变,但是数组的长度不可被改变。
  • 2、数组是采用索引对应元素,索引从0开始,到len(数组)-1结束,不支持负向索引
  • 3、数组是值类型、传递数组的开销比较大

数组的特点决定了其使用的局限性,因此在Go语言中很少直接使用数组。

和数组对应的类型是Slice(切片),它是可以增长和收缩动态序列,slice的使用更为灵活,并且切片是引用类型、传递开销小,因此切片要比数组更为常用一些,但是要理解slice工作原理的话需要先理解数组。

二 声明与初始化

2.1 数组的声明语法

var 数组变量名 [元素数量]T  

// 例如定义一个长度为3元素类型为int的数组a
var a [3]int

2.2 数组的初始化

  • 1、零值初始化:数组是值类,若声明时未初始化值,则依据其包含的元素类型填充对应零值。

    var a [3]int   // 元素的类型为int,零值是0
    fmt.Println(a) // [0 0 0]
  • 2、指定一组值来初始化数组:值个数等于数组长度

    var cityArray = [3]string{"北京", "上海", "深圳"}  
    fmt.Println(cityArray) // [北京 上海 深圳]
  • 3、指定一组值来初始化数组:值个数小于数组长度,缺失的则自动填充对应零值

    var numArray [3]int = [3]int{111, 222}  // 长度为3,初始化了前两个值,第三个值用零值填充
    fmt.Println(numArray)  // [111 2222 0]
  • 4、用“…”省略号代替长度,数组的长度会根据初始化值的个数推导出来

    var names = [...]string{"egon", "EGON"}
    fmt.Println(names) // [egon EGON]
    
    fmt.Println(len(names))  // 2
    fmt.Printf("%T\n",names) // [2]string
    
    // 小练习:指定索引
    type Currency int
    
    const (
      USD Currency = iota // 美元
      EUR                 // 欧元
      GBP                 // 英镑
      RMB                 // 人民币
    )
    
    symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
    
    fmt.Println(RMB, symbol[RMB]) // "3 ¥"
  • 5、我们还可以使用指定索引值的方式来初始化数组,缺失的则自动填充对应零值

    // 索引分配到了5,所以总共有6个元素,初始化了两个元素,缺失的元素则用零值初始化
    nums := [...]int{1: 111, 5: 222}
    fmt.Println(nums) // [0 111 0 0 0 222]
    fmt.Printf("%T\n", nums) // [6]int

注意:

1、因为数组的长度需要在编译阶段确定,所以在声明时数组时,数组的长度必须是常量或者一个常量表达式(常量表达式是指在编译期即可计算结果的表达式)

2、不能往数组里添加或删除值,数组一旦定义,长度不可改变。

3、数组属于复合类型,而复合类型所包含的元素个数(或称长度),还有位置,都是类型一部分,所以 [3]int[5]int是不同的类型。

var a [3]int
var b [5]int
a = b // 错误,因为此时a和b是不同的类型,

4、数组嵌套数组又称多维数组,多维数组只有第一层可以使用...来让编译器推导数组长度。例如:

//支持的写法
a := [...][2]string{
    {"林大牛", "林二牛"},
    {"李一蛋", "李二蛋"},
    {"王一炮", "王三炮"},
}

//不支持多维数组的内层使用...
b := [3][...]string{
    {"林大牛", "林二牛"},
    {"李一蛋", "李二蛋"},
    {"王一炮", "王三炮"},
}

三 数据类型转换

仅限制用同类型数组之间进行,即数组长度、元素类型都必须一样,如下

var x [3]int = [3]int{111,222,333}
var y [3]int = [3]int(x)  // x与y的长度、元素类型都一样

错误案例

// 例1
var x []int = []int{111,222,333}
var y [3]int = [3]int(x)  // 错误

// 例2
var y [3]int32 = [3]int32("hello")  // 错误

思考,为何下面的转换就可以
var y []int32 = []int32("hello")
fmt.Println(y)  // [104 101 108 108 111] 

四 基本操作

4.1 增删改查

数组长度不可变,所以无法不能增、删,只能改、查,索引必须是非负数

var cityArray = [3]string{"北京", "上海", "深圳"}

// 查询:
fmt.Println(cityArray[0])        // 北京
fmt.Println(cityArray[len(cityArray)-1]) // 深圳
fmt.Println(cityArray[-1]) // 错误,索引必须是非负的

// 修改
cityArray[1] = "魔都"
fmt.Println(cityArray)  // [北京 魔都 深圳]
cityArray[666] = "魔都"  // 索引不存在则报错

4.2 二元运算

(1)数组属于复合类型中的值类型,复合类型中的值类型支持进行相等性判断,但必备两个条件

​ I.待比较的两个数组本身,必须是相同的类型

​ II:待比较的两个数组中,对应的元素都是可进行相等性判断的

// 例1:
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // 编译通过,输出:"true false false"

// 例2:
a := [2]int{1, 2}
d := [3]int{1, 2}
fmt.Println(a == d) // 编译失败: cannot compare [2]int == [3]int

// 例3:如下m与n是同类型,但内部元素是切片,切片是不能比较的
var m [2][]int = [2][]int{
    {111, 222},
    {333, 444},
}

var n [2][]int = [2][]int{
    {111, 222},
    {333, 444},
}

fmt.Println(m) // [[111 222] [333 444]]
fmt.Println(n) // [[111 222] [333 444]]

fmt.Println(m == n) // 报错:[2][]int cannot be compared

(2)数组不可以比大小,若想比较,可以编写函数依次取出元素进行比较,就像字符串比大小的底层原理一样

4.3 遍历数组元素

遍历一维数组

cityArray := [...]string{"北京", "上海", "深圳"}

// 遍历数组元素的方法1:基于索引
for i := 0; i < len(cityArray); i++ {
    fmt.Println(cityArray[i])
}

// 遍历数组元素的方法2:基于range
for index, value := range cityArray {
    fmt.Println(index, value)
}

遍历多维数组

// 遍历二维数组:如下3行2列
a := [3][2]string{
    {"林大牛", "林二牛"},
    {"李一蛋", "李二蛋"},
    {"王一炮", "王三炮"},
}

for _,v1:=range  a{
    for _,v2:=range v1{
        fmt.Println(v2)
    }
}

​```
输出:
林大牛  林二牛  
李一蛋  李二蛋  
王一炮  王三炮  
​```

五 在函数参数中传递

在其他其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数,但在go语言中数组就是值传递,详细地说:关于数组的赋值和传参会复制整个数组,操作的都是副本的值,不会改变本身的值。

举例如下:

package main

import "fmt"

func modifyArray(x [3]int) {
    fmt.Printf("拷贝得到一个新数组,地址是:%p\n",&x)
    x[0] = 100
}

func main() {
    c := [3]int{10, 20, 30}
    fmt.Printf("原数组,地址是:%p\n",&c)  // 原数组,地址是:0xc000018140
    modifyArray(c) // 在modify中修改的是c的副本x,打印的新数组地址:0xc000018180
    fmt.Println(c) // 仍然为[10 20 30]
}

小练习

package main

import "fmt"

func modifyArray2(x [3][2]int) {
    fmt.Printf("拷贝得到一个新数组,地址是:%p\n", &x)
    x[2][0] = 100
}
func main() {
    // 练习1
    var a = 10                                   //定义变量a
    b := a                                       //将a的值赋值给b
    b = 101                                      //修改b的值,是否会影响a?
    fmt.Printf("a的内存地址是%p\n,b的内存地址是%p\n", a, &b) //a与b的内存地址是否一样?

    // 练习2
    d := [3][2]int{
        {1, 1},
        {1, 1},
        {1, 1},
    }
    fmt.Printf("原数组,地址是:%p\n", &d)
    modifyArray2(d) // 此处打印的地址与上一行代码打印的是否一致?
    fmt.Println(d)  //值为?
}

至此,我们可以总结一下数组因为是值类型带来的弊端

1、在函数参数中是值传递,导致传递大数组将是低效的

2、传递给函数后,任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。

当然,我们可以显式地传入一个数组指针来达到引用传递与修改原值的目的,如下

func zero(ptr *[32]byte) {
    *ptr = [32]byte{}  // 给[32]byte类型的数组清零
}

注意:
[n]*T:表示数组的元素由指针类型组成
*[n]T:表示数组指针

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息,例如:上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。

由于这些原因,数组很少用作函数参数,我们一般使用slice来替代数组。接下来就让我们来介绍一下切片Slice吧

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