Go語言————7.2 切片

FLy_鵬程萬里發表於2018-07-01

7.2 切片

7.2.1 概念

切片(slice)是對陣列一個連續片段的引用(該陣列我們稱之為相關陣列,通常是匿名的),所以切片是一個引用型別(因此更類似於 C/C++ 中的陣列型別,或者 Python 中的 list 型別)。這個片段可以是整個陣列,或者是由起始和終止索引標識的一些項的子集。需要注意的是,終止索引標識的項不包括在切片內。切片提供了一個相關陣列的動態視窗。

切片是可索引的,並且可以由 len() 函式獲取長度。

給定項的切片索引可能比相關陣列的相同元素的索引小。和陣列不同的是,切片的長度可以在執行時修改,最小為 0 最大為相關陣列的長度:切片是一個 長度可變的陣列

切片提供了計算容量的函式 cap() 可以測量切片最長可以達到多少:它等於切片的長度 + 陣列除切片之外的長度。如果 s 是一個切片,cap(s) 就是從 s[0] 到陣列末尾的陣列長度。切片的長度永遠不會超過它的容量,所以對於 切片 s 來說該不等式永遠成立:0 <= len(s) <= cap(s)

多個切片如果表示同一個陣列的片段,它們可以共享資料;因此一個切片和相關陣列的其他切片是共享儲存的,相反,不同的陣列總是代表不同的儲存。陣列實際上是切片的構建塊。

優點 因為切片是引用,所以它們不需要使用額外的記憶體並且比使用陣列更有效率,所以在 Go 程式碼中 切片比陣列更常用。

宣告切片的格式是: var identifier []type(不需要說明長度)。

一個切片在未初始化之前預設為 nil,長度為 0。

切片的初始化格式是:var slice1 []type = arr1[start:end]

這表示 slice1 是由陣列 arr1 從 start 索引到 end-1 索引之間的元素構成的子集(切分陣列,start:end 被稱為 slice 表示式)。所以 slice1[0] 就等於 arr1[start]。這可以在 arr1 被填充前就定義好。

如果某個人寫:var slice1 []type = arr1[:] 那麼 slice1 就等於完整的 arr1 陣列(所以這種表示方式是arr1[0:len(arr1)] 的一種縮寫)。另外一種表述方式是:slice1 = &arr1

arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了陣列從第三個到最後的所有元素。

arr1[:3] 和 arr1[0:3] 相同,包含了從第一個到第三個元素(不包括第三個)。

如果你想去掉 slice1 的最後一個元素,只要 slice1 = slice1[:len(slice1)-1]

一個由數字 1、2、3 組成的切片可以這麼生成:s := [3]int{1,2,3}[:] 甚至更簡單的 s := []int{1,2,3}

s2 := s[:] 是用切片組成的切片,擁有相同的元素,但是仍然指向相同的相關陣列。

一個切片 s 可以這樣擴充套件到它的大小上限:s = s[:cap(s)],如果再擴大的話就會導致執行時錯誤(參見第 7.7 節)。

對於每一個切片(包括 string),以下狀態總是成立的:

s == s[:i] + s[i:] // i是一個整數且: 0 <= i <= len(s)
len(s) <= cap(s)

切片也可以用類似陣列的方式初始化:var x = []int{2, 3, 5, 7, 11}。這樣就建立了一個長度為 5 的陣列並且建立了一個相關切片。

切片在記憶體中的組織方式實際上是一個有 3 個域的結構體:指向相關陣列的指標,切片長度以及切片容量。下圖給出了一個長度為 2,容量為 4 的切片y。

  • y[0] = 3 且 y[1] = 5
  • 切片 y[0:4] 由 元素 3,5,7 和 11 組成。


示例 7.7 array_slices.go

package main
import "fmt"

