包含引用类型字段的自定义结构体,能作为map的key吗
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
的。
包含引用类型字段的自定义结构体,能作为map的key吗的更多相关文章
- Solidity的自定义结构体深入详解
一.结构体定义 结构体,Solidity中的自定义类型.我们可以使用Solidity的关键字struct来进行自定义.结构体内可以包含字符串,整型等基本数据类型,以及数组,映射,结构体等复杂类型.数组 ...
- typedef和自定义结构体类型
在自定义结构体类型时会用到typedef关键字.大家都知道typedef是取别名的意思,在C语言中跟它容易混淆的有const,#define等,其区别不在本篇文章讨论之列. /*定义单链表结点类型*/ ...
- qsettings 保存自定义结构体(QVariant与自定义结构体相互转化)
参考博文:QVariant与自定义数据类型转换的方法. 这里摘取其关键内容: 1.将自定义数据类型使用Q_DECLARE_METATYPE宏进行声明,便于编译器识别. 2.在插入对象的时候,声明QVa ...
- iOS自定义结构体
一.提要 通过以官方的CGSize为例,自定义Objective-C中的结构体,并使用. 二.CGSize 1.系统定义的CGSize结构体 struct CGSize { CGFloat width ...
- Qt--信号槽传递自定义结构体参数
自定义结构体参数的信号槽连接 (1) 对于自定义的结构体参数,信号槽无法识别参数,导致信号槽连接不起作用.所以需要注册结构体参数.在结构体中声明结束的地方加上结构体注册. struct DealDet ...
- 用set、map等存储自定义结构体时容器内部判别各元素是否相同的注意事项
STL作为通用模板极大地方便了C++使用者的编程,因为它可以存储任意数据类型的元素 如果我们想用set与map来存储自定义结构体时,如下 struct pp { double xx; double y ...
- gin中绑定表单数据至自定义结构体
package main import "github.com/gin-gonic/gin" type StructA struct { FieldA string `form:& ...
- 【大型软件开发】浅谈大型Qt软件开发(三)QtActive Server如何通过COM口传递自定义结构体?如何通过一个COM口来获得所有COM接口?
前言 最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考. 鉴于接下来的一年我要进行这个 ...
- QT:用QSet储存自定义结构体的问题——QSet和STL的set是有本质区别的,QSet是基于哈希算法的,要求提供自定义==和qHash函数
前几天要用QSet作为储存一个自定义的结构体(就像下面这个程序一样),结果死活不成功... 后来还跑到论坛上问人了,丢脸丢大了... 事先说明:以下这个例子是错误的 #include <QtCo ...
- [UE4]自定义结构体、类、数据表
自定义数据表: #pragma once #include "CoreMinimal.h" #include "Engine/UserDefinedStruct.h&qu ...
随机推荐
- java数据类型转换有哪几种?看这篇就够了!
前言 在上一篇文章中,壹哥给大家讲解了Java中的数据类型,从此大家就知道了基本类型和引用类型,尤其是8种基本类型的使用和各自特点.但实际上数据类型的使用还有很多更深入的内容,比如java数据类型直接 ...
- 简述SpringAOP的实现原理
Spring默认采取的动态代理机制实现AOP,当动态代理不可用时 (代理类无接口)会使用CGlib机制. Spring提供了两种方式来生成代理对象:JDKProxy和Cglib,具体使用哪种方式生 ...
- 恰好经过k条边的最短路
需要用到离散数学中关于关系矩阵的运算的知识 一个表示一个图中任意两点间经过2条边最短路的关系矩阵的平方表示的是任意两点间经过4条边的最短路的关系矩阵 原因在于当我们选定中间点时,路径的前半部分和后半部 ...
- std常用类型
std::getline 文档 std::reverse 文档 注意事项 reverse()返回值为void,是对原序列进行修改 std::vector 文档 emplace 和 emplace_ba ...
- Conda in Windows under MSYS2 and Zsh 的问题解决
Conda in Windows under MSYS2 and Zsh 的问题解决 在Window11上使用git bash 安装zsh,并配置p10k主题,主要问题就是prompt中无法显示con ...
- nodsjs POST请求
PHP 遇到问题: 1.跨域问题 报failed to load response data:no data fond for response with give header("Cont ...
- 从APNIC获取中国IP地址列表
关于APNIC 全球IP地址块被IANA(Internet Assigned Numbers Authority)分配给全球三大地区性IP地址分配机构,它们分别是: ARIN (American Re ...
- 手写Mybatis代码实现会出现的问题
实现自定义框架过程中遇到的问题及解决方案: 1.执行 Resources.class.getClassLoader().getResourceAsStream(path) 方法无法获得去字节输入流 解 ...
- Python常见面试题016. 请实现如下功能|谈谈你对闭包的理解
016. 请实现如下功能|谈谈你对闭包的理解 摘自<流畅的python> 第七章 函数装饰器和闭包 实现一个函数(可以不是函数)avg,计算不断增加的系列值的平均值,效果如下 def av ...
- Thread面试题
面试题目录https://www.cnblogs.com/Kaelthas/p/15005844.html 1.一个Thread对象代表一个线程,同一个线程能否多次启动? 不能,在Thread类中变量 ...