學習筆記-數組和切片

一、數組(array)類型和切片(slice)類型 區別是什麼?

數組類型的值(如下簡稱數組)的長度是固定的,而切片類型的值(如下簡稱切片)是可變長的。編程

數組的長度在聲明它的時候就必須給定,而且以後不會再改變。能夠說,數組的長度是其類型的一部分。好比,[1]string和[2]string就是兩個不一樣的數組類型。而切片的類型字面量中只有元素的類型,而沒有長度。切片的長度能夠自動地隨着其中元素數量的增加而增加,但不會隨着元素數量的減小而減少。
學習筆記-數組和切片數組

其實能夠把切片看作是對數組的一層簡單的封裝,由於在每一個切片的底層數據結構中,必定會包含一個數組。數組能夠被叫作切片的底層數組,而切片也能夠被看做是對數組的某個連續片斷的引用。數據結構

Go 語言的切片類型屬於引用類型,同屬引用類型的還有字典類型、通道類型、函數類型等;而 Go 語言的數組類型則屬於值類型,同屬值類型的有基礎數據類型以及結構體類型。app

Go 語言裏不存在像 Java 等編程語言中使人困惑的「傳值或傳引用」問題。在 Go 語言中,咱們判斷所謂的「傳值」或者「傳引用」只要看被傳遞的值的類型就行了。若是傳遞的值是引用類型的,那麼就是「傳引用」。若是傳遞的值是值類型的,那麼就是「傳值」。從傳遞成本的角度講,引用類型的值每每要比值類型的值低不少。編程語言

咱們在數組和切片之上均可以應用索引表達式,獲得的都會是某個元素。咱們在它們之上也均可以應用切片表達式,也都會獲得一個新的切片。ide

調用內建函數len,獲得數組和切片的長度。經過調用內建函數cap,咱們能夠獲得它們的容量。但要注意,數組的容量永遠等於其長度,都是不可變的。切片的容量卻不是這樣,而且它的變化是有規律可尋的。函數

二、怎樣正確估算切片的長度和容量?

package main

import "fmt"

