又一道比较运算符相关的面试题让我明白基础很重要
来自公众号:新世界杂货铺
比较运算不简单啊
我们先看一下上一期的投票结果:
首先,笔者自己选择了true
,所以实际结果是41%
的读者都选择了错误的答案。看到这个结果,笔者相信上一篇文章还是能够帮助到大家。
经过千辛万苦终于明白了上一道面试题是咋回事儿,这个时候却见面试官微微一笑道:“下面的输出结果是什么”。
这里笔者先留个悬念,结果见后文。
类型声明
注意:本节不介绍语法等基础内容,主要描述一些名词以便于后文的理解。
类型声明将标识符(类型名称)
绑定到类型
,其两种形式为类型定义和类型别名。
下面我们通过一个例子对标识符
、类型
和defined type
(后文会使用这个名词)进行解释:
标识符(类型名称)
:在上面的例子中,t1,t2 为标识符。
类型
:struct{ x, y float64 }
为类型。
类型别名不会创建新的类型。在上述例子中t1
和struct{ x, y float64 }
是相同的类型。
类型定义会创建新的类型,且这个新类型又被叫做defined type
。在上述例子中,新类型t2
和struct{ x, y float64 }
是不同的类型。
underlying type
定理一:
每一个类型 T 都有一个underlying type
(笔者称之为原始类型,在后面文章中的原始类型均代表 underlying type)。
定理二:如果 T 是预定义的boolean
、numeric
和string
类型之一,或者是类型字面量则 T 的原始类型是其本身,否则 T 的原始类型为 T 在其类型声明中引用的类型的原始类型。
Go 中的数组、结构体、指针、函数、interface{}、slice、map 和 channel 类型均由类型字面量构成。下面以 map 为例:
在上面的例子中,T 为 map 类型,a 为 map 类型的变量,类型字面量均为map[int]string
且根据定理二可知 T 的原始类型为map[int]string
。
下面再看一个例子加深对原始类型的理解:
上述例子中,string
、A1
、 A2
、B1
和B2
的原始类型为string
、[]B1
、B3
和B4
的原始类型为[]B1
。
类型相同
在 Go 中一个defined type
类型总是和其他类型不同。类型相同情况如下:
1、两个数组长度相同且元素类型相同则这两个数组类型相同。
2、两个切片元素类型相同则这两个切片类型相同。
3、两个函数有相同数量的参数和相同数量的返回值,且对应位置的参数类型和返回值类型均相同则这两个函数类型相同。
4、如果两个指针具有相同的基本类型则这两个指针类型相同。
5、如果两个 map 具有相同类型的 key 和相同类型的元素则这两个 map 类型相同。
6、如果两个 channel 具有相同的元素类型且方向相同则这两个 channel 类型相同。
7、如果两个结构体具有相同数量的字段,且对应字段名称相同,类型相同并且标签相同则这两个结构体类型相同。对于不同包下面的结构体,只要包含未导出字段则这两个结构体类型不相同。
8、如果两个接口的方法数量和名称均相等,且相同名称的方法具有相同的函数类型则这两个接口类型相同。
类型可赋值
满足下列任意条件时,变量 x 能够赋值给类型为 T 的变量。
1、x 的类型和 T 类型相同。
2、x 的类型 V 和 T 具有相同的原始类型,并且 V 和 T 至少有一个不是defined type
。
map1 和 map2 变量的原始类型为map[int]string
,且满足只有 map2 是defined type
,所以能够正常赋值。
map3 和 map1 同样满足条件,所以能够正常赋值。但是 map4 和 map2 不满足至少有一个不是defined type
这一条件,故会编译报错。
3、T 是 interface{} 并且 x 的类型实现了 T 的所有方法。
4、x 是双向通道,T 是通道类型,x 的类型 V 和 T 具有相同的元素类型,并且 V 和 T 中至少有一个不是defined type
。
根据上面我们可以知道一个隐藏逻辑是,双向通道能够赋值给单向通道,但是单向通道不能赋值给双向通道。
因为 c1 能够正常赋值给 c2,所以根据前一篇文章的定理“在任何比较中,至少满足一个操作数能赋值给另一个操作数类型的变量”知 c1 和 c2 可比较。
5、x 是预声明标识符nil
,T 是指针、函数、切片、map、channel 或 interface{}类型。
6、x 是可由类型 T 的值表示的无类型常量。
上述例子中,s1 是无类型字符串常量故 s1 可以赋值给类型为 str1 和 str2 的变量。
下图是在 vscode 中当鼠标悬浮在变量 s1 上时给的提示。
注意:笔者在实际的验证过程中发现部分有类型的常量和变量在赋值时会编译报错。
上述代码在 vscode 中的错误为cannot use s2 (constant "1111" of type string) as str1 value in variable declaration
。
看到上述编译报错,笔者顿时惊了,就算不满足第 6 点也应该满足第 2 点呀。抱着满是疑惑的心情笔者利用代码跳转,最后在builtin.go
发现了type string string
这样一条语句。
结合上述代码我们知道str1
和string
是由类型定义创建的新类型即defined type
,所以var s5 str1 = s2
也不满足第 2 点。
builtin.go
文件对boolean
、numeric
和string
的类型均做了类型定义,下面以int
做近一步验证:
上述结果符合预期,因此我们在平时的开发中对于变量赋值的细节还需牢记于心。
分析总结
有了前面类型相同和*类型可赋值*两小节的基础知识我们按照下面步骤对本篇的面试题进行分析总结。
1、类型是否相同?
我们先列出面试题中需要比较的两个结构体:
根据类型相同小节的第 7 点知,这两个结构体具有相同数量的字段,且对应字段名称相同、类型相同并且标签也相同,因此这两个结构体类型相同。
2、是否满足可赋值条件?
根据类型可赋值小节的第 1 点知,这两个结构体类型相同因此满足可赋值条件。
面试题中的两个结构体比较简单,下面笔者对结构体的不同场景进行补充。
结构体 tag 不同
上述代码在 vscode 中的报错为cannot use bst11 (variable of type struct{a int; _ string}) as blankSt1 value in variable declaration
。两个结构体只要 tag 不同则这两个结构体类型不同,此时这两个结构体不满足任意可赋值条件。
结构体在不同包,且所有字段均导出
根据类型相同小节的第 7 点和类型可赋值小节的第 1 点知,ST1 和 st1 类型相同且可赋值,因此上述代码能够正常运行
结构体在不同包,且包含未导出字段
运行上述代码时出现cannot use ttt.B (type struct { F string; ttt.a string }) as type st2 in assignment
错误。
由于 st2 和 ST2 类型不同且他们的原始类型分别为struct { F string a string }
和 struct { F string; ttt.a string }
,所以 ttt.b 无法赋值给 st21。
3、总结
blankSt
和struct { a int _ string }
类型相同且满足可赋值条件,因此根据“在任何比较中,至少满足一个操作数能赋值给另一个操作数类型的变量”这一定理知面试题中的bst1
和bst2
可比较。
接下来根据上一篇文章提到的结构体比较规则知bst1
和bst2
相等,所以面试题最终输出结果为true
。
如果不是再去研读一篇 Go 的基础语法,笔者还不知道曾经遗漏了这么多细节。“读书百遍其义自见”,古人诚不欺我!
最后,衷心希望本文能够对各位读者有一定的帮助。
注:
1. 写本文时, 笔者所用 go 版本为: go1.14.2
2. 文章中所用完整例子:https://github.com/Isites/go-coder/blob/master/types/main.go
版权声明: 本文为 InfoQ 作者【Gopher指北】的原创文章。
原文链接:【http://xie.infoq.cn/article/1f048820ee09bec992ef7d264】。未经作者许可,禁止转载。
评论