func main() {
	var arr1 [6]int
	var slice1 []int = arr1[2:5] // item at index 5 not included!

	// load the array with integers: 0,1,2,3,4,5
	for i := 0; i < len(arr1); i++ {
		arr1[i] = i
	}

	// print the slice
	for i := 0; i < len(slice1); i++ {
		fmt.Printf("Slice at %d is %d\n", i, slice1[i])
	}

	fmt.Printf("The length of arr1 is %d\n", len(arr1))
	fmt.Printf("The length of slice1 is %d\n", len(slice1))
	fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))

	// grow the slice
	slice1 = slice1[0:4]
	for i := 0; i < len(slice1); i++ {
		fmt.Printf("Slice at %d is %d\n", i, slice1[i])
	}
	fmt.Printf("The length of slice1 is %d\n", len(slice1))
	fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))

	// grow the slice beyond capacity
	//slice1 = slice1[0:7 ] // panic: runtime error: slice bound out of range
}

輸出:

Slice at 0 is 2  
Slice at 1 is 3  
Slice at 2 is 4  
The length of arr1 is 6  
The length of slice1 is 3  
The capacity of slice1 is 4  
Slice at 0 is 2  
Slice at 1 is 3  
Slice at 2 is 4  
Slice at 3 is 5  
The length of slice1 is 4  
The capacity of slice1 is 4  

如果 s2 是一個 slice,你可以將 s2 向後移動一位 s2 = s2[1:],但是末尾沒有移動。切片只能向後移動,s2 = s2[-1:] 會導致編譯錯誤。切片不能被重新分片以獲取陣列的前一個元素。

注意 絕對不要用指標指向 slice。切片本身已經是一個引用型別,所以它本身就是一個指標!!

問題 7.2: 給定切片 b:= []byte{'g', 'o', 'l', 'a', 'n', 'g'},那麼 b[1:4]b[:2]b[2:] 和 b[:] 分別是什麼?

7.2.2 將切片傳遞給函式

如果你有一個函式需要對陣列做操作,你可能總是需要把引數宣告為切片。當你呼叫該函式時,把陣列分片,建立為一個 切片引用並傳遞給該函式。這裡有一個計算陣列元素和的方法:

func sum(a []int) int {
	s := 0
	for i := 0; i < len(a); i++ {
		s += a[i]
	}
	return s
}

func main() {
	var arr = [5]int{0, 1, 2, 3, 4}
	sum(arr[:])
}

7.2.3 用 make() 建立一個切片

當相關陣列還沒有定義時,我們可以使用 make() 函式來建立一個切片 同時建立好相關陣列:var slice1 []type = make([]type, len)

也可以簡寫為 slice1 := make([]type, len),這裡 len 是陣列的長度並且也是 slice 的初始長度。

所以定義 s2 := make([]int, 10),那麼 cap(s2) == len(s2) == 10

make 接受 2 個引數:元素的型別以及切片的元素個數。

如果你想建立一個 slice1,它不佔用整個陣列,而只是佔用以 len 為個數個項,那麼只要:slice1 := make([]type, len, cap)

make 的使用方式是:func make([]T, len, cap),其中 cap 是可選引數。

所以下面兩種方法可以生成相同的切片:

make([]int, 50, 100)
new([100]int)[0:50]

下圖描述了使用 make 方法生成的切片的記憶體結構:


示例 7.8 make_slice.go

package main
import "fmt"

func main() {
	var slice1 []int = make([]int, 10)
	// load the array/slice:
	for i := 0; i < len(slice1); i++ {
		slice1[i] = 5 * i
	}

	// print the slice:
	for i := 0; i < len(slice1); i++ {
		fmt.Printf("Slice at %d is %d\n", i, slice1[i])
	}
	fmt.Printf("\nThe length of slice1 is %d\n", len(slice1))
	fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
}

輸出:

Slice at 0 is 0  
Slice at 1 is 5  
Slice at 2 is 10  
Slice at 3 is 15  
Slice at 4 is 20  
Slice at 5 is 25  
Slice at 6 is 30  
Slice at 7 is 35  
Slice at 8 is 40  
Slice at 9 is 45  

The length of slice1 is 10  
The capacity of slice1 is 10  

因為字串是純粹不可變的位元組陣列,它們也可以被切分成 切片。

