GO學習筆記——切片的概念(11)

數組是不可以調整大小的,所以數組使用起來不夠靈活。因此GO語言使用了Slice(切片),切片是數組建立的一種方便、靈活且功能強大的包裝。

正因爲數組不可以調整大小,使用不夠靈活這個缺點,所以在GO語言開發中,用的更多的是Silce,下面來進入Slice的學習。


Slice的定義

先來看一下slice的一個簡單的定義

func main() {
	arr := [...]int{0,1,2,3,4,5,6}
	s := arr[2:6]    
	fmt.Println(s)
}

看一下輸出結果

[2 3 4 5]

這邊解釋一下,切片賦值的那一句話,[2:6]的意思是,從arr數組的下標2開始,到arr數組的下標(6-1)也就是5結束,注意這裏是一個半開半閉的區間,左邊是閉,右邊是開,所以打印slice的輸出結果也就是2,3,4,5。

下面來看一下一些靈活的定義

func main() {
	arr := [...]int{0,1,2,3,4,5,6}
	s := arr[2:6]	//從下標2到下標5
	s1 := arr[2:]	//從下標2一直到最後,也就是下標6
	s2 := arr[:6]	//從下標0到下標5
	s3 := arr[:]	//等價於整個數組
 	fmt.Println(s)
 	fmt.Println(s1)
 	fmt.Println(s2)
 	fmt.Println(s3)
}

輸出結果

[2 3 4 5]
[2 3 4 5 6]
[0 1 2 3 4 5]
[0 1 2 3 4 5 6]

另外,在Slice之上還可以創建Slice

func main() {
	arr := []int{0,1,2,3,4,5,6}
	s1 := arr[:]
	fmt.Println(s1)
	s1 = s1[2:]
	fmt.Println(s1)
	s1 = arr[:4]
	fmt.Println(s1)
}

輸出結果

[0 1 2 3 4 5 6]
[2 3 4 5 6]
[0 1 2 3]

另一種創建方式

func main() {
	s1 := []int{0,1,2,3,4,5,6}    
	fmt.Println(s1)
}

上面的創建方式相當於下面這樣,s1其實就是整個數組的映射,只是在創建數組的同時,返回的是對整個數組的Slice

func main() {
	arr := []int{0,1,2,3,4,5,6}
	s1 := arr[:]
	fmt.Println(s1)
}

Slice的原理

Slice本身其實不擁有任何數據,它其實相當於一個視圖,映射了數組的一部分,對視圖(Slice)的修改會直接映射到原來的數組上。

所以說,要有Slice,必須得先有一個數組,因爲Slice只是數組的一個反映而已.

來看一個簡單的例子

//注意函數參數寫成[],就表示這個參數是一個Slice,而不是數組
func modifySlice(s []int){
	s[0] = 100
}

func main() {
	arr := [...]int{0,1,2,3,4,5,6}
	s := arr[:6]	//從下標2到下標5
	fmt.Println(s)
	modifySlice(s)
	fmt.Print("After modifySlice:")
 	fmt.Println(s)
	fmt.Println(arr)
}

輸出結果

[0 1 2 3 4 5]
After modifySlice:[100 1 2 3 4 5]
[100 1 2 3 4 5 6]

我們在函數內部將Slice修改了,修改了之後,因爲Slice是數組的一層視圖,因此實際上也就是改了數組本身,我們在最後一行打印了這個數組,發現數組本身也被改變了,另外數組改了之後,Slice也就隨之改變了。

所以,如果函數的參數傳的是一個Slice,那麼這就相當於一個引用傳遞,對Slice的修改會改變數組的本身。這樣我們就不用像之前那樣,傳一個數組的指針進去了。

 

當一個數組的多個Slice同時修改該數組時,會怎麼樣?

func main() {
	arr := [...]int{0,1,2,3,4,5,6}
	s1 := arr[:]
	s2 := arr[:]

	s1[0] = 100
	fmt.Println(arr)
	s2[1] = 200
	fmt.Println(arr)
}

輸出結果

[100 1 2 3 4 5 6]
[100 200 2 3 4 5 6]

所以,當多個Slice共享同一個數組的時候,每個Slice所做的修改都會映射在原數組中。

Slice的越界問題

先上代碼

func main() {
	arr := []int{0,1,2,3,4,5,6}
	s1 := arr[2:6]
	s2 := s1[3:5]
	fmt.Println(s1)
	fmt.Println(s2)
}

s1我們知道,是[2,3,4,5],但是s2究竟是什麼呢?

它從s1的下標3開始,也就是5,到下標4結束,但是s1中沒有下標爲4的元素,那麼s2會不會報錯呢?如果不報錯,那麼輸出的是什麼呢?

輸出結果

[2 3 4 5]
[5 6]

注意,這裏我們就可以發現了。雖然在s1中沒有下標爲4的元素,但是這麼說,原數組中其實還是有這個元素的存在的。還是畫個圖來理解一下吧。

 看上圖,s1雖然只是映射了[2,3,4,5]這四個元素,但是其實後面的6那個元素在數組底層還是存在的。因爲本身s1和s2都只是視圖而已,但不包含實際的數據,所以s2雖然超出了s1的界限,但是還是沒有超過原數組的界限。正因爲如此,這也是Slice的一個靈活之處,只要沒有超過原數組的界限,這就是可以的,不會報錯。

但是一旦超過了原數組的界限,就會報錯了,比如改一下數據

func main() {
	arr := []int{0,1,2,3,4,5,6}
	s1 := arr[2:6]
	s2 := s1[3:6]    //這裏從[3:5]改成了[3:6]
	fmt.Println(s1)
	fmt.Println(s2)
}

這樣就報錯了

panic: runtime error: slice bounds out of range

Slice的底層實現

有了上面的越界問題,再來看看Slice的底層實現究竟是怎麼樣的。

它內部實現了三個變量:ptr,len,capacity

  • ptr表示Slice對應的數組的下標,如在之前的例子中,s1的ptr就是指向下標爲2的元素
  • len表示這個Slice的長度,指在Slice定義時候給的大小
  • capacity表示這個Slice的容量,也就是數組最後一個元素到ptr指向的元素的距離大小

如果學過C++的人很好理解,這和vector中的三個指針實現起來差不多,begin,end和last,下面來畫一張圖然後結合代碼來看看

我們以s1爲例

如上,ptr就是指向下標爲2的元素,len就是s1[2:6]這個Slice的長度,capacity就是數組最後一個元素的下標減到ptr指向的元素的下標。

對於s1而言,它有四個元素,下標分別爲0,1,2,3,那麼如果訪問s1[4]會出現什麼問題呢?按之前的理解,s1[4]在原數組上其實是指下標爲6的那個元素。

func main() {
	arr := []int{0,1,2,3,4,5,6}
	s1 := arr[2:6]
	s2 := s1[3:5]
	s1[4] = 4	//這裏訪問s1[4]
	fmt.Println(s1)
	fmt.Println(s2)
}

編譯器會報錯

panic: runtime error: index out of range

所以,這裏就知道了,如果要訪問這個Slice,那麼下標最多不可以超過Slice的 len - 1 這個大小,這和我們平時的理解是一樣的,有4個元素,那麼最高下標是3,訪問下標爲4就是越界了。

所以,要將這部分和上面定義s2時做區別,在訪問Slice的時候,下標不可以超過 len - 1;但是用一個Slice定義另一個Slice的時候,Slice是可以進行擴展的,只要不超過原數組的界限,就可以定義成功。

另外,Slice不可以向前擴展,所以用一些Slice定義另一個Slice的時候,新定義的Slice的ptr不可能比原來的Slice的ptr要小。

 

總結一下Slice使用的注意點

  • Slice不可以向前擴展
  • s[i]使用時不可以超越len(s)
  • Slice向後擴展不可以超過底層數組cap(s)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章