練習 - 探索配量

已完成

我們已在上一節中探索陣列,並了解陣列是配量和對應的基礎。 您馬上就會了解其原因。 就像陣列一樣,配量是 Go 中的資料類型,其代表相同類型元素的序列。 但是,其與陣列之間更顯著的差異在於配量大小是動態的,而不是固定的。

配量是位於陣列或另一個配量之上的資料結構。 我們會以「基礎陣列」的名稱來稱呼原始的陣列或配量。 使用配量時,您可以存取整個基礎陣列,或是只存取元素的子序列。

一個配量只有三個元件:

  • 針對基礎陣列第一個可連線元素的指標。 這個元素並不一定是陣列的第一個元素 array[0]
  • 配量的長度。 配量中的元素數目。
  • 配量的容量。 配量開頭和基礎陣列結尾之間的元素數目。

下圖會說明配量:

顯示配量在 Go 中外觀的圖表。

請注意,配量只是基礎陣列的子集。 讓我們看看如何在程式碼中呈現上圖。

針對配量進行宣告及初始化

若要宣告配量,您會利用與宣告陣列相同的方式來進行。 例如,下列程式碼代表您在配量影像中看到的內容:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}

執行程式碼時,您會看到下列輸出:

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

請注意,目前配量與陣列之間的差異並不大。 您可以用相同方式來宣告這兩者。 若要從配量取得資訊,您可以使用內建函式 len()cap()。 我們將繼續使用這些函式,來確認配量可以有來自基礎陣列的元素子序列。

配量項目

Go 支援配量運算子 s[i:p],其中:

  • s 代表陣列。
  • i 代表針對要新增至新配量之基礎陣列 (或另一個配量) 中第一個元素的指標。 變數 i 會對應到陣列 array[i] 中位於索引位置 i 的元素。 請記住,此元素並不一定是基礎陣列的第一個元素 array[0]
  • p 代表建立新配量時要使用之基礎陣列中的元素數目,以及元素位置。 變數 p 會對應到基礎陣列中的最後一個元素,其可在新配量中使用。 基礎陣列中位於 p 的元素可在 array[i+1] 的位置上找到。 請注意,此元素不一定是基礎陣列的最後一個元素 array[len(array)-1]

因此,配量只能參考元素的子集。

讓我們假設您想要使用四個變數來個別代表一年的每一季,而且您有一個具有 12 個元素的配量 months。 下圖說明如何將配量 months 分成四個新的 quarter 配量:

顯示多個配量在 Go 中外觀的圖表。

若要以程式碼表示您在上圖中看到的內容,您可以使用下列程式碼:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}

當您執行程式碼時,會取得下列輸出:

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

請注意,配量的長度相同,但容量不同。 讓我們探索 quarter2 配量。 當您宣告此配量時,便表示您希望配量從位置編號三開始,而最後一個元素則位於位置編號六。 配量的長度為三個元素,但容量為九個,因為基礎陣列有更多可用的元素或位置,但配量看不到。 例如,若您嘗試列印類似 fmt.Println(quarter2[3]) 的內容,您將會收到下列錯誤:panic: runtime error: index out of range [3] with length 3

配量的容量只會告訴您可以擴充配量的程度。 基於這個原因,您可以從 quarter2 建立擴充的配量,如下列範例所示:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}

當您執行上述程式碼時,會得到如下的輸出:

[April May June] 3 9
[April May June July] 4 9

請注意,當您宣告 quarter2Extended 變數時,不需要指定初始位置 ([:4])。 如果您那麼做,Go 會假設您想要配量的第一個位置。 您可以針對最後一個位置 ([1:]) 執行相同動作。 Go 假設您想要參考直到配量的最新位置 (len()-1) 為止的所有元素。

附加項目

我們已探索配量的運作方式,以及其與陣列的相似之處。 現在讓我們探索其與陣列有何差異。 第一個差異是配量的大小不是固定的,而是動態的。 建立配量之後,您可以在其中新增更多元素,而且配量將會擴充。 您很快就會看到基礎陣列會發生什麼事。

為了將元素新增至配量,Go 提供了 append(slice, element) 內建函式。 您會將要修改的配量及要附加為值的元素傳遞給該函式。 然後,append 函式會傳回您存放在變數中的新配量。 其可以是您正在變更之配量的相同變數。

讓我們看看附加程序在程式碼中的樣子:

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}

當您執行上述程式碼時,應會看到下列輸出:

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]

這個輸出很有趣。 尤其是呼叫 cap() 函式所傳回的內容。 一切看起來都很正常,直到第三次反覆運算為止,其中容量變更為 4,而配量中只有三個元素。 在第五次反覆運算中,容量再次變化為 8,並在第九次變化為 16。

您是否注意到來自容量輸出的模式? 「當配量沒有足夠容量可容納更多元素時,Go 會將其容量加倍」。 其會以新的容量建立新的基礎陣列。 您不需要執行任何動作,系統便會自動增加容量。 Go 會自動加以執行。 您必須特別小心。 在某些情況下,配量的容量可能會比其所需的還多,而您將會浪費記憶體。

移除項目

您可能正在思考要如何移除元素? Go 並沒有內建函式可從配量中移除元素。 您可以使用我們先前涵蓋的配量運算子 s[i:p],只使用您需要的元素來建立新配量。

例如,下列程式碼會從配量中移除元素:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}

當您執行上述程式碼時,會得到如下的輸出:

Before [A B C D E] Remove  C
After [A B D E]

此程式碼會從配量中移除元素。 其會使用配量中的下一個元素來取代要移除的元素,或者,如果您要移除的是最後一個元素,則其不會取代任何元素。

另一種方法是建立新的配量複本。 我們將在下一節了解如何建立配量的複本。

建立配量的複本

Go 具有內建 copy(dst, src []Type) 函式,可用來建立配量的複本。 您會傳送目的地配量和來源配量。 例如,您可以建立配量的複本,如下列範例所示:

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

您為什麼需要建立複本? 當您變更配量中的元素時,也會變更基礎陣列。 參考相同基礎陣列的任何其他配量都會受到影響。 讓我們在程式碼中查看此程序,然後透過建立配量的複本來修正此問題。

使用下列程式碼來確認配量會指向某個陣列,而您在配量中所做的每個變更都會影響基礎陣列。

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

當您執行上述程式碼時,會看到下列輸出:

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

請注意我們對 slice1 所做的變更會如何影響 letters 陣列和 slice2。 您可以在輸出中看到字母 B 已由 Z 取代,而且其會影響指向 letters 陣列的每一個項目。

若要修正此問題,您必須建立配量複本,其會在幕後建立新的基礎陣列。 您可以使用以下程式碼:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

當您執行上述程式碼時,會看到下列輸出:

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

請注意 slice1 中的變更會對基礎陣列造成影響,但不會影響新的 slice2