1. 引言
在 Go 语言中,map
是一种内置的数据类型,它提供了一种高效的形式来存储和检索数据。map
是一种无序的键值对汇合,其中每个键与一个值相关联。应用 map 数据结构能够疾速地依据键找到对应的值,而无需遍历整个汇合。
在 Go 语言中,map
是一种内置的数据类型,能够通过以下形式申明和初始化:
m := make(map[keyType]valueType)
在应用 map
时,咱们通常会应用根本数据类型作为键。然而,当咱们须要将自定义的构造体作为键时,就须要思考构造体中是否蕴含援用类型的字段。援用类型是指存储了数据的地址的类型,如指针、切片、字典和通道等。在 Go
中,援用类型具备动静的个性,可能会被批改或指向新的数据。这就引发了一个问题:是否将蕴含援用类型的自定义构造体作为 map
的键呢?
2. map 的根本模型
理解是否将蕴含援用类型的自定义构造体作为 map
的键这个问题,咱们须要先理解下 map
的根本模型。在 Go 语言中,map
是应用哈希表、实现的。哈希表是一种以键 - 值对模式存储数据的数据结构,它通过应用哈希函数将键映射到哈希值。
哈希函数是用于将键映射到哈希值的算法。它承受键作为输出并生成一个固定长度的哈希值。Go 语言的 map
应用了外部的哈希函数来计算键的哈希值。
而不同的 key
通过哈希函数生成的哈希值可能是雷同的,此时便产生了哈希抵触。哈希抵触指的是不同的键通过哈希函数计算后失去雷同的哈希值。因为哈希函数的输入空间远远小于键的输出空间,哈希抵触是不可避免的。此时无奈判断该 key
是以后哈希表中本来便曾经存在的元素还是因为哈希抵触导致不同的键映射到同一个 bucket。此时便须要判断这两个 key
是否相等。
因而,在 map
中,作为 map
中的 key
,须要保障其反对比照操作的,可能比拟两个key
是否相等。
3. map 键的要求
从下面 map
根本的模型介绍中,咱们理解到,map
中的 Key 须要反对哈希函数的计算,同时键的类型必须反对比照操作。
在 map
中,计算 key
的哈希值,是由默认哈希函数实现的,对于 map
中的 key
并没有额定的要求。
在 map
中,判断两个键是否相等是通过调用键类型的相等运算符(==
或 !=
)来实现的,因而key
必须确保该类型反对 ==
操作。这个要求是由 map
的实现机制决定的。map
外部应用键的相等性来确定键的存储地位和检索值。如果键的类型不可比拟,就无奈进行相等性比拟,从而导致无奈精确地定位键和检索值。
在 Go 中,根本数据类型(如整数、浮点数、字符串)和一些内置类型都是可比拟的,因而它们能够间接用作 map
的键。然而,自定义的构造体作为键时,须要确保构造体的所有字段都是可比拟的类型。如果构造体蕴含援用类型的字段,那么该构造体就不能间接用作 map
的键,因为援用类型不具备简略的相等性比拟。
因而,如果 map
中的键为自定义类型,同时蕴含援用字段,此时将无奈作为 map
的键,会间接编译失败,代码示例如下:
type Person struct {
Name string
Age int
address []Address}
func main() {
// 这里会间接编译不通过
m := make(map[Person]int)
}
其次还有一个例外,那便是自定义构造体中蕴含指针类型的字段,此时其是反对 ==
操作的,然而其是应用指针地址来进行 hash
计算以及相等性比拟的,有可能咱们了解是同一个 key
, 事实上从map
来看并不是,此时非常容易导致谬误,示例如下:
type Person struct {
Name string
Age int
address *Address
}
func main(){m := make(map[Person]int)
p1 := Person{Name: "Alice", Age: 30, address: &Address{city: "beijing"}}
p2 := Person{Name: "Alice", Age: 30, address: &Address{city: "beijing"}}
m[p1] = 1
m[p2] = 2
// 输入 1
fmt.Println(m[p1])
// 输入 2
fmt.Println(m[p2])
}
这里咱们定义了一个 Person
构造体,蕴含一个指针类型的字段 address
。创立了两个对象p1
和p2
, 在咱们的了解中,其是同一个对象,事实上在 map
中为两个两个互不相干的对象,次要起因都是应用地址来进行 hash 计算以及相等性比拟的。
综上所述,如果自定义构造体中蕴含援用类型的字段 (指针为非凡的援用类型),此时将不能作为map
类型的key
。
4. 为什么不抽取 hashCode 和 equals 办法接口,由用户自行实现呢?
以后 go
中map
中哈希值的计算,其提供了默认的哈希函数,不须要由用户去实现;其次 key
的相等性比拟,是通过==
操作符来实现的,也不禁用户自定义比拟函数。那咱们就有一个疑难了,为什么不抽取 hashCode 和 equals 办法接口,由用户来实现呢?
4.1 简略性和性能角度
相等性比拟在 Go
语言中应用 ==
操作符来实现,而哈希函数是由运行时库提供的默认实现。这种设计抉择我了解可能基于以下几个起因:
- 简略性:对于默认哈希函数函数来说,其内置在语言中的,无需用户额定的实现和配置。这简化了 map 的应用。对于相等性比拟操作,
==
操作符进行比拟是一种直观且简略的形式。在语法上,==
操作符用于比拟两个值是否相等,这种语法的简洁性使得代码更易读和了解。 - 性能 :默认的哈希函数是通过优化和测试的,可能在大多数状况下提供良好的性能。其次应用
==
来实现相等性比拟,因为==
操作符是语言层面的原生操作,编译器能够对其进行优化,从而进步代码的执行效率。
4.2 key 不可变的限度
map
键的不可变性也是一个思考因素。基于 ==
来判断对象是否相等,间接保障了键的不可变性。目前,==
曾经反对了大部分类型的比拟,只有自定义构造体中的援用类型字段无奈间接应用 ==
进行比拟。如果键中不存在援用类型字段,这意味着放入 Map 键的值在运行时不能发生变化,从而保障了键在运行时的不可变性。
如果 key
没有不可变的限度,那么之前存储在 map
中的键值对可能会呈现问题。因为在搁置元素时,map
会依据键的以后值计算哈希值,并应用哈希值来查找对应的存储地位。如果放在 map
中的键的值产生了变动,此时计算出来的 hash
值可能也发生变化,这象征数据放在了谬误的地位。后续即便应用跟 map
中的键的同一个值去查找数据,也可能查找不到数据。
上面展现一个简略的代码,来阐明可变类型作为 key
会导致的问题:
type Person struct {
Name string
Age int
SliceField []string}
func main() {person := Person{Name: "Alice", Age: 25, SliceField: []string{"A", "B"}}
// 假如 Person 能够作为键,事实上是不反对的
personMap := make(map[Person]string)
personMap[person] = "Value 1"
// 批改 person 中 SliceField 的值
person.SliceField[0] = "X"
// 尝试通过雷同的 person 查找值
fmt.Println(personMap[person]) // 输入空字符串,找不到对应的值
}
如果抽取 equals
办法接口,由用户自行实现,此时 key
的不可变性就须要用户实现,其次 go
语言也须要减少一些检测机制,这首先减少了用户应用的累赘,这并不合乎 go
语言设计的哲学。
4.3 总结
综上所述,基于简略性、性能和语义一致性的思考以及键的不可变性,Go 语言选择应用 ==
操作符进行键的比拟,而将哈希函数作为运行时库的默认实现,更加合乎 go
语言设计的哲学。
5. 总结
在 Go 语言中,map 是一种无序的键值对汇合,它提供了高效的数据存储和检索机制。在应用 map 时,通常应用根本数据类型作为键。然而,当咱们想要应用自定义构造体作为键时,须要思考构造体中是否蕴含援用类型的字段。
自定义构造体作为 map
的键须要满足一些要求。首先,键的类型必须是可比拟的,也就是反对通过 ==
运算符进行相等性比拟。在 Go
中,根本数据类型和一些内置类型都满足这个要求。然而,如果构造体中蕴含援用类型的字段,那么该构造体就不能间接作为 map
的键,因为援用类型不具备简略的相等性比拟。
因而总的来说,蕴含援用类型字段的自定义构造体,是不能作为 map
的key
的。