練習 7.4: fobinacci_funcarray.go: 為練習 7.3 寫一個新的版本,主函式呼叫一個使用序列個數作為引數的函式,該函式返回一個大小為序列個數的 Fibonacci 切片。

7.2.4 new() 和 make() 的區別

看起來二者沒有什麼區別,都在堆上分配記憶體,但是它們的行為不同,適用於不同的型別。

  • new(T) 為每個新的型別T分配一片記憶體,初始化為 0 並且返回型別為*T的記憶體地址:這種方法 返回一個指向型別為 T,值為 0 的地址的指標,它適用於值型別如陣列和結構體(參見第 10 章);它相當於 &T{}
  • make(T) 返回一個型別為 T 的初始值,它只適用於3種內建的引用型別:切片、map 和 channel(參見第 8 章,第 13 章)。

換言之,new 函式分配記憶體,make 函式初始化;下圖給出了區別:


在圖 7.3 的第一幅圖中:

var p *[]int = new([]int) // *p == nil; with len and cap 0
p := new([]int)

在第二幅圖中, p := make([]int, 0) ,切片 已經被初始化,但是指向一個空的陣列。

以上兩種方式實用性都不高。下面的方法:

var v []int = make([]int, 10, 50)

或者

v := make([]int, 10, 50)

這樣分配一個有 50 個 int 值的陣列,並且建立了一個長度為 10,容量為 50 的 切片 v,該 切片 指向陣列的前 10 個元素。

問題 7.3 給定 s := make([]byte, 5),len(s) 和 cap(s) 分別是多少?s = s[2:4],len(s) 和 cap(s) 又分別是多少?
問題 7.4 假設 s1 := []byte{'p', 'o', 'e', 'm'} 且 s2 := s1[2:],s2 的值是多少?如果我們執行 s2[1] = 't',s1 和 s2 現在的值又分別是多少?

7.2.5 多維 切片

和陣列一樣,切片通常也是一維的,但是也可以由一維組合成高維。通過分片的分片(或者切片的陣列),長度可以任意動態變化,所以 Go 語言的多維切片可以任意切分。而且,內層的切片必須單獨分配(通過 make 函式)。

7.2.6 bytes 包

型別 []byte 的切片十分常見,Go 語言有一個 bytes 包專門用來解決這種型別的操作方法。

bytes 包和字串包十分類似(參見第 4.7 節)。而且它還包含一個十分有用的型別 Buffer:

import "bytes"

type Buffer struct {
	...
}

這是一個長度可變的 bytes 的 buffer,提供 Read 和 Write 方法,因為讀寫長度未知的 bytes 最好使用 buffer。

Buffer 可以這樣定義:var buffer bytes.Buffer

或者使用 new 獲得一個指標:var r *bytes.Buffer = new(bytes.Buffer)

或者通過函式:func NewBuffer(buf []byte) *Buffer,建立一個 Buffer 物件並且用 buf 初始化好;NewBuffer 最好用在從 buf 讀取的時候使用。

通過 buffer 串聯字串

類似於 Java 的 StringBuilder 類。

在下面的程式碼段中,我們建立一個 buffer,通過 buffer.WriteString(s) 方法將字串 s 追加到後面,最後再通過buffer.String() 方法轉換為 string:

var buffer bytes.Buffer
for {
	if s, ok := getNextString(); ok { //method getNextString() not shown here
		buffer.WriteString(s)
	} else {
		break
	}
}
fmt.Print(buffer.String(), "\n")

這種實現方式比使用 += 要更節省記憶體和 CPU,尤其是要串聯的字串數目特別多的時候。

練習 7.5 給定切片 sl,將一個 []byte 陣列追加到 sl 後面。寫一個函式 Append(slice, data []byte) []byte,該函式在 sl 不能儲存更多資料的時候自動擴容。

練習 7.6 把一個快取 buf 分片成兩個 切片:第一個是前 n 個 bytes,後一個是剩餘的,用一行程式碼實現。



相關文章