18 | if語句、for語句和switch語句
現在,讓我們暫時走下神壇,回歸民間。我今天要講的if語句、for語句和switch語句都屬於 Go 語言的基本流程控制語句。它們的語法看起來很朴素,但實際上也會有一些使用技巧和注意事項。我在本篇文章中會以一系列面試題為線索,為你講述它們的用法。
那么,今天的問題是:使用攜帶range子句的for語句時需要注意哪些細節? 這是一個比較籠統的問題。我還是通過編程題來講解吧。
本問題中的代碼都被放在了命令源碼文件 demo41.go 的main函數中的。為了專注問題本身,本篇文章中展示的編程題會省略掉一部分代碼包聲明語句、代碼包導入語句和main函數本身的聲明部分。
numbers1 := []int{1, 2, 3, 4, 5, 6}
for i := range numbers1 {
if i == 3 {
numbers1[i] |= i
}
}
fmt.Println(numbers1)
我先聲明了一個元素類型為int的切片類型的變量numbers1,在該切片中有 6 個元素值,分別是從1到6的整數。我用一條攜帶range子句的for語句去迭代numbers1變量中的所有元素值。
在這條for語句中,只有一個迭代變量i。我在每次迭代時,都會先去判斷i的值是否等於3,如果結果為true,那么就讓numbers1的第i個元素值與i本身做按位或的操作,再把操作結果作為numbers1的新的第i個元素值。最后我會打印出numbers1的值。
所以具體的問題就是,這段代碼執行后會打印出什么內容?
這里的典型回答是:打印的內容會是[1 2 3 7 5 6]。
問題解析
你心算得到的答案是這樣嗎?讓我們一起來復現一下這個計算過程。
當for語句被執行的時候,在range關鍵字右邊的numbers1會先被求值。
這個位置上的代碼被稱為range表達式。range表達式的結果值可以是數組、數組的指針、切片、字符串、字典或者允許接收操作的通道中的某一個,並且結果值只能有一個。
對於不同種類的range表達式結果值,for語句的迭代變量的數量可以有所不同。
就拿我們這里的numbers1來說,它是一個切片,那么迭代變量就可以有兩個,右邊的迭代變量代表當次迭代對應的某一個元素值,而左邊的迭代變量則代表該元素值在切片中的索引值。
那么,如果像本題代碼中的for語句那樣,只有一個迭代變量的情況意味着什么呢?這意味着,該迭代變量只會代表當次迭代對應的元素值的索引值。
更寬泛地講,當只有一個迭代變量的時候,數組、數組的指針、切片和字符串的元素值都是無處安放的,我們只能拿到按照從小到大順序給出的一個個索引值。
因此,這里的迭代變量i的值會依次是從0到5的整數。當i的值等於3的時候,與之對應的是切片中的第 4 個元素值4。對4和3進行按位或操作得到的結果是7。這就是答案中的第 4 個整數是7的原因了。
現在,我稍稍修改一下上面的代碼。我們再來估算一下打印內容。
numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 {
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2)
注意,我把迭代的對象換成了numbers2。numbers2中的元素值同樣是從1到6的 6 個整數,並且元素類型同樣是int,但它是一個數組而不是一個切片。
在for語句中,我總是會對緊挨在當次迭代對應的元素后邊的那個元素,進行重新賦值,新的值會是這兩個元素的值之和。當迭代到最后一個元素時,我會把此range表達式結果值中的第一個元素值,替換為它的原值與最后一個元素值的和,最后,我會打印出numbers2的值。
對於這段代碼,我的問題依舊是:打印的內容會是什么?你可以先思考一下。
好了,我要公布答案了。打印的內容會是[7 3 5 7 9 11]。我先來重現一下計算過程。當for語句被執行的時候,在range關鍵字右邊的numbers2會先被求值。
這里需要注意兩點:
1、range表達式只會在for語句開始執行時被求值一次,無論后邊會有多少次迭代;
2、range表達式的求值結果會被復制,也就是說,被迭代的對象是range表達式結果值的副本而不是原值。
基於這兩個規則,我們接着往下看。在第一次迭代時,我改變的是numbers2的第二個元素的值,新值為3,也就是1和2之和。
但是,被迭代的對象的第二個元素卻沒有任何改變,畢竟它與numbers2已經是毫不相關的兩個數組了。因此,在第二次迭代時,我會把numbers2的第三個元素的值修改為5,即被迭代對象的第二個元素值2和第三個元素值3的和。
以此類推,之后的numbers2的元素值依次會是7、9和11。當迭代到最后一個元素時,我會把numbers2的第一個元素的值修改為1和6之和。
好了,現在該你操刀了。你需要把numbers2的值由一個數組改成一個切片,其中的元素值都不要變。為了避免混淆,你還要把這個切片值賦給變量numbers3,並且把后邊代碼中所有的numbers2都改為numbers3。
問題是不變的,執行這段修改版的代碼后打印的內容會是什么呢?如果你實在估算不出來,可以先實際執行一下,然后再嘗試解釋看到的答案。提示一下,切片與數組是不同的,前者是引用類型的,而后者是值類型的。
我們可以先接着討論后邊的內容,但是我強烈建議你一定要回來,再看看我留給你的這個問題,認真地思考和計算一下。
知識擴展
問題 1:switch語句中的switch表達式和case表達式之間有着怎樣的聯系?
先來看一段代碼。
value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch 1 + 3 {
case value1[0], value1[1]:
fmt.Println("0 or 1")
case value1[2], value1[3]:
fmt.Println("2 or 3")
case value1[4], value1[5], value1[6]:
fmt.Println("4 or 5 or 6")
}
我先聲明了一個數組類型的變量value1,該變量的元素類型是int8。在后邊的switch語句中,被夾在switch關鍵字和左花括號{之間的是1 + 3,這個位置上的代碼被稱為switch表達式。這個switch語句還包含了三個case子句,而每個case子句又各包含了一個case表達式和一條打印語句。
所謂的case表達式一般由case關鍵字和一個表達式列表組成,表達式列表中的多個表達式之間需要有英文逗號,分割,比如,上面代碼中的case value1[0], value1[1]就是一個case表達式,其中的兩個子表達式都是由索引表達式表示的。
另外的兩個case表達式分別是case value1[2], value1[3]和case value1[4], value1[5], value1[6]。
此外,在這里的每個case子句中的那些打印語句,會分別打印出不同的內容,這些內容用於表示case子句被選中的原因,比如,打印內容0 or 1表示當前case子句被選中是因為switch表達式的結果值等於0或1中的某一個。另外兩條打印語句會分別打印出2 or 3和4 or 5 or 6。
現在問題來了,擁有這樣三個case表達式的switch語句可以成功通過編譯嗎?如果不可以,原因是什么?如果可以,那么該switch語句被執行后會打印出什么內容。
我剛才說過,只要switch表達式的結果值與某個case表達式中的任意一個子表達式的結果值相等,該case表達式所屬的case子句就會被選中。
並且,一旦某個case子句被選中,其中的附帶在case表達式后邊的那些語句就會被執行。與此同時,其他的所有case子句都會被忽略。
當然了,如果被選中的case子句附帶的語句列表中包含了fallthrough語句,那么緊挨在它下邊的那個case子句附帶的語句也會被執行。
正因為存在上述判斷相等的操作(以下簡稱判等操作),switch語句對switch表達式的結果類型,以及各個case表達式中子表達式的結果類型都是有要求的。畢竟,在 Go 語言中,只有類型相同的值之間才有可能被允許進行判等操作。
如果switch表達式的結果值是無類型的常量,比如1 + 3的求值結果就是無類型的常量4,那么這個常量會被自動地轉換為此種常量的默認類型的值,比如整數4的默認類型是int,又比如浮點數3.14的默認類型是float64。
因此,由於上述代碼中的switch表達式的結果類型是int,而那些case表達式中子表達式的結果類型卻是int8,它們的類型並不相同,所以這條switch語句是無法通過編譯的。
再來看一段很類似的代碼:
value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value2[4] {
case 0, 1:
fmt.Println("0 or 1")
case 2, 3:
fmt.Println("2 or 3")
case 4, 5, 6:
fmt.Println("4 or 5 or 6")
}
其中的變量value2與value1的值是完全相同的。但不同的是,我把switch表達式換成了value2[4],並把下邊那三個case表達式分別換為了case 0, 1、case 2, 3和case 4, 5, 6。
如此一來,switch表達式的結果值是int8類型的,而那些case表達式中子表達式的結果值卻是無類型的常量了。這與之前的情況恰恰相反。那么,這樣的switch語句可以通過編譯嗎?
答案是肯定的。因為,如果case表達式中子表達式的結果值是無類型的常量,那么它的類型會被自動地轉換為switch表達式的結果類型,又由於上述那幾個整數都可以被轉換為int8類型的值,所以對這些表達式的結果值進行判等操作是沒有問題的。
當然了,如果這里說的自動轉換沒能成功,那么switch語句照樣通不過編譯。
通過上面這兩道題,你應該可以搞清楚switch表達式和case表達式之間的聯系了。由於需要進行判等操作,所以前者和后者中的子表達式的結果類型需要相同。
switch語句會進行有限的類型轉換,但肯定不能保證這種轉換可以統一它們的類型。還要注意,如果這些表達式的結果類型有某個接口類型,那么一定要小心檢查它們的動態值是否都具有可比性(或者說是否允許判等操作)。
因為,如果答案是否定的,雖然不會造成編譯錯誤,但是后果會更加嚴重:引發 panic(也就是運行時恐慌)。
問題 2:switch語句對它的case表達式有哪些約束?
我在上一個問題的闡述中還重點表達了一點,不知你注意到了沒有,那就是:switch語句在case子句的選擇上是具有唯一性的。
正因為如此,switch語句不允許case表達式中的子表達式結果值存在相等的情況,不論這些結果值相等的子表達式,是否存在於不同的case表達式中,都會是這樣的結果。具體請看這段代碼:
value3 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value3[4] {
case 0, 1, 2:
fmt.Println("0 or 1 or 2")
case 2, 3, 4:
fmt.Println("2 or 3 or 4")
case 4, 5, 6:
fmt.Println("4 or 5 or 6")
}
變量value3的值同value1,依然是由從0到6的 7 個整數組成的數組,元素類型是int8。switch表達式是value3[4],三個case表達式分別是case 0, 1, 2、case 2, 3, 4和case 4, 5, 6。
由於在這三個case表達式中存在結果值相等的子表達式,所以這個switch語句無法通過編譯。不過,好在這個約束本身還有個約束,那就是只針對結果值為常量的子表達式。
比如,子表達式1+1和2不能同時出現,1+3和4也不能同時出現。有了這個約束的約束,我們就可以想辦法繞過這個對子表達式的限制了。再看一段代碼:
value5 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value5[4] {
case value5[0], value5[1], value5[2]:
fmt.Println("0 or 1 or 2")
case value5[2], value5[3], value5[4]:
fmt.Println("2 or 3 or 4")
case value5[4], value5[5], value5[6]:
fmt.Println("4 or 5 or 6")
}
變量名換成了value5,但這不是重點。重點是,我把case表達式中的常量都換成了諸如value5[0]這樣的索引表達式。
雖然第一個case表達式和第二個case表達式都包含了value5[2],並且第二個case表達式和第三個case表達式都包含了value5[4],但這已經不是問題了。這條switch語句可以成功通過編譯。
不過,這種繞過方式對用於類型判斷的switch語句(以下簡稱為類型switch語句)就無效了。因為類型switch語句中的case表達式的子表達式,都必須直接由類型字面量表示,而無法通過間接的方式表示。代碼如下:
value6 := interface{}(byte(127))
switch t := value6.(type) {
case uint8, uint16:
fmt.Println("uint8 or uint16")
case byte:
fmt.Printf("byte")
default:
fmt.Printf("unsupported type: %T", t)
}
變量value6的值是空接口類型的。該值包裝了一個byte類型的值127。我在后面使用類型switch語句來判斷value6的實際類型,並打印相應的內容。
這里有兩個普通的case子句,還有一個default case子句。前者的case表達式分別是case uint8, uint16和case byte。你還記得嗎?byte類型是uint8類型的別名類型。
因此,它們兩個本質上是同一個類型,只是類型名稱不同罷了。在這種情況下,這個類型switch語句是無法通過編譯的,因為子表達式byte和uint8重復了。好了,以上說的就是case表達式的約束以及繞過方式,你學會了嗎。
總結
我們今天主要討論了for語句和switch語句,不過我並沒有說明那些語法規則,因為它們太簡單了。我們需要多加注意的往往是那些隱藏在 Go 語言規范和最佳實踐里的細節。
這些細節其實就是我們很多技術初學者所謂的“坑”。比如,我在講for語句的時候交代了攜帶range子句時只有一個迭代變量意味着什么。你必須知道在迭代數組或切片時只有一個迭代變量的話是無法迭代出其中的元素值的,否則你的程序可能就不會像你預期的那樣運行了。
筆記源碼
https://github.com/MingsonZheng/go-core-demo
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。