Go语言中的nil切片、非nil切片和空切片

e37o9pze  于 2022-12-07  发布在  Go
关注(0)|答案(3)|浏览(210)

我是一个围棋编程的新手。我在围棋编程书中读到过,那片由三样东西组成:数组指针、长度和容量。
我渐渐糊涂了:

  • nil slice(slice没有指向的基础数组,len = 0,cap=0)
  • 只有len = 0、cap = 0的非零切片
  • 空切片。

请问有没有人能分辨出nil和空切片是否是相同的东西?如果它们都是不同的,那么请告诉这两者之间的区别是什么?如何测试一个切片是否是空的?还有,在长度和容量都为零的非nil切片中,指针持有什么值?

3vpjnl9f

3vpjnl9f1#

可观察到的行为

nil和空切片(容量为0)并不相同,但它们的可观察行为是相同的(几乎一直如此)。

  • 您可以将它们传递给内置的len()cap()函数
  • 你可以对它们进行for range(将是0次迭代)
  • 您可以对它们进行切片(不违反"规范:切片表达式;因此结果也将是空切片)
  • 由于其长度为0,因此不能更改其内容(追加值将创建新的切片值)

请参阅以下简单示例(一个nil切片和两个非nil空切片):

var s1 []int         // nil slice
s2 := []int{}        // non-nil, empty slice
s3 := make([]int, 0) // non-nil, empty slice

fmt.Println("s1", len(s1), cap(s1), s1 == nil, s1[:], s1[:] == nil)
fmt.Println("s2", len(s2), cap(s2), s2 == nil, s2[:], s2[:] == nil)
fmt.Println("s3", len(s3), cap(s3), s3 == nil, s3[:], s3[:] == nil)

for range s1 {}
for range s2 {}
for range s3 {}

输出(在Go Playground上试用):

s1 0 0 true [] true
s2 0 0 false [] false
s3 0 0 false [] false

(Note对x1M6 N1 x切片进行切片产生x1M7 N1 x切片,对非x1M8 N1 x切片进行切片产生非x1M9 N1 x切片。
除了一个例外,您只能通过比较切片值和预先声明的标识符nil来区分它们,它们在其他方面的行为都是相同的。但是请注意,许多包确实将切片与nil进行比较,并可能基于此而采取不同的行为(例如encoding/jsonfmt包)。
唯一的区别是将切片转换为数组指针(在Go 1.17中添加到语言中)。将非nil切片转换为数组指针将导致非nil指针,将nil切片转换为数组指针将导致nil指针。
要判断切片是否为空,只需将其长度与0进行比较:无论它是nil片还是非nil片,也无论它是否具有正容量;如果没有元素,则为空。

s := make([]int, 0, 100)
fmt.Println("Empty:", len(s) == 0, ", but capacity:", cap(s))

打印效果(在Go Playground上试用):

Empty: true , but capacity: 100

引擎盖下
切片值由reflect.SliceHeader中定义的结构表示:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

nil切片的情况下,该结构体将具有其零值,即其所有字段都将是其零值,即:0 .
如果一个非nil切片的容量和长度都等于0,那么LenCap字段肯定是0,但是Data指针可能不是。它 * 不会 * 是,这就是它与nil切片的区别。它将指向一个大小为零的底层数组。
注意Go规范允许大小为0的不同类型的值具有相同的内存地址。系统注意事项:尺寸和对齐保证:
如果结构或数组类型不包含大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量在内存中可能具有相同的地址。
让我们检查一下,为此我们调用unsafe包的帮助,并“获取”切片值的reflect.SliceHeader结构体“view”:

var s1 []int
s2 := []int{}
s3 := make([]int, 0)

fmt.Printf("s1 (addr: %p): %+8v\n",
    &s1, *(*reflect.SliceHeader)(unsafe.Pointer(&s1)))
fmt.Printf("s2 (addr: %p): %+8v\n",
    &s2, *(*reflect.SliceHeader)(unsafe.Pointer(&s2)))
fmt.Printf("s3 (addr: %p): %+8v\n",
    &s3, *(*reflect.SliceHeader)(unsafe.Pointer(&s3)))

输出(在Go Playground上试用):

s1 (addr: 0x1040a130): {Data:       0 Len:       0 Cap:       0}
s2 (addr: 0x1040a140): {Data: 1535812 Len:       0 Cap:       0}
s3 (addr: 0x1040a150): {Data: 1535812 Len:       0 Cap:       0}

我们看到了什么?

  • 所有片(片标题)都有不同的内存地址
  • nil片具有0数据指针
  • s2s3片具有相同的数据指针,共享/指向相同的大小为0的内存值
2wnc66cl

2wnc66cl2#

从字面意义上讲,slice可以是nil:

var n []int
n == nil // true

这是唯一容易的情况。“空”切片的概念没有很好地定义:一个包含len(s) == 0的切片s肯定是空的,不管它的容量有多大。忽略底层的实现,我们永远不需要知道切片在内部是如何表示的,重要的是切片的定义行为。
如何测试切片是否为空?
“slice s is empty”最合理的定义是一个不包含元素的切片,它可以转换为len(s) == 0。这个定义适用于nil以及非nil切片。
有人能告诉我们nil和空切片是不是同一个东西吗?如果两者不同,那么请告诉我们两者之间的区别是什么?
从技术上讲,nil切片和非nil切片是不同的(一个是== nil,另一个是!= nil),但这种区别通常并不重要,因为您可以将append转换为nil切片,nil切片上的len和cap返回0

var n []int
len(n) == cap(n) == 0 // true
n = append(n, 123)
len(n) == 1 // true

请阅读Go语言中有关零值的内容,了解更多信息。nil切片就像nil通道或nil贴图:它是未初始化的。你可以通过make ing或者一个字面量来初始化它们。如上所述,没有理由去考虑底层的表示。
另外,在长度和容量都为零的非nil切片中,指针的值是什么?
这是一个实现细节,可能会因编译器的不同,甚至版本的不同而不同。没有人需要知道这一点来编写正确的和可移植的Go语言程序。

zqdjd7g9

zqdjd7g93#

var s1 []int         // nil slice
s2 := []int{}        // non-nil, empty slice
s3 := make([]int, 0) // non-nil, empty slice

警告,如果处理JSON,nil切片将编码为null而不是[],这可能会破坏一些(javascript)客户端,这些客户端试图在不可迭代的null上迭代(没有null检查)。

相关问题