- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
作者: @apocelipes 本文为作者原创,转载请注明出处: https://www.cnblogs.com/apocelipes/p/17235955.html 。
对于无类型常量,可能大家是第一次听说,但这篇我就不放进 拾遗系列 里了.
因为虽然名字很陌生,但我们每天都在用,每天都有无数潜在的坑被埋下。包括我本人也犯过同样的错误,当时代码已经合并并发布了,当我意识到出了什么问题的时候为时已晚,最后不得不多了个合并请求留下了丢人的黑历史.
为什么我要提这种尘封往事呢,因为最近有朋友遇到了一样的问题,于是勾起了上面的那些“美好”回忆。于是我决定记录一下,一来备忘,二来帮大家避坑.
由于涉及各种隐私,朋友提问的代码没法放出来,但我可以给一个简单的复现代码,正如我所说,这个问题是很常见的:
package main
import "fmt"
type S string
const (
A S = "a"
B = "b"
C = "c"
)
func output(s S) {
fmt.Println(s)
}
func main() {
output(A)
output(B)
output(C)
}
这段代码能正常编译并运行,能有什么问题?这里我就要提示你一下了, B 和 C 的类型是什么?
你会说他们都是 S 类型,那你就犯了第一个错误,我们用发射看看:
fmt.Println(reflect.TypeOf(any(A)))
fmt.Println(reflect.TypeOf(any(B)))
fmt.Println(reflect.TypeOf(any(C)))
输出是:
main.S
string
string
惊不惊喜意不意外,常量的类型是由等号右边的值推导出来的(iota是例外,但只能处理整型相关的),除非你显式指定了类型.
所以在这里B和C都是string.
那真正的问题来了,正如我在 这篇 所说的,从原类型新定义的类型是独立的类型,不能隐式转换和赋值给原类型.
所以这样的代码就是错的:
func output(s S) {
fmt.Println(s)
}
func main() {
var a S = "a"
output(a)
}
编译器会报错。然而我们最开始的复现代码是没有报错的:
const (
A S = "a"
B = "b"
C = "c"
)
func output(s S) {
fmt.Println(s)
}
output 函数只接受 S 类型的值,但我们的 B 和 C 都是string类型的,为什么这里可以编译通过还正常运行了呢?
这就要说到golang的坑点之一—— 无类型常量了 .
这个好理解,定义常量时没指定类型,那就是无类型常量,比如:
const (
A S = "a"
B = "b"
C = "c"
)
这里A显式指定了类型,所以不是无类型常量;而B和C没有显式指定类型,所以就是无类型常量(untyped constant).
无类型常量有一些特性和其他有类型的常量以及变量不一样,得单独讲讲.
正如下面的代码里我们看到的:
const (
A = "a"
B = 1
C = 1.0
)
func main() {
fmt.Println(reflect.TypeOf(any(A))) // string
fmt.Println(reflect.TypeOf(any(B))) // int
fmt.Println(reflect.TypeOf(any(C))) // float64
}
虽说我们没给这些常量指定某个类型,但他们还是有自己的类型,和初始化他们的字面量的默认类型相应,比如整数字面量是 int ,字符串字面量是 string 等等.
但只有一种情况下他们才会表现出自己的默认类型,也就是在上下文中没法推断出这个常量现在应该是什么类型的时候,比如赋值给空接口.
这个名字不好,是我根据它的表现起的,官方的名字叫 Representability ,直译过来是“代表性”.
看下这个例子:
const delta = 1 // untyped constant, default type is int
var num int64
num += delta
如果我们把 const 换成 var ,代码无法编译,会爆出这种错误: invalid operation: num + delta (mismatched types int64 and int) .
但为什么常量可以呢?这就是 Representability 或者说类型自动匹配在捣鬼.
按照官方的解释: 如果一个无类型常量的值是一个类型T的有效值,那么这个常量的类型就可以是类型T .
举个例子, int8 类型的所有合法的值是 [-128, 127) ,那么只要值在这个范围内的整数常量,都可以被转换成int8.
字符串类型同理, 所有用字符串初始化的无类型常量都可以转换成字符串以及那些基于字符串创建的新类型 .
这就解释了开头那段代码为什么没问题:
type S string
const (
A S = "a"
B = "b"
C = "c"
)
func output(s S) {
fmt.Println(s)
}
func main() {
output(A) // A 本来就是 S,自然没问题
output(B) // B 是无类型常量,默认类型string,可以表示成 S,没问题
output(C) // C 是无类型常量,默认类型string,可以表示成 S,没问题
// 下面的是有问题的,因为类型自动匹配不会发生在无类型常量和字面量以外的地方
// s := "string"
// output(s)
}
也就是说,在有明确给出类型的上下文里,无类型常量会尝试去匹配那个目标类型T,如果常量的值符合目标类型的要求,常量的类型就会变成目标类型T。例子里的 delta 的类型就会自动变成 int64 类型.
我没有去找为什么golang会这么设计,在c++、rust和Java里常量的类型就是从初始化表达式推导或显式指定的那个类型.
一个猜测是golang的设计初衷想让常量的行为表现和字面量一样。除了两者都有的类型自动匹配,另一个有力证据是golang里能作为常量的只有那些能做字面类型的类型(字符串、整数、浮点数、复数).
无类型常量的类型自动匹配会带来很有限的好处,以及很恶心的坑.
便利只有一个,可以少些几次类型转换,考虑下面的例子:
const factor = 2
var result int64 = int64(num) * factor / ( (a + b + c) / factor )
这样复杂的计算表达式在数据分析和图像处理的代码里是很常见的,如果我们没有自动类型匹配,那么就需要显式转换factor的类型,光是想想就觉得烦人,所以我也就不写显式类型转换的例子了.
有了无类型常量,这种表达式的书写就没那么折磨了.
说完聊胜于无的好处,下面来看看坑.
一种常见的在golang中模拟enum的方法如下:
type ConfigType string
const (
CONFIG_XML ConfigType = "XML"
CONFIG_JSON = "JSON"
)
发现上面的问题了吗,没错,只有 CONFIG_XML 是 ConfigType 类型的! 。
但因为无类型常量有自动类型匹配,所以你的代码目前为止运行起来一点问题也没有,这也导致你没发现这个缺陷,直到:
// 给enum加个方法,现在要能获取常量的名字,以及他们在配置数组里的index
type ConfigType string
func (c ConfigType) Name() string {
switch c {
case CONFIG_XML:
return "XML"
case CONFIG_JSON:
return "JSON"
}
return "invalid"
}
func (c ConfigType) Index() int {
switch c {
case CONFIG_XML:
return 0
case CONFIG_JSON:
return 1
}
return -1
}
目前为止一切安好,然后代码炸了:
fmt.Println(CONFIG_XML.Name())
fmt.Println(CONFIG_JSON.Name()) // !!! error
编译器不乐意,它说: CONFIG_JSON.Name undefined (type untyped string has no field or method Name) .
为什么呢,因为上下文里没明确指定类型, fmt.Println 的参数要求都是 any ,所以这里用了无类型常量的默认类型。当然在其他地方也一样, CONFIG_JSON.Name() 这个表达式是无法推断出 CONFIG_JSON 要匹配成什么类型的.
这一切只是因为你少写了一个类型.
这还只是第一个坑,实际上因为只要是目标类型可以接受的值,就可以赋值给目标类型,那么出现这种代码也不奇怪:
const NET_ERR_MESSAGE = "site is unreachable"
func doWithConfigType(t ConfigType)
doWithConfigType(CONFIG_JSON)
doWithConfigType(NET_ERR_MESSAGE) // WTF???
一不小心就能把错得离谱的参数传进去,如果你没想到这点而做好防御的话,生产事故就理你不远了.
第一个坑还可以通过把常量定义写全每个都加上类型来避免,第二个就只能靠防御式编程凑活了.
看到这里,你也应该猜到我当年闯的是什么祸了。好在及时发现,最后补全声明 + 防御式编程在出事故前把问题解决了.
最后也许有人会问,golang实现enum这么折磨?没有别的办法了吗?
当然有,而且有不少,其中一个比较著名的是 stringer : https://pkg.go.dev/golang.org/x/tools/cmd/stringer 。
这个工具也只能解决一部分问题,但以及比什么都做不了要强太多了.
无类型常量会自动转换到匹配的类型,这会带来意想不到的麻烦.
一点建议:
这就是golang的大道至简,简单它自己,坑都留给你.
https://go.dev/ref/spec#Representability 。
最后此篇关于小心golang中的无类型常量的文章就讲到这里了,如果你想了解更多关于小心golang中的无类型常量的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我想做的是让 JTextPane 在 JPanel 中占用尽可能多的空间。对于我使用的 UpdateInfoPanel: public class UpdateInfoPanel extends JP
我在 JPanel 中有一个 JTextArea,我想将其与 JScrollPane 一起使用。我正在使用 GridBagLayout。当我运行它时,框架似乎为 JScrollPane 腾出了空间,但
我想在 xcode 中实现以下功能。 我有一个 View Controller 。在这个 UIViewController 中,我有一个 UITabBar。它们下面是一个 UIView。将 UITab
有谁知道Firebird 2.5有没有类似于SQL中“STUFF”函数的功能? 我有一个包含父用户记录的表,另一个表包含与父相关的子用户记录。我希望能够提取用户拥有的“ROLES”的逗号分隔字符串,而
我想使用 JSON 作为 mirth channel 的输入和输出,例如详细信息保存在数据库中或创建 HL7 消息。 简而言之,输入为 JSON 解析它并输出为任何格式。 最佳答案 var objec
通常我会使用 R 并执行 merge.by,但这个文件似乎太大了,部门中的任何一台计算机都无法处理它! (任何从事遗传学工作的人的附加信息)本质上,插补似乎删除了 snp ID 的 rs 数字,我只剩
我有一个以前可能被问过的问题,但我很难找到正确的描述。我希望有人能帮助我。 在下面的代码中,我设置了varprice,我想添加javascript变量accu_id以通过rails在我的数据库中查找记
我有一个简单的 SVG 文件,在 Firefox 中可以正常查看 - 它的一些包装文本使用 foreignObject 包含一些 HTML - 文本包装在 div 中:
所以我正在为学校编写一个 Ruby 程序,如果某个值是 1 或 3,则将 bool 值更改为 true,如果是 0 或 2,则更改为 false。由于我有 Java 背景,所以我认为这段代码应该有效:
我做了什么: 我在这些账户之间创建了 VPC 对等连接 互联网网关也连接到每个 VPC 还配置了路由表(以允许来自双方的流量) 情况1: 当这两个 VPC 在同一个账户中时,我成功测试了从另一个 La
我有一个名为 contacts 的表: user_id contact_id 10294 10295 10294 10293 10293 10294 102
我正在使用 Magento 中的新模板。为避免重复代码,我想为每个产品预览使用相同的子模板。 特别是我做了这样一个展示: $products = Mage::getModel('catalog/pro
“for”是否总是检查协议(protocol)中定义的每个函数中第一个参数的类型? 编辑(改写): 当协议(protocol)方法只有一个参数时,根据该单个参数的类型(直接或任意)找到实现。当协议(p
我想从我的 PHP 代码中调用 JavaScript 函数。我通过使用以下方法实现了这一点: echo ' drawChart($id); '; 这工作正常,但我想从我的 PHP 代码中获取数据,我使
这个问题已经有答案了: Event binding on dynamically created elements? (23 个回答) 已关闭 5 年前。 我有一个动态表单,我想在其中附加一些其他 h
我正在尝试找到一种解决方案,以在 componentDidMount 中的映射项上使用 setState。 我正在使用 GraphQL连同 Gatsby返回许多 data 项目,但要求在特定的 pat
我在 ScrollView 中有一个 View 。只要用户按住该 View ,我想每 80 毫秒调用一次方法。这是我已经实现的: final Runnable vibrate = new Runnab
我用 jni 开发了一个 android 应用程序。我在 GetStringUTFChars 的 dvmDecodeIndirectRef 中得到了一个 dvmabort。我只中止了一次。 为什么会这
当我到达我的 Activity 时,我调用 FragmentPagerAdapter 来处理我的不同选项卡。在我的一个选项卡中,我想显示一个 RecyclerView,但他从未出现过,有了断点,我看到
当我按下 Activity 中的按钮时,会弹出一个 DialogFragment。在对话框 fragment 中,有一个看起来像普通 ListView 的 RecyclerView。 我想要的行为是当
我是一名优秀的程序员,十分优秀!