V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
liulaomo
V2EX  ›  Go 编程语言

一个关于移位操作的细节

  •  
  •   liulaomo · 2019-06-17 23:35:45 +08:00 · 3246 次点击
    这是一个创建于 2020 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先看一段代码( 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
    • 有些常量是类型确定值,有些是类型不确定值;但所有的字面型常量都属于类型不确定值,比如1, true, "Go"等,它们可以用作很类型的值。
    • 也有一些类型不确定值为非常量,比如预声明的 nil。

    第二,我们需要知道,每个类型不确定值(除了预声明的 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公众号将尽量在每个工作日发表一篇原创短文,有意关注者请扫描下面的二维码。

    image


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

    3 条回复    2019-06-18 09:43:12 +08:00
    akira
        1
    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
        2
    jingxyy  
       2019-06-18 09:29:12 +08:00
    有点意思
    liulaomo
        3
    liulaomo  
    OP
       2019-06-18 09:43:12 +08:00
    @akira 立即反馈的意想不到比不知不觉的意想不到好得多,;)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2699 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 12:20 · PVG 20:20 · LAX 04:20 · JFK 07:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.