一个关于移位操作的细节

2019-06-17 23:35:45 +08:00
 liulaomo

先看一段代码( aa.go ):

package main

func foo() float32 {
	const n = uint(2)
	var x float32 = 1 << n  // OK
	var y = float32(1 << n) // OK
	return x + y
}

func bar() float32 {
	var n = uint(2)
	var x float32 = 1 << n  // error: 不可对浮点数进行移位
	var y = float32(1 << n) // error: 不可对浮点数进行移位
	return x + y
}

func main() {}

编译之,得到如下出错信息:

./aa.go:12:20: invalid operation: 1 << n (shift of type float32)
./aa.go:13:17: invalid operation: 1 << n (shift of type float32)

为什么foo函数编译没问题,但是bar函数编译却通不过呢?两者代码中唯一的差别就是n在函数foo中为常量,而在函数bar中为变量。

在搞懂这个问题之前,我们需要了解 Go 语言设计中的一些细节。

首先,我们需要知道,Go 中的值可以分为两类:类型确定值和类型不确定值。一般来说,

第二,我们需要知道,每个类型不确定值(除了预声明的 nil )都有一个默认类型。 Go 支持类型推断。一般说来,当一个孤单的类型不确定数值用在一个需要确定类型的场景,此类型不确定数值将被视为一个类型为它的默认类型的类型确定值。 对于本文,我们只需知道,码点数值字面值(使用单引号括起来的数值字面值)的默认类型为内置类型runeint32类型的别名),其它任何数值字面值的内置类型为int。 比如,在下例中,字面值5在此场景中的类型将被推断为int,而字面值'A'的类型将被推断为rune

package main

import "fmt"

var n = 5 << 1
var c = 'A' >> 1

func main() {
	fmt.Printf("%T %T\n", n, 5)   // int int
	fmt.Printf("%T %T\n", c, 'A') // int32 int32
}

另一条常用的类型推断规则是对于加减乘除等二元操作,如果其中一个操作数是类型不确定的,而另一个是类型确定的,则此类型不确定操作数将被视为一个类型确定值并且的类型将被推断为另一个类型确定操作数的类型。比如,在下面这条语句中,字面值 2 的类型将被推断为 byte (因此 Y 也将被视为一个类型为 byte 的类型确定值)。

const Y = byte(1) + 2

第三,我们需要知道,一个类型不确定数值可以超出它的默认类型的取值范围,但是类型确定数值却不可以。 比如下例中的Zz行是编译不过的,但是Xx行却可以。

package main

// '@' == 64: 2 的 6 次方
// '@' << 25: 2 的 31 次方
const Y = rune('@') << 24 // OK
const Z = rune('@') << 25 // error: overflows rune
const X = '@' << 25       // OK

const z = int(1) << 100 // error: overflows int
const x = 1 << 100      // OK

func main() {} 

上例可以被认为是类型不确定值的一个优点。其实类型不确定值还有另外一个优点:一个类型不确定值可以不用显式转换而被当做某些合适的类型的值用。 比如:

package main

const X = '@' << 25 // X 为一个类型不确定值
const Y = int64(X)  // Y 为一个类型确定值

var m int = X          // OK
var n uint16 = X >> 16 // OK
var o float32 = X      // OK

var p int = Y          // error: 类型不匹配
var q uint16 = Y >> 16 // error: 类型不匹配
var r float32 = Y      // error: 类型不匹配

func main() {}

第四,我们需要知道,一个所有操作数均为常量的运算称为一个常量表达式。 每个常量表达式都将在编译时刻被估值,其估值结果仍然是一个常量。 特别地,对于一个移位运算常量表达式来说,如果它的左操作数为类型不确定值,则估值结果也是一个(可以超出它的默认类型的取值范围)类型不确定值。 如果一个移位运算中的任意一个操作数不为常量,则此表达式的估值肯定发生在运行阶段,因此其类型必须在编译时刻确定下来。

OK,到这里,我们可以回到正题了。

对于本文一开始展示的例子中的函数foo,按照上述规则,其中的字面值1将被认为是一个默认类型为 int 的类型不确定值。 常量表达式1 << n的结果为一个在编译时刻估值的类型不确定常量。 编译器将检查此估值结果是否溢出了float32

func foo() float32 {
	const n = uint(2)
	var x float32 = 1 << n  // OK
	var y = float32(1 << n) // OK
	return x + y
}

如果n的值过大,编译器将报错:

func foo() float32 {
	const n = uint(200)
	var x float32 = 1 << n  // error: 溢出 float32
	var y = float32(1 << n) // error: 溢出 float32
	return x + y
}

编译器可以在编译时刻得出常量表达式的值,但是却不能在编译时刻得出非常量表达式的值。 如果编译器将本文一开始展示的例子中的函数 bar 中的字面值 1 的类型推断为它的默认类型 int 而不是报错, 则此函数在运行时刻会在不同架构的机器上对于某些变量 n 值返回不同的值。 比如当n等于32时,函数bar在 32 位架构的机器上将返回0,而在 64 位架构的机器上将返回大约+8.589935e+009

func bar() float32 {
	var n = uint(32)
	var x float32 = 1 << n // 在 32 系统上运行时刻溢出
	var y = float32(1 << n)
	return x + y
}

这种在编译时刻不报错但在运行时刻返回不同结果的行为不是一个好的设计,特别对于跨平台编译来说更是如此。 为了防止此情况的发生,Go 语言的设计者对上面提到的类型推断规则“当一个孤单的类型不确定数值用在一个需要确定类型的场景,此类型不确定数值将被视为一个类型为它的默认类型的类型确定值”添加了一个例外:当一个移位运算的左操作数为一个类型不确定值并且右操作数(移动位数)不是一个常量时,左操作数将被视为一个类型确定值,但是它的类型将并不被推断为它的默认类型,而是被推断为它的设想类型。

什么是设想类型呢?下面这条赋值语句中的字面值1的设想类型为float32(如果n是一个非常量)。

var x float32 = 1 << n

所以它等价于下面这条语句:

var x = float32(1) << n

同样地,下面这条语句也等价于上面这条语句(如果 n 是一个非常量):

var x = float32(1 << n)

浮点数是不能参与移位运算的,所以编辑器将报错。

当然,如果n是一个常量,则上述等价关系并不存在。

到此为止,文章开头的问题已回答完毕。 如果你已经理解了上面的解释,则也会理解下例中为什么xX的值会不同。

package main

var n uint = 10
var x byte = (1 << n) / 100
var y int16 = (1 << n) / 100

const N uint = 10
const X byte = (1 << N) / 100
const Y int16 = (1 << N) / 100

func main() {
	println(x, y) // 0 10
	println(X, Y) // 10 10
}

本文首发在微信Go 101公众号,欢迎各位转载本文。Go 101公众号将尽量在每个工作日发表一篇原创短文,有意关注者请扫描下面的二维码。


关于更多 Go 语言编程中的事实、细节和技巧,请访问《 Go 语言 101 》项目 https://github.com/golang101/golang101

3070 次点击
所在节点    Go 编程语言
3 条回复
akira
2019-06-18 01:03:23 +08:00
var n uint = 10
var x byte = (1 << n) / 100 // x = 0
const N uint = 10
const X byte = (1 << N) / 100 // X = 10

怎么看怎么都觉得是坑啊。。
jingxyy
2019-06-18 09:29:12 +08:00
有点意思
liulaomo
2019-06-18 09:43:12 +08:00
@akira 立即反馈的意想不到比不知不觉的意想不到好得多,;)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/574910

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX