前往
大廳
主題 達人專欄

Go 語言的陣列,搭配迴圈輕鬆省事

解凍豬腳 | 2021-10-27 19:15:01 | 巴幣 4276 | 人氣 1616

 
本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版

說到利用電腦程式來把處理資料的流程自動化,通常都會遇上要處理很多相同性質的資料,那自然是少不了陣列(array)的蹤影。

比如說,我們現在有十個數:
4, 16, 17, 23, 28, 32, 47, 129, 231, 477

想要把這裡面的質數挑出來,自然是先把這些數裝到陣列裡面,同時寫個 isPrime() 函式來負責判斷一個數是不是質數,然後把整個陣列 run 過一次,一個一個丟進去檢查。

這時候問題就來了:在 Golang 裡面,我們要怎麼使用陣列?


► Golang 的陣列(array)

在 C 語言裡面,我們知道 array 就相當於一次宣告好固定數量的多個變數,例如:

int myArray[5] = {4, 6, 8, 10, 12};
for (int i=0; i<5; i++) {
    printf("myArray[%d] = %d\n", i, myArray[i]);
}

在 Golang 裡面也是一樣,只是 Go 語言的陣列是把數量和資料型態寫在一起:

var myArray = [5]int{4, 6, 8, 10, 12}
for i := 0; i < 5; i++ {
    fmt.Printf("myArray[%d] = %d\n", i, myArray[i])
}

如果我們賦值的時候懶得去細數元素數量呢?我們可以這麼做:

var myArray = [...]int{4, 6, 8, 10, 12}

宣告變數的時候使用 […],編譯器就會自動幫你判斷出是 [5]int 型態的 array 了,因為你在賦值的時候填了五個數值進去。如果今天我們不想對陣列本身賦值,但想先指定好大小,我們有兩種寫法:

var myArray [5]int

var myArray = [5]int{}

這兩種寫法造成的結果相同,myArray 的內容都會自動被設為 {0, 0, 0, 0, 0},只是前者的做法是「宣告一個 [5]int 型態的變數,但不去指定內容」,後者的做法則是「宣告一個變數,然後創造一個 [5]int 型態的空 array,再賦值進這個變數」

所以,[5]int 表示的是「型態」,而 [5]int{} 表示的是「一個 array 的實體」,加上大括號意義是不一樣的,千萬不要搞混了。

就像剛才提到的,Golang 是把元素數量和元素的型態綁在一起,所以 [5]int 和 [8]int 是無法直接互通的東西。如果你嘗試這麼做:

func main() {
    var myArray = [...]int{4, 6, 8, 10, 12}
    printArrayContent(myArray)
}

func printArrayContent(myArray [8]int) {
    for i := 0; i < 8; i++ {
        fmt.Println(i, myArray[i])
    }
}

你會得到這樣的錯誤訊息:
cannot use myArray (variable of type [5]int) as [8]int value in argument to printArrayContent

簡單來說就是「不能把一個 [5]int 型態的變數直接拿來當成 [8]int 型態的變數使用」。

除此之外,只要你宣告了它是 [5]int,那它就永遠不能夠存放超過五個元素,你也不能試圖存取 myArray[6],否則會產生 index out of range 的錯誤,導致程式進入 panic。

這麼說來,如果我們只使用 array 的話,其實非常不方便——特別是當我們無法確定陣列裡究竟有多少元素的時候。


► Golang 的切片(slice)

於是切片(slice)誕生了。顧名思義,它就是從 array 切下來的片段,而且長度是動態的,不受限於元素數量。我們用白話來說,[]int、[5]int、[8]int 都是不一樣的東西。

因為使用固定長度的 array 來處理資料很不方便,所以實務上我們多會直接用 []int 來處理。從 Golang 的底層來看,其實 slice 的背後仍然是 array,只是比 array 還要多實現了更多的功能,是一種 array 的延伸。

如果想要產生一個 slice,你可以直接宣告而成,只要中括號裡面不填任何東西就好了:

mySlice := []int{37, 43, 57, 66, 99, 125}

也可以從 array 切出一段 slice:

myArray := [...]int{37, 43, 57, 66, 99, 125}
mySlice := myArray[2:5] // get a []int instead of [3]int
fmt.Println(mySlice) // -> [57 66 99]


一組含有 6 個元素的 array(或 slice),切割的時候我們的 index 從 0 開始算起,一直到 6 結束。也因此,我們求 myArray[2:5] 會得到一個 []int{57, 66, 99},當然你也可以省略尾端或起點:

myArray := [...]int{37, 43, 57, 66, 99, 125}
mySlice := myArray[3:] // same as myArray[3:6]
fmt.Println(mySlice) // -> [66 99 125]

myArray := [...]int{37, 43, 57, 66, 99, 125}
mySlice := myArray[:2] // same as myArray[0:2]
fmt.Println(mySlice) // -> [37 43]

如果起點跟終點都省略了,那就相當於是從頭到尾都完整地複製一次,一樣會是得到一個 slice,所以我們如果有一個 array 叫做 myArray,只要用 myArray[:] 就可以把這個 myArray 轉換成一個 slice:

myArray := [...]int{37, 43, 57, 66, 99, 125}
fmt.Println(myArray[:]) // get a []int instead of [6]int

當然你也可以從 slice 切出 slice 來:

mySlice := []int{37, 43, 57, 66, 99, 125}
fmt.Println(mySlice[2:5]) // -> [57 66 99]

如果你手上有一個 string,一樣也可以利用上面這種 [start:end] 的句法來切割它,切出來的東西仍然會是一個 string:

myString := "ABCDEFGHIJKLMN"
fmt.Println(myString[3:7]) // -> DEFG


► 操作切片的元素增減

一般我們會直接使用 append() 來產生一個新的 slice,把舊的 slice 和新的元素拼接而成:

mySlice := []int{555, 666, 777}
mySlice = append(mySlice, 8899)
fmt.Println(mySlice) // -> [555 666 777 8899]

因為 append 函式的自變數數量是浮動的,所以在 slice 後面緊跟的元素可以只填一個,也可以兩個以上:

mySlice := []int{555, 666, 777}
mySlice = append(mySlice, 8899, 7788, 5566)
fmt.Println(mySlice) // -> [555 666 777 8899 7788 5566]

要注意,append 函式僅限於「slice 和元素的拼接」,如果你想要把兩個 slice 拼接起來,第二個 slice 就必須使用 … 的後綴符號來展開:

mySliceA := []int{555, 666, 777}
mySliceB := []int{8899, 7788, 5566}
mySliceA = append(mySliceA, mySliceB...)
fmt.Println(mySliceA) // -> [555 666 777 8899 7788 5566]

如果要從一個 slice 當中剔除 index=2(也就是第三個)的元素,我們可以分成兩個切片再拼接起來:

mySlice := []int{55, 66, 77, 88, 99}
mySlice = append(mySlice[:2], mySlice[3:]...)
fmt.Println(mySlice) // -> [55 66 88 99]


► 陣列或切片的迭代

瞭解 array 和 slice 的差異之後,接下來最重要的當屬迭代(iteration)。當我們想要用迭代的方式巡完整個陣列或切片,一般的做法是先取得元素數量,然後用 for 迴圈去處理它,例如:

mySlice := []int{55, 66, 77, 88, 99}
// you can get the length of slice with len()
for i := 0; i < len(mySlice); i++ {
    fmt.Println(mySlice[i])
}

利用一般的 for 迴圈可以達到效果(就像在 C 語言一樣),但有的場合我們希望寫出來的程式碼更接近 for-each 的語意,並不在乎這個 slice 到底裝了確切多少個元素,那就會直接使用 range 關鍵字來針對整個 slice 循環一次:

mySlice := []int{55, 66, 77, 88, 99}
for i := range mySlice {
    fmt.Println(mySlice[i])
}

這迴圈裡的 i 就會依序是 0, 1, 2, 3, 4。如果同時用兩個變數來接收 range 每次回傳的值,則會收到 index 和 value:

mySlice := []int{55, 66, 77, 88, 99}
for i, v := range mySlice {
    fmt.Println(i, v) // try it!
}

