diff --git a/SUMMARY.md b/SUMMARY.md index 75de8d4..334988a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -32,8 +32,8 @@ * [常量](ch3/ch3-06.md) * [複合數據類型](ch4/ch4.md) * [數組](ch4/ch4-01.md) - * [切片](ch4/ch4-02.md) - * [字典](ch4/ch4-03.md) + * [Slice](ch4/ch4-02.md) + * [Map](ch4/ch4-03.md) * [結構體](ch4/ch4-04.md) * [JSON](ch4/ch4-05.md) * [文本和HTML模闆](ch4/ch4-06.md) diff --git a/ch4/ch4-02.md b/ch4/ch4-02.md index f1ba38b..43c45e1 100644 --- a/ch4/ch4-02.md +++ b/ch4/ch4-02.md @@ -1,6 +1,132 @@ -## 4.2. 切片 +## 4.2. Slice -TODO +Slice(切片)代表變長的序列,序列中每個元素都有相同的類型。一個slice類型一般寫作[]T,其中T代表slice中元素的類型;語法和數組很像隻是沒有長度而已。 + +數組和slice之間有着緊密的聯繫。一個slice是一個輕量級的數據結果,提供了訪問數組子序列(或者全部)元素的功能,因爲slice的底層確實引用一個數組對象。一個slice有三個部分構成:指針、長度和容量。指針指向第一個元素對應的底層數組元素的地址,slice的第一個元素併不一定就是數組的第一個元素。長度對應slice中元素的數目;長度不能超過容量,容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分别返迴slice的長度和容量。 + +多個slice之間可以共享底層的數據,併且引用的數組部分區間可能重疊。圖4.1顯示了表示一年中每個月份名字的字符串數組,還有重疊引用了該數組的兩個slice。數組這樣定義 + +```Go +months := [...]string{1: "January", /* ... */, 12: "December"} +``` + +因此一月份是months[1],十二月份是months[12]。通常,數組的第一個元素從索引0開始,但是月份一般是從1開始的,因此我們聲明數組時直接第0個元素,第0個元素會被自動初始化爲空字符串。 + +slice的操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用於創建一個新的slice,引用s的從第i個元素開始到第j-1個元素的子序列。新的slice將隻有j-i個元素。如果i位置的索引被省略的話將使用0代替,如果j位置的索引被省略的話將使用len(s)代替。因此,months[1:13]切片操作將引用全部有效的月份,和months[1:]操作等價;months[:]切片操作則是引用整個數組。讓我們分别定義表示第二季度和北方夏天的slice,它們有重疊部分: + +![](../images/ch4-01.png) + +```Go +Q2 := months[4:7] +summer := months[6:9] +fmt.Println(Q2) // ["April" "May" "June"] +fmt.Println(summer) // ["June" "July" "August"] +``` + +兩個slice都包含了六月份,下面的代碼是一個包含相同月份的測試(性能較低): + +```Go +for _, s := range summer { + for _, q := range Q2 { + if s == q { + fmt.Printf("%s appears in both\n", s) + } + } +} +``` + +如果切片操作長處cap(s)的上限將導致一個panic異常,但是超出len(s)則是擴展了slice,因此新slice的長度會變大: + +```Go +fmt.Println(summer[:20]) // panic: out of range + +endlessSummer := summer[:5] // extend a slice (within capacity) +fmt.Println(endlessSummer) // "[June July August September October]" +``` + +另外,字符串的切片操作和[]byte字節類型切片的切片操作是類似的。它們都寫作x[m:n],併且都是返迴一個原始字節繫列的子序列,底層都是共享之前的底層數組,因此切片操作對應常量時間複雜度。x[m:n]切片操作對於字符串則生成一個新字符串,如果x是[]byte的話則生成一個新的[]byte。 + +因爲slice值包含指向第一個元素的指針,因此向函數傳遞slice將運行在函數內部脩改底層數組的元素。換句話説,複雜一個slice隻是對底層的數組創建了一個新的slice别名(§2.3.2)。下面的reverse函數在原內存空間將[]int類型的slice反轉,而且它可以用於任意長度的slice。 + +```Go +gopl.io/ch4/rev + +// reverse reverses a slice of ints in place. +func reverse(s []int) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} +``` + +這里我們反轉數組的應用: + +```Go +a := [...]int{0, 1, 2, 3, 4, 5} +reverse(a[:]) +fmt.Println(a) // "[5 4 3 2 1 0]" +``` + +一種將slice元素循環向左镟轉n個元素的方法是三次調用reverse反轉函數,第一次是反轉開頭的n個元素,然後是反轉剩下的元素,最後是反轉整個slice的元素。(如果是向右循環镟轉,則將第三個函數調用移到第一個調用位置就可以了。) + +```Go +s := []int{0, 1, 2, 3, 4, 5} +// Rotate s left by two positions. +reverse(s[:2]) +reverse(s[2:]) +reverse(s) +fmt.Println(s) // "[2 3 4 5 0 1]" +``` + +要註意的是slice類型的變量s和數組類型的變量a的初始化語法的差異。slice和數組的字面值語法很類似,它們都是用花括弧包含一繫列的初始化元素,但是對於slice併沒有指明序列的長度。這會隱式地創建一個合適大小的數組,然後slice的指針指向底層的數組。就像數組字面值一樣,slice的字面值也可以按順序指定初始化值序列,或者是通過索引和元素值指定,或者的兩種風格的混合語法初始化。 + +和數組不同的是,slice不能比較,因此我們不能使用==操作符來判斷兩個slice是否有相同的元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等([]byte),但是對於其他類型的slice,我們必鬚自己展開每個元素進行比較: + +```Go +func equal(x, y []string) bool { + if len(x) != len(y) { + return false + } + for i := range x { + if x[i] != y[i] { + return false + } + } + return true +} +``` + +上面關於兩個slice的深度相等測試,運行的時間併不比支持==操作的數組或字符串更多,但是爲何slice卻不支持比較運算符呢?這方面有兩個原因。第一個原因,一個slice的元素是間接引用的,一個slice甚至可以包含自身。雖然有很多辦法處理這種情形,但是沒有一個是簡單有效的。 + +第二個原因,因爲slice的元素是間接引用的,一個固定值的slice在不同的時間可能包含不同的元素,因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝,它要求在整個聲明週期中相等的key必鬚對相同的元素。對於像指針或chan之類的引用類型,==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的,也能臨時解決map類型的key問題,但是slice和數組不同的相等測試行爲會讓人睏惑。因此,安全的做飯是直接禁止slice之間的比較操作。 + +slice唯一合法的比較是和nil比較,例如: + +```Go +if summer == nil { /* ... */ } +``` + +一個零值的slice等於nil。一個nil值的slice併沒有底層數組。一個nil值的slice的長度和容量都是0,但是也有非nil值的slice的長度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。與任意類型的nil值一樣,我們可以用[]int(nil)類型轉換表達式來生成一個對應類型slice的nil值。 + +```Go +var s []int // len(s) == 0, s == nil +s = nil // len(s) == 0, s == nil +s = []int(nil) // len(s) == 0, s == nil +s = []int{} // len(s) == 0, s != nil +``` + +如果你需要測試一個slice是否是空的,使用len(s) == 0來判斷,而不是用s == nil來判斷。除了和nil相等比較外,一個nil值的slice的行爲和其它任意0産長度的slice一樣;例如reverse(nil)也是安全的。除了文檔已經明確説明的地方,所有的Go語言函數應該以相同的方式對待nil值的slice和0長度的slice。 + +內置的make函數創建一個指定元素類型、長度和容量的slice。容量部分可以省略,在這種情況下,容量將等於長度。 + +```Go +make([]T, len) +make([]T, len, cap) // same as make([]T, cap)[:len] +``` + +Under the hood, make creates an unnamed array variable and returns a slice of it; the array is accessible only through the returned slice. In the first form, the slice is a view of the entire array. In the second, the slice is a view of only the array’s first len elements, but its capacity includes the entire array. The additional elements are set aside for future growth. + +在底層,make創建了一個匿名的數組變量,然後返迴一個slice;隻有通過返迴的slice才能引用底層匿名的數組變量。在第一種語句中,slice是整個數組的view。在第二個語句中,slice隻引用了底層數組的前len個元素,但是容量將包含整個的數組。額外的元素是留給未來的增長用的。 {% include "./ch4-02-1.md" %}