func main() {
    // 示例1。
    s1 := make([]int, 5)
    fmt.Printf("The length of s1: %d\n", len(s1))
    fmt.Printf("The capacity of s1: %d\n", cap(s1))
    fmt.Printf("The value of s1: %d\n", s1)
    s2 := make([]int, 5, 8)
    fmt.Printf("The length of s2: %d\n", len(s2))
    fmt.Printf("The capacity of s2: %d\n", cap(s2))
    fmt.Printf("The value of s2: %d\n", s2)
    fmt.Println()
go run demo15.go 
The length of s1: 5
The capacity of s1: 5
The value of s1: [0 0 0 0 0]
The length of s2: 5
The capacity of s2: 8
The value of s2: [0 0 0 0 0]

我用內建函數make聲明瞭一個[]int類型的變量s1。我傳給make函數的第二個參數是5,從而指明瞭該切片的長度。我用幾乎一樣的方式聲明瞭切片s2,只不過多傳入了一個參數8以指明該切片的容量。如今,具體的問題是:切片s1和s2的容量都是多少?學習

這道題的典型回答:切片s1和s2的容量分別是5和8。
s1的容量爲何是5呢?由於我在聲明s1的時候把它的長度設置成了5。當咱們用make函數初始化切片時,若是不指明其容量,那麼它就會和長度一致。若是在初始化時指明瞭容量,那麼切片的實際容量也就是它了。這也正是s2的容量是8的緣由。code

過s2再來明確下長度、容量以及它們的關係。我在初始化s2表明的切片時,同時也指定了它的長度和容量。我在剛纔說過,能夠把切片看作是對數組的一層簡單的封裝,由於在每一個切片的底層數據結構中,必定會包含一個數組。數組能夠被叫作切片的底層數組,而切片也能夠被看做是對數組的某個連續片斷的引用。在這種狀況下,切片的容量實際上表明瞭它的底層數組的長度,這裏是8。(注意,切片的底層數組等同於咱們前面講到的數組,其長度不可變。)blog

有一個窗口,你能夠經過這個窗口看到一個數組,可是不必定能看到該數組中的全部元素,有時候只能看到連續的一部分元素。

這個數組就是切片s2的底層數組,而這個窗口就是切片s2自己。s2的長度實際上指明的就是這個窗口的寬度,決定了你透過s2,能夠看到其底層數組中的哪幾個連續的元素。因爲s2的長度是5,因此你能夠看到底層數組中的第 1 個元素到第 5 個元素,對應的底層數組的索引範圍是 [0, 4]。切片表明的窗口也會被劃分紅一個一個的小格子,就像咱們家裏的窗戶那樣。每一個小格子都對應着其底層數組中的某一個元素。
s2爲例,這個窗口最左邊的那個小格子對應的正好是其底層數組中的第一個元素,即索引爲0的那個元素。所以能夠說,s2中的索引從0到4所指向的元素偏偏就是其底層數組中索引從0到4表明的那 5 個元素。
咱們用make函數或切片值字面量(好比[]int{1, 2, 3})初始化一個切片時,該窗口最左邊的那個小格子老是會對應其底層數組中的第 1 個元素。

package main

import "fmt"

func main() {

    // 示例2。
    s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
    s4 := s3[3:6]
    fmt.Printf("The length of s4: %d\n", len(s4))
    fmt.Printf("The capacity of s4: %d\n", cap(s4))
    fmt.Printf("The value of s4: %d\n", s4)
    fmt.Println()
go run demo15.go 
The length of s4: 3
The capacity of s4: 5
The value of s4: [4 5 6]

學習筆記-數組和切片

切片s3中有 8 個元素,分別是從1到8的整數。s3的長度和容量都是8。而後,我用切片表達式s3[3:6]初始化了切片s4。問題是,這個s4的長度和容量分別是多少?

切片表達式中的方括號裏的那兩個整數[3:6]都表明什麼?

[3:6]要表達的就是透過新窗口能看到的s3中元素的索引範圍是從3到5(注意,不包括6)。這裏的3可被稱爲起始索引,6可被稱爲結束索引。那麼s4的長度就是6減去3,即3。所以能夠說,s4中的索引從0到2指向的元素對應的是s3及其底層數組中索引從3到5的那 3 個元素。

再來看容量。我在前面說過,切片的容量表明瞭它的底層數組的長度,但這僅限於使用make函數或者切片值字面量初始化切片的狀況。

更通用的規則是:一個切片的容量能夠被看做是透過這個窗口最多能夠看到的底層數組中元素的個數。

因爲s4是經過在s3上施加切片操做得來的,因此s3的底層數組就是s4的底層數組。又由於,在底層數組不變的狀況下,切片表明的窗口能夠向右擴展,直至其底層數組的末尾。因此,s4的容量就是其底層數組的長度8, 減去上述切片表達式中的那個起始索引3,即5。切片表明的窗口是沒法向左擴展的。也就是說,咱們永遠沒法透過s4看到s3中最左邊的那 3 個元素。

順便提一下把切片的窗口向右擴展到最大的方法。對於s4來講,切片表達式s4[0:cap(s4)]就能夠作到。我想你應該能看懂。該表達式的結果值(即一個新的切片)會是[]int{4, 5, 6, 7, 8},其長度和容量都是5。

三、怎樣估算切片容量的增加?

一旦一個切片沒法容納更多的元素,Go 語言就會想辦法擴容。但它並不會改變原來的切片,而是會生成一個容量更大的切片,而後將把原有的元素和新元素一併拷貝到新切片中。在通常的狀況下,你能夠簡單地認爲新切片的容量(如下簡稱新容量)將會是原切片容量(如下簡稱原容量)的 2 倍。

可是,當原切片的長度(如下簡稱原長度)大於或等於1024時,Go 語言將會以原容量的1.25倍做爲新容量的基準(如下新容量基準)。新容量基準會被調整(不斷地與1.25相乘),直到結果不小於原長度與要追加的元素數量之和(如下簡稱新長度)。最終,新容量每每會比新長度大一些,固然,相等也是可能的。

另外,若是咱們一次追加的元素過多,以致於使新長度比原容量的 2 倍還要大,那麼新容量就會以新長度爲基準。注意,與前面那種狀況同樣,最終的新容量在不少時候都要比新容量基準更大一些。更多細節可參見runtime包中 slice.go 文件裏的growslice及相關函數的具體實現。

package main

import "fmt"

func main() {
    // 示例1。
    s6 := make([]int, 0)
    fmt.Printf("The capacity of s6: %d\n", cap(s6))
    for i := 1; i <= 5; i++ {
        s6 = append(s6, i)
        fmt.Printf("s6(%d): len: %d, cap: %d\n", i, len(s6), cap(s6))
    }
    fmt.Println()

    // 示例2。
    s7 := make([]int, 1024)
    fmt.Printf("The capacity of s7: %d\n", cap(s7))
    s7e1 := append(s7, make([]int, 200)...)
    fmt.Printf("s7e1: len: %d, cap: %d\n", len(s7e1), cap(s7e1))
    s7e2 := append(s7, make([]int, 400)...)
    fmt.Printf("s7e2: len: %d, cap: %d\n", len(s7e2), cap(s7e2))
    s7e3 := append(s7, make([]int, 600)...)
    fmt.Printf("s7e3: len: %d, cap: %d\n", len(s7e3), cap(s7e3))
    fmt.Println()

    // 示例3。
    s8 := make([]int, 10)
    fmt.Printf("The capacity of s8: %d\n", cap(s8))
    s8a := append(s8, make([]int, 11)...)
    fmt.Printf("s8a: len: %d, cap: %d\n", len(s8a), cap(s8a))
    s8b := append(s8a, make([]int, 23)...)
    fmt.Printf("s8b: len: %d, cap: %d\n", len(s8b), cap(s8b))
    s8c := append(s8b, make([]int, 45)...)
    fmt.Printf("s8c: len: %d, cap: %d\n", len(s8c), cap(s8c))
}
go run demo16.go 
The capacity of s6: 0
s6(1): len: 1, cap: 1
s6(2): len: 2, cap: 2
s6(3): len: 3, cap: 4
s6(4): len: 4, cap: 4
s6(5): len: 5, cap: 8

The capacity of s7: 1024
s7e1: len: 1224, cap: 1280
s7e2: len: 1424, cap: 1696
s7e3: len: 1624, cap: 2048

The capacity of s8: 10
s8a: len: 21, cap: 22
s8b: len: 44, cap: 44
s8c: len: 89, cap: 96

四、切片的底層數組何時會被替換?

切地說,一個切片的底層數組永遠不會被替換。爲何?雖然在擴容的時候 Go 語言必定會生成新的底層數組,可是它也同時生成了新的切片。

它只是把新的切片做爲了新底層數組的窗口,而沒有對原切片,及其底層數組作任何改動。

請記住,在無需擴容時,append函數返回的是指向原底層數組的新切片,而在須要擴容時,append函數返回的是指向新底層數組的新切片。因此,嚴格來說,「擴容」這個詞用在這裏雖然形象但並不合適。不過鑑於這種稱呼已經用得很普遍了,咱們也不必另找新詞了。

順便說一下,只要新長度不會超過切片的原容量,那麼使用append函數對其追加元素的時候就不會引發擴容。這隻會使緊鄰切片窗口右邊的(底層數組中的)元素被新的元素替換掉。你能夠運行 demo17.go 文件以加強對這些知識的理解。

相關文章
相關標籤/搜索