有的場合我們只需要 value 而不需要 index,那就利用底線把回傳的 index 拋掉:

mySlice := []int{55, 66, 77, 88, 99}
for _, v := range mySlice {
    fmt.Println(v)
}

能宣告、能切割、能增減、能遍歷,這些基本上就是 slice 的常用功能了。


► slice 只能裝基本型態嗎?

當然不是。無論是基本型態、結構體或者是空介面(interface{})都是支援 slice 的。除此之外,slice 也可以裝 slice,也就是平常在其他語言的多維陣列。

既然 []int 可以放很多個 int,那麼 [][]int 就可以放很多個 []int 了:

mySlice2D := [][]int{
    []int{1, 2, 3, 4, 5},
    []int{2, 4, 6, 8, 10},
    []int{3, 6, 9, 12, 15},
}
for i := range mySlice2D {
    for j := range mySlice2D[i] {
        fmt.Printf("%d,", mySlice2D[i][j])
    }
    fmt.Println("----")
}

因為 [][]int 裡面裝的一定都會是 []int,所以我們可以把裡面的 []int 省略,而 range 的環節同樣也可以用 value 的方式來接收:

mySlice2D := [][]int{
    {1, 2, 3, 4, 5},
    {2, 4, 6, 8, 10},
    {3, 6, 9, 12, 15},
}
for _, s := range mySlice2D {
    for _, v := range s {
        fmt.Printf("%d,", v)
    }
    fmt.Println("----")
}

至於結構體和空介面的 slice,就留待下次講結構體主題的時候再一起說吧。


► array 和 slice 用來傳值時產生的差異

這兩者的差異,不只體現在動態或靜態的元素數量。我們前面曾經提到,slice 本身就是一種 array 的延伸,其實 slice 的結構包含了一個指向底層 array 的指標(只是它封裝得很好,讓你覺得 slice 是一種獨立於 array 的東西)。

如果我們把 slice 傳到函式裡面,那就相當於把底層 array 的指標傳進去,也因此如果把 slice 傳進去修改數值,我們會發現這個更動在函式之外也是有效的:

func main() {
    mySlice := []int{1, 2, 3, 4, 5}
    fmt.Println(mySlice) // -> [1 2 3 4 5]
    changeValue(mySlice)
    fmt.Println(mySlice) // -> [1 2 99 4 5]
}

func changeValue(s []int) {
    if len(s) < 3 {
        return
    }
    s[2] = 999
}

而當我們傳遞給函式的是 array 而非 slice 時,Golang 會直接把整個 array 複製一份到函式裡面,即使在函式裡修改了 array,原先外面的 array 也不會受影響:

func main() {
    mySlice := [5]int{1, 2, 3, 4, 5}
    fmt.Println(mySlice) // -> [1 2 3 4 5]
    changeValue(mySlice)
    fmt.Println(mySlice) // -> [1 2 3 4 5]
}

func changeValue(s [5]int) {
    s[2] = 999
}

這就是 slice 跟 array 的差別了。原則上我們平常使用還是以 slice 為主,至少我平常在寫 Go 的時候幾乎沒用過固定長度的 array 就是。有的時候我們可能會把 slice 作為參數傳遞給函式使用,就要特別注意 slice 傳進去的會是指標,以避免遇到預期之外的 bug。

唉,slice 這麼好用,我真的不會想寫 C 了。




縮圖素材原作者:Renée French(CC BY-SA 3.0)
送禮物贊助創作者 !
87
留言

創作回應

小紅帽向創作者進行贊助 ✦
2021-10-27 19:28:50
解凍豬腳
帽帽…什麼時候和我一起辦理登記
2021-10-27 19:30:57
宇宙吃貨胖宅貓
一頭霧水,不明覺厲
2021-10-27 20:12:19
解凍豬腳
沒關係,離開之前你可以在這留下美味的料理 [e16]
2021-10-29 10:48:18
勳章向創作者進行贊助 ✦
2021-10-28 10:59:50
解凍豬腳
感謝抖內!
2021-10-29 11:44:32

更多創作