为什么 Go 禁止获取 (&) 映射成员的地址,但允许 (&) 切片元素?
Why does Go forbid taking the address of (&) map member, yet allows (&) slice element?
Go 不允许获取地图成员的地址:
// if I do this:
p := &mm["abc"]
// Syntax Error - cannot take the address of mm["abc"]
基本原理是,如果 Go 允许获取此地址,当映射后备存储增长或缩小时,该地址可能会变得无效,从而使用户感到困惑。
但是当 Go 切片超出其容量时,它会重新定位,但是,Go 允许我们获取切片元素的地址:
a := make([]Test, 5)
a[0] = Test{1, "dsfds"}
a[1] = Test{2, "sdfd"}
a[2] = Test{3, "dsf"}
addr1 := reflect.ValueOf(&a[2]).Pointer()
fmt.Println("Address of a[2]: ", addr1)
a = append(a, Test{4, "ssdf"})
addrx := reflect.ValueOf(&a[2]).Pointer()
fmt.Println("Address of a[2] After Append:", addrx)
// Note after append, the first address is invalid
Address of a[2]: 833358258224
Address of a[2] After Append: 833358266416
为什么 Go 是这样设计的?取slice元素地址有什么特别之处?
切片和映射之间存在一个主要区别:切片由支持数组支持,而映射则不然。
如果地图增长或缩小,指向地图元素的潜在指针可能会变成指向无处(未初始化的内存)的悬空指针。这里的问题不是 "confusion of the user",而是它会破坏 Go 的一个主要设计元素:没有悬挂指针。
如果一个切片用完了容量,一个新的、更大的后备数组被创建,旧的后备数组被复制到新的;并且旧的后备数组 仍然存在 存在 。因此,从指向旧后备数组的 "ungrown" 切片获得的任何指针仍然是指向有效内存的有效指针。
如果您有一个切片仍然指向旧的后备数组(例如,因为您在将切片增长到超出其容量之前制作了该切片的副本),您仍然可以访问旧的后备数组。这与切片元素的指针关系不大,但切片是数组的视图,并且数组在切片增长期间被复制。
注意切片收缩时没有"reducing the backing array of a slice"
map 和 slice 之间的根本区别在于,map 是一种动态数据结构,随着它的增长会移动它包含的值。 Go map 的具体实现甚至可能增量增长,在插入和删除操作期间一点点增长,直到所有值都移动到更大的内存结构。所以你可以删除一个值,突然另一个值可能会移动。另一方面,切片只是子数组的 interface/pointer。切片永远不会增长。 append 函数可以将一个切片复制到另一个容量更大的切片中,但它会完整地保留旧切片并且它也是一个函数,而不仅仅是一个索引运算符。
用地图实现者自己的话来说:
https://www.youtube.com/watch?v=Tl7mi9QmLns&feature=youtu.be&t=21m45s
"It interferes with this growing procedure, so if I take the address
of some entry in the bucket, and then I keep that entry around for a
long time and in the meantime the map grows, then all of a sudden that
pointer points to an old bucket and not a new bucket and that pointer
is now invalid, so it's hard to provide the ability to take the
address of a value in a map, without constraining how grow works...
C++ grows in a different way, so you can take the address of a bucket"
因此,即使 &m[x] 可能被允许并且对 short-lived 操作很有用(对值进行修改,然后不再使用该指针),实际上映射在内部这样做,我认为 designers/implementors 语言选择使用 map 是为了安全,不允许 &m[x] 以避免程序中的细微错误,这些错误可能会在没有意识到的情况下长时间保留指针指向与程序员认为不同的数据。
另请参阅 以获取相关评论。
Go 不允许获取地图成员的地址:
// if I do this:
p := &mm["abc"]
// Syntax Error - cannot take the address of mm["abc"]
基本原理是,如果 Go 允许获取此地址,当映射后备存储增长或缩小时,该地址可能会变得无效,从而使用户感到困惑。
但是当 Go 切片超出其容量时,它会重新定位,但是,Go 允许我们获取切片元素的地址:
a := make([]Test, 5)
a[0] = Test{1, "dsfds"}
a[1] = Test{2, "sdfd"}
a[2] = Test{3, "dsf"}
addr1 := reflect.ValueOf(&a[2]).Pointer()
fmt.Println("Address of a[2]: ", addr1)
a = append(a, Test{4, "ssdf"})
addrx := reflect.ValueOf(&a[2]).Pointer()
fmt.Println("Address of a[2] After Append:", addrx)
// Note after append, the first address is invalid
Address of a[2]: 833358258224
Address of a[2] After Append: 833358266416
为什么 Go 是这样设计的?取slice元素地址有什么特别之处?
切片和映射之间存在一个主要区别:切片由支持数组支持,而映射则不然。
如果地图增长或缩小,指向地图元素的潜在指针可能会变成指向无处(未初始化的内存)的悬空指针。这里的问题不是 "confusion of the user",而是它会破坏 Go 的一个主要设计元素:没有悬挂指针。
如果一个切片用完了容量,一个新的、更大的后备数组被创建,旧的后备数组被复制到新的;并且旧的后备数组 仍然存在 存在 。因此,从指向旧后备数组的 "ungrown" 切片获得的任何指针仍然是指向有效内存的有效指针。
如果您有一个切片仍然指向旧的后备数组(例如,因为您在将切片增长到超出其容量之前制作了该切片的副本),您仍然可以访问旧的后备数组。这与切片元素的指针关系不大,但切片是数组的视图,并且数组在切片增长期间被复制。
注意切片收缩时没有"reducing the backing array of a slice"
map 和 slice 之间的根本区别在于,map 是一种动态数据结构,随着它的增长会移动它包含的值。 Go map 的具体实现甚至可能增量增长,在插入和删除操作期间一点点增长,直到所有值都移动到更大的内存结构。所以你可以删除一个值,突然另一个值可能会移动。另一方面,切片只是子数组的 interface/pointer。切片永远不会增长。 append 函数可以将一个切片复制到另一个容量更大的切片中,但它会完整地保留旧切片并且它也是一个函数,而不仅仅是一个索引运算符。
用地图实现者自己的话来说:
https://www.youtube.com/watch?v=Tl7mi9QmLns&feature=youtu.be&t=21m45s "It interferes with this growing procedure, so if I take the address of some entry in the bucket, and then I keep that entry around for a long time and in the meantime the map grows, then all of a sudden that pointer points to an old bucket and not a new bucket and that pointer is now invalid, so it's hard to provide the ability to take the address of a value in a map, without constraining how grow works... C++ grows in a different way, so you can take the address of a bucket"
因此,即使 &m[x] 可能被允许并且对 short-lived 操作很有用(对值进行修改,然后不再使用该指针),实际上映射在内部这样做,我认为 designers/implementors 语言选择使用 map 是为了安全,不允许 &m[x] 以避免程序中的细微错误,这些错误可能会在没有意识到的情况下长时间保留指针指向与程序员认为不同的数据。
另请参阅