我理解 Go 语言中数组和切片的区别。但是我不理解为什么拥有数组是有帮助的。为什么数组类型定义需要指定长度和元素类型?为什么我们不能把每个使用的“数组”都当做切片来使用呢?
数组arrays不仅仅是固定长度,它们还可以进行比较,并且它们是值类型(而不是引用或指针类型)。
在某些情况下,数组比切片具有无数优势,所有这些优势加起来足以证明数组的存在(以及切片)。让我们看看它们。(我甚至没有计算数组作为切片构建块的情况。)
1. 可比性意味着你可以将数组用作映射的键,但不能使用切片。是的,现在你可能会问为什么不让切片具有可比性,这样就不足以证明两者的存在了。切片上的相等性没有很好地定义。FAQ:为什么映射不允许使用切片作为键?
它们没有实现相等性,因为这些类型上的相等性没有很好地定义;涉及浅层比较与深度比较、指针比较与值比较、如何处理递归类型等多个考虑因素。
2. 数组还可以提供更高的编译时安全性,因为索引边界可以在编译时进行检查(数组长度必须计算为非负常量,该常量可由类型为int
的值表示):
s := make([]int, 3)
s[3] = 3 // "Only" a runtime panic: runtime error: index out of range
a := [3]int{}
a[3] = 3 // Compile-time error: invalid array index 3 (out of bounds for 3-element array)
3. 同样地,传递或分配数组值会隐式地复制整个数组,因此它将与原始值"分离"。如果你传递一个切片,它仍然会复制但只是切片的头部,而切片值(头部)将指向同一后备数组。这可能是你想要的,也可能不是。如果你想要从"原始"切片中"分离"出一个切片,你必须显式地复制内容,例如使用内置的 copy()
函数到一个新的切片。
a := [2]int{1, 2}
b := a
b[0] = 10 // This only affects b, a will remain {1, 2}
sa := []int{1, 2}
sb := sa
sb[0] = 10 // Affects both sb and sa
4. 由于数组长度是数组类型的一部分,长度不同的数组是不同的类型。这可能会带来一些麻烦(例如,你编写了一个函数,它需要一个类型为[4]int
的参数,你不能使用该函数来处理类型为[5]int
的数组),但这也可能是一个优点:可以用它来显式地指定期望的数组长度。例如,你想编写一个函数,它需要一个IPv4地址作为参数,它可以使用类型[4]byte
来建模。现在,你在编译时就可以保证传递给函数的值恰好有4个字节,不多不少(否则就是无效的IPv4地址)。
5. 与之前的相关,数组长度也可以用于文档目的。类型[4]byte
适当记录了IPv4有4个字节。类型为[3]byte
的rgb
变量告诉我们每个颜色分量有1个字节。在某些情况下,它甚至被取出并单独记录;例如在crypto/md5
包中: md5.Sum()
返回类型为[Size]byte
的值,其中md5.Size
是一个常量,为16
:MD5校验和的长度。
6. 当planning memory layout of struct types
时,它们也非常有用,请参见JimB的答案,以及this answer in greater detail and real-life example。
这样的例子有:
将指向数组的指针 p
切片: p[low:high]
是 (*p)[low:high]
的简写。如果 p
是指向切片的指针,则会出现编译时错误 (spec: Slice expressions)。
对指向数组的指针 p
进行索引: p[i]
是 (*p)[i]
的简写。如果 p
是指向切片的指针,则会出现编译时错误 (spec: Index expressions)。
示例:
pa := &[2]int{1, 2}
fmt.Println(pa[1:1]) // OK
fmt.Println(pa[1]) // OK
ps := &[]int{3, 4}
println(ps[1:1]) // Error: cannot slice ps (type *[]int)
println(ps[1]) // Error: invalid operation: ps[1] (type *[]int does not support indexing)
8. 访问(单个)数组元素比访问切片元素更有效率,因为在切片的情况下,运行时必须经过隐式指针解引用。此外,“如果s的类型是数组或指向数组的指针,则表达式len(s)和cap(s)是常量”。
可能会让人惊讶,但您甚至可以编写:
type IP [4]byte
const x = len(IP{}) // x will be 4
IP{}
不是常量表达式,所以例如const i = IP{}
将导致编译时错误!在此之后,甚至以下内容也能正常工作:const x2 = len((*IP)(nil)) // x2 will also be 4
请查看相关问题,了解什么情况下使用数组比切片更合适:
这只是出于好奇: 一个切片可以包含自己,而数组则不能。(实际上,这个属性使得比较更容易,因为你不必处理递归数据结构)
必读博客:
6
和8
之外,这些都不是“基本”优势(即不特定于Go中所做的糟糕设计选择)。我相信Go的设计者只是想留下更精确的内存管理可能性,因此我们有了数组。 - gavv数组是值,而且通常使用值而不是指针更加有用。
值可以进行比较,因此您可以使用数组作为映射的键。
值始终被初始化,因此您不需要像对待切片一样进行初始化或者make
操作。
数组让您更好地控制内存布局,在结构体中无法直接分配切片空间,但可以使用数组:
type Foo struct {
buf [64]byte
}
Foo
值将包含64字节的值,而不是需要单独初始化的切片标头。在与C代码进行交互时,数组还用于填充结构以匹配对齐,并防止错误共享以提高缓存性能。数组在节省空间方面更加高效。如果您从未更新切片的大小(即以预定义的大小开始,并且永远不会超过它),则实际上没有太大的性能差异。但是,切片存在额外的空间开销,因为切片只是一个包含其核心数组的包装器。在上下文中,这也提高了清晰度,因为它使变量的预期用途更加明显。