Go語言 表達(dá)式估值順序規(guī)則

2023-02-16 17:39 更新

本文將解釋各種情形下表達(dá)式的估值順序。

一個(gè)表達(dá)式將在其所依賴的其它表達(dá)式估值之后進(jìn)行估值

這屬于常識(shí),沒什么好解釋的。一個(gè)顯然的例子是一個(gè)表達(dá)式將在組成它的子表達(dá)式都估值之后才能進(jìn)行估值。 比如,在一個(gè)函數(shù)調(diào)用f(x, y[n])中,

  • f()將在它所依賴的子表達(dá)式fxy[n]估值之后進(jìn)行估值;
  • 表達(dá)式y[n]將在它所依賴的子表達(dá)式ny估值之后進(jìn)行估值。

另外,程序代碼要素初始化順序章節(jié)中提供了一個(gè)關(guān)于包級(jí)變量初始化順序的例子。

包級(jí)變量初始化順序

在運(yùn)行時(shí)刻,當(dāng)一個(gè)包被加載的時(shí)候,不依賴于任何其它未初始化包級(jí)變量的未初始化包級(jí)變量將按照它們?cè)诖a中的聲明順序被初始化,直到此過程中不再有任何包級(jí)變量被初始化。 對(duì)于一個(gè)成功編譯了的Go程序,當(dāng)所有這樣的過程結(jié)束之后,所有的包級(jí)變量都應(yīng)該被初始化了。

在包級(jí)變量初始化過程中,呈現(xiàn)為空標(biāo)識(shí)符的包級(jí)變量和其它包級(jí)變被同等對(duì)待。

舉個(gè)例子,在下面的這個(gè)程序中,變量a依賴于變量b,變量c_依賴于變量a。 所以

  1. 第一個(gè)被初始化的變量是b,因?yàn)樗堑谝粋€(gè)不依賴于其它變量的包級(jí)變量。
  2. 第二個(gè)被初始化的變量是a,因?yàn)樵谧兞?code>b初始化之后,變量a是第一個(gè)不再依賴于未初始化包級(jí)變量的包級(jí)變量。.
  3. 第三和第四個(gè)被初始化的變量是_c。 在變量ab初始化之后,標(biāo)記變量_c均不再依賴于未初始化包級(jí)變量。
package main

import "fmt"

var (
	_ = f()
	a = b / 2
	b = 6
	c = f()
)

func f() int {
	a++
	return a
}

func main() {
	fmt.Println(a, b, c) // 5 6 5
}

上面這個(gè)程序打印出5 6 5。

通過一個(gè)多值表達(dá)式源值來初始化的多個(gè)包級(jí)變量將被一起初始化。 比如,在包級(jí)變量聲明var x, y = f()中,變量xy將被一起初始化。 或者說,在它們的初始化之間不會(huì)有其它包級(jí)變量被初始化。

在初始化所有包級(jí)變量之前,含有多個(gè)源值表達(dá)式的包級(jí)變量聲明將被拆解為多個(gè)單源值表達(dá)式變量聲明。 比如

var m, n = expr1, expr2


var m = expr1
var n = expr2

是等價(jià)的。

如果一些包級(jí)變量之間存在著編譯器難以覺察的依賴關(guān)系,則這些包級(jí)變量的初始化順序是未定義的,依賴于具體編譯器實(shí)現(xiàn)。 在下面這個(gè)Go白皮書中的例子中,

  • 變量a肯定在變量b之后初始化;
  • 但是變量x有可能在變量b之前、或者在變量b和變量a之間、或者在變量a之后初始化,取決于具體的編譯器實(shí)現(xiàn);
  • 函數(shù)sideEffect()有可能在變量x初始化之前或者之后被調(diào)用,取決于具體的編譯器實(shí)現(xiàn)。
// x是否依賴于a和b,不同的編譯器有不同的見解。
var x = I(T{}).ab()
// 假設(shè)函數(shù)sideEffect和x、a以及b均無關(guān)系。
var _ = sideEffect()
var a = b
var b = 42

type I interface    { ab() []int }
type T struct{}
func (T) ab() []int { return []int{a, b} }

注意:因?yàn)橐粋€(gè)代碼包中常常包含若干個(gè)代碼源文件,而Go白皮書并沒有強(qiáng)制規(guī)定編譯器按照如何順序來編譯一個(gè)代碼包中的源文件, 所以盡量不要讓一個(gè)代碼包中不同源文件中的包級(jí)變量存在復(fù)雜的依賴關(guān)系;否則不同的編譯器可能會(huì)將一些包級(jí)變量初始化為不同結(jié)果。

布爾(邏輯)運(yùn)算表達(dá)式中的操作數(shù)子表達(dá)式的估值順序

在一個(gè)布爾運(yùn)算a && b中,右操作數(shù)表達(dá)式b只在左操作數(shù)表達(dá)式a被估值為true的時(shí)候才會(huì)被估值。 所以,操作數(shù)表達(dá)式b如果需要被估值的話,它肯定在操作數(shù)表達(dá)式a之后估值。

在一個(gè)布爾運(yùn)算a || b中,右操作數(shù)表達(dá)式b只在左操作數(shù)表達(dá)式a被估值為false的時(shí)候才會(huì)被估值。 所以,操作數(shù)表達(dá)式b如果需要被估值的話,它肯定在操作數(shù)表達(dá)式a之后估值。

通常估值順序(The Usual Order)

Go白皮書這樣描述通常估值順序

...,當(dāng)估值一個(gè)表達(dá)式、賦值語句或者函數(shù)返回語句中的操作數(shù)時(shí),所有的函數(shù)調(diào)用、方法調(diào)用和通道操作將按照它們?cè)诖a中的出現(xiàn)順序進(jìn)行估值。

注意:一個(gè)顯式類型轉(zhuǎn)換T(v)不屬于函數(shù)調(diào)用。

舉個(gè)例子,在表達(dá)式[]int{x, fa(), fb(), y}中,假設(shè)xy是兩個(gè)int類型的變量,fafb是兩個(gè)返回值為int類型的函數(shù),則調(diào)用fa()保證在調(diào)用fb()之前執(zhí)行。 但是,下面這兩個(gè)估值順序沒有在Go白皮書中指定:

  • 變量x(或者y)和調(diào)用fa()(或者fb())的相對(duì)估值順序;
  • 變量x、變量y、函數(shù)值fa和函數(shù)值fb的相對(duì)估值順序。

下面是Go白皮書中提到的另一個(gè)例子:

y[z.f()], ok = g(h(a, b), i()+x[j()], <-c), k()

在此賦值語句中,

  • c是一個(gè)通道表達(dá)式,它將被估值為一個(gè)通道值;
  • g、hi、jk是一些函數(shù)表達(dá)式,它們將被估值為一些函數(shù)值;
  • f是表達(dá)式z值的一個(gè)方法。

綜合考慮上一節(jié)和本節(jié)上面已經(jīng)提到的規(guī)則,編譯器應(yīng)該保證下列在運(yùn)行時(shí)刻的估值順序:

  • 此賦值中涉及到的函數(shù)調(diào)用、方法調(diào)用和通道操作必須按照這樣的順序執(zhí)行:z.f()h()i()j()<-cg()k();
  • 調(diào)用h()在表達(dá)式h、ab估值之后調(diào)用;
  • y[]在方法調(diào)用z.f()執(zhí)行之后被估值;
  • 方法調(diào)用z.f()在表達(dá)式z估值之后執(zhí)行;
  • x[]在調(diào)用j()執(zhí)行之后被估值。

然而,下列次序在Go白皮書中未指定,它們依賴于具體編譯器實(shí)現(xiàn):

  • 表達(dá)式y、z、gh、a、bx、ij、ck之間的相對(duì)估值順序;
  • 表達(dá)式y[]、x[]<-c之間的相對(duì)估值順序。

根據(jù)上述通常估值順序規(guī)則,我們知道下面聲明的變量xmn的初始值可能將出現(xiàn)歧義。

	a := 1
	f := func() int { a++; return a }

	// x可能初始化為[1, 2]或者[2, 2],
	// 因?yàn)閍和f()的相對(duì)估值順序未指定。
	x := []int{a, f()}

	// m可能初始化為{2: 1}或者{2: 2},
	// 因?yàn)閮蓚€(gè)映射元素的賦值順序未指定。
	m := map[int]int{a: 1, a: 2}

	// n可能初始化為{2: 3}或者{3: 3},
	// 因?yàn)閍和f()的相對(duì)估值順序未指定。
	n := map[int]int{a: f()}

賦值語句中的表達(dá)式估值和賦值執(zhí)行順序

除了上面介紹的規(guī)則,Go白皮書對(duì)賦值語句中的表達(dá)式估值和各個(gè)單值賦值執(zhí)行順序進(jìn)行了更多描述(原英文描述不是十分精確,在翻譯過程中對(duì)之略加改進(jìn)):

一條賦值語句的執(zhí)行分為兩個(gè)階段。 首先,做為目標(biāo)值的元素索引表達(dá)式中的容器值表達(dá)式和索引值表達(dá)式、做為目標(biāo)值的指針解引用表達(dá)式中的指針值表達(dá)式、以及此賦值語句中的其它非目標(biāo)值表達(dá)式將按照上述通常估值順序估值。 然后,各個(gè)單值賦值將按照從左到右的順序執(zhí)行。

以后,我們可以稱第一個(gè)階段為估值階段,稱第二個(gè)階段為實(shí)施階段。

Go白皮書并沒有清楚地說明在第二個(gè)階段中發(fā)生的賦值操作是否會(huì)對(duì)在第一個(gè)階段結(jié)尾確定下來的各個(gè)子表達(dá)式的估值結(jié)果造成影響,此舉曾造成了一些爭(zhēng)議。 所以,這里下面將對(duì)賦值語句中的表達(dá)式估值順序做出一些補(bǔ)充解釋。

首先,先明確一下:第二個(gè)階段中發(fā)生的賦值操作絕不會(huì)對(duì)在第一個(gè)階段結(jié)尾確定下來的各個(gè)子表達(dá)式的估值結(jié)果造成影響.

為了方便下面的解釋,對(duì)于一個(gè)賦值語句,我們假設(shè)一個(gè)做為目標(biāo)值的容器(切片或者映射)元素索引表達(dá)式中的映射值總是可尋址的。 如果它是不可尋址的,我們可以認(rèn)為在實(shí)施第二個(gè)階段之前,此容器值已經(jīng)被賦給了一個(gè)臨時(shí)變量(可尋址的)并且在此賦值語句中此容器值已經(jīng)被此臨時(shí)變量取代。

在估值階段結(jié)束之后、實(shí)施階段開始之前的時(shí)刻,賦值語句中的每個(gè)目標(biāo)值表達(dá)式都已經(jīng)被估值為它的最基本形式。 不同風(fēng)格的目標(biāo)值表達(dá)式有著不同的最基本形式:

  • 如果一個(gè)目標(biāo)值表達(dá)式是一個(gè)空標(biāo)識(shí)符,則它的最基本形式依舊是一個(gè)空標(biāo)識(shí)符;
  • 如果一個(gè)目標(biāo)值表達(dá)式是一個(gè)容器(數(shù)組或者切片或者映射)元素索引表達(dá)式c[k],則它的最基本形式為(*cAddr)[k],其中一個(gè)cAddr為指向c的指針;
  • 對(duì)于其它情形的任何一個(gè)目標(biāo)值表達(dá)式,它必然是可尋址的,則它的最基本形式為它的地址的解引用形式。

假設(shè)ab為兩個(gè)可尋址的同類型變量,則下面的賦值語句

	a, b = b, a

將按照如下步驟執(zhí)行:

// 估值階段
P0 := &a; P1 := &b
R0 := b; R1 := a

// 最基本形式:*P0, *P1 = R0, R1

// 實(shí)施階段
*P0 = R0
*P1 = R1

下面是另外一個(gè)例子,其中x[0]而不是x[1]被修改了。

	x := []int{0, 0}
	i := 0
	i, x[i] = 1, 2
	fmt.Println(x) // [2 0]

上例中的第3行的分解執(zhí)行步驟如下:

// 估值階段
P0 := &i; P1 := &x; T2 := i
R0 := 1; R1 := 2
// 到這里,T2 == 0

// 最基本形式:*P0, (*P1)[T2] = R0, R1

// 實(shí)施階段
*P0 = R0
(*P1)[T2] = R1

下面是一個(gè)略為復(fù)雜一點(diǎn)的例子。

package main

import "fmt"

func main() {
	m := map[string]int{"Go": 0}
	s := []int{1, 1, 1}; olds := s
	n := 2
	p := &n
	s, m["Go"], *p, s[n] = []int{2, 2, 2}, s[1], m["Go"], 5
	fmt.Println(m, s, n) // map[Go:1] [2 2 2] 0
	fmt.Println(olds)    // [1 1 5]
}

上例中的第10行的分解執(zhí)行步驟如下:

// 估值階段
P0 := &s; PM1 := &m; K1 := "Go"; P2 := p; PS3 := &s; T3 := 2
R0 := []int{2, 2, 2}; R1 := s[1]; R2 := m["Go"]; R3 := 5
// 到這里,R1 == 1, R2 == 0

// 最基本形式:*P0, (*PM1)[K1], *P2, (*PS3)[T3] = R0, R1, R2, R3

// 實(shí)施階段
*P0 = R0
(*PM1)[K1] = R1
*P2 = R2
(*PS3)[T3] = R3

下面這個(gè)例子將一個(gè)切片中的所有元素循環(huán)順移了一位。

	x := []int{2, 3, 5, 7, 11}
	t := x[0]
	var i int
	for i, x[i] = range x {}
	x[i] = t
	fmt.Println(x) // [3 5 7 11 2]

另一個(gè)例子:

	x := []int{123}
	x, x[0] = nil, 456        // 此句不會(huì)發(fā)生恐慌
	x, x[0] = []int{123}, 789 // 此句將產(chǎn)生恐慌

盡管使用復(fù)雜的多值賦值語句是合法的,但是在實(shí)踐中并不推薦使用,因?yàn)閺?fù)雜多值賦值語句的可讀性不高,編譯速度較慢,并且執(zhí)行效率相對(duì)略低。

上面已經(jīng)提到了,并非所有的估值順序都在Go白皮書中指定清楚了,所以不同的Go編譯器對(duì)這些未指定的估值順序有著自己的理解。 一些跨編譯器兼容性不好的代碼將導(dǎo)致使用不同的編譯器編譯的程序的運(yùn)行結(jié)果不同。 在下面這個(gè)例子中,表達(dá)式x+1f(&x)的估值順序是編譯器相關(guān)的,所以此程序輸出100 99或者1 99都是合理的。

package main

import "fmt"

func main() {
	f := func (p *int) int {
		*p = 99
		return *p
	}

	x := 0
	y, z := x+1, f(&x)
	fmt.Println(y, z)
}

下面是另一個(gè)可能輸出不同結(jié)果的程序。它可能輸出1 7 21 8 2或者1 9 2,取決于不同的編譯器實(shí)現(xiàn)。

package main

import "fmt"

func main() {
	x, y := 0, 7
	f := func() int {
		x++
		y++
		return x
	}
	fmt.Println(f(), y, f())
}

switch-case流程控制代碼塊中的表達(dá)式估值順序

switch-case流程控制代碼塊中的表達(dá)式估值順序已經(jīng)在前面的文章中大致描述過了。 這里僅僅展示一個(gè)例子。簡(jiǎn)單來說,在進(jìn)入一個(gè)分支代碼塊之前,各個(gè)case關(guān)鍵字后跟隨的表達(dá)式將按照從上到下和從左到右的順序進(jìn)行估值,直到某個(gè)比較結(jié)果為true為止。

package main

import "fmt"

func main() {
	f := func(n int) int {
		fmt.Printf("f(%v) is called.\n", n)
		return n
	}

	switch x := f(3); x + f(4) {
	default:
	case f(5):
	case f(6), f(7), f(8):
	case f(9), f(10):
	}
}

在運(yùn)行時(shí)刻,各個(gè)f()調(diào)用將按照傳給它們參數(shù)的大小順序進(jìn)行估值,直到和調(diào)用f(7)的比較結(jié)果為true為止。 所以調(diào)用f(8)、f(9)f(10)將不會(huì)被估值。

輸出結(jié)果:

f(3) is called.
f(4) is called.
f(5) is called.
f(6) is called.
f(7) is called.

select-case流程控制代碼塊中的表達(dá)式估值順序

當(dāng)執(zhí)行一個(gè)select-case流程控制代碼塊時(shí),各個(gè)case關(guān)鍵值后跟隨的所有通道操作中的通道表達(dá)式和所有通道發(fā)送操作中的發(fā)送值表達(dá)式都將被按照它們?cè)诖a中的出現(xiàn)次序(從上到下從左到右)估值一次。

注意:以通道接收操作做為源值的賦值語句中的目標(biāo)值表達(dá)式只有在此通道接收操作被選中之后才會(huì)被估值。

比如,在下面這個(gè)例子中,表達(dá)式*fptr("aaa")將永不會(huì)得到估值,因?yàn)樗鼘?duì)應(yīng)的通道接收操作<-fchan("bbb", nil)是個(gè)不可能被選中的阻塞操作。

package main

import "fmt"

func main() {
	c := make(chan int, 1)
	c <- 0
	fchan := func(info string, c chan int) chan int {
		fmt.Println(info)
		return c
	}
	fptr := func(info string) *int {
		fmt.Println(info)
		return new(int)
	}

	select {
	case *fptr("aaa") = <-fchan("bbb", nil): // blocking
	case *fptr("ccc") = <-fchan("ddd", c):   // non-blocking
	case fchan("eee", nil) <- *fptr("fff"):  // blocking
	case fchan("ggg", nil) <- *fptr("hhh"):  // blocking
	}
}

上例的輸出結(jié)果:

bbb
ddd
eee
fff
ggg
hhh
ccc

注意,表達(dá)式*fptr("ccc")是上例中最后一個(gè)被估值的表達(dá)式。 它在對(duì)應(yīng)的數(shù)據(jù)接收操作<-fchan("ddd", c)被選中之后才會(huì)進(jìn)行估值。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)