Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Apakah []*T dalam adegan kekeliruan berskala besar? *[]Apakah itu T? *[]*Tapa itu?

Apakah []*T dalam adegan kekeliruan berskala besar? *[]Apakah itu T? *[]*Tapa itu?

醉折花枝作酒筹
醉折花枝作酒筹ke hadapan
2021-08-02 09:19:273190semak imbas

Baru-baru ini saya melihat sekeping kod yang sangat pelik, termasuk "[]*T", "*[]T" dan "*[]*T". dengan teliti, kita dapati bahawa mereka adalah Perbezaannya. Hari ini kami akan memperkenalkan “[]*T”, “*[]T” dan “*[]*T” golong untuk memahami perbezaan antara mereka. Mari kita lihat

Sebagai Pemula bahasa akan ingin tahu apabila melihat mana-mana kod "pelik" sebagai contoh, terdapat beberapa kaedah yang saya lihat baru-baru ini:

func FindA() ([]*T,error) {
}

func FindB() ([]T,error) {
}

func SaveA(data *[]T) error {
}

func SaveB(data *[]*T) error {
}

Saya percaya bahawa kebanyakan orang baru yang baru bermula dengan Pergi akan melihat sesuatu seperti ini Kod ini juga mengelirukan, dan perkara yang paling membingungkan ialah:

[]*T
*[]T
*[]*T

mengisytiharkan kepingan seperti ini. Jangan lihat pada dua cara terakhir untuk menulisnya; T sahaja, ia masih mudah difahami:
Kepingan ini menyimpan alamat memori semua T, yang menjimatkan lebih banyak ruang daripada menyimpan T itu sendiri Pada masa yang sama, []*T boleh mengubah suai nilai T dalam kaedah , tetapi []T tidak boleh.

func TestSaveSlice(t *testing.T) {
    a := []T{{Name: "1"}, {Name: "2"}}
    for _, t2 := range a {
        fmt.Println(t2)
    }
    _ = SaveB(a)
    for _, t2 := range a {
        fmt.Println(t2)
    }

}
func SaveB(data []T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

type T struct {
    Name string
}

Sebagai contoh, contoh di atas mencetak

{1}
{2}
{1}
{2}

Nilai T hanya boleh diubah suai dengan menukar kaedah kepada

func SaveB(data []*T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

:

&{1}
&{2}
&{1233}
&{2}

Contoh

Mari kita fokus pada perbezaan antara []*T dan *[]T Berikut adalah dua fungsi tambahan:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

Mari kita lihat yang pertama. satu dahulu. Outputnya ialah:

appendA [1000 2 3]
main [1000 2 3]

Ini bermakna semasa proses pemindahan fungsi, pengubahsuaian di dalam fungsi boleh menjejaskan bahagian luar.


Mari kita lihat contoh lain:

func appendB(x []int) {
    x = append(x, 4)
    fmt.Printf("appendA %v\n", x)
}

Hasil akhir ialah:

appendA [1 2 3 4]
main [1 2 3]

Tiada kesan luaran.

Dan apabila kita melaraskannya semula, kita akan mendapati sesuatu yang berbeza:

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v\n", x)
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendA %v\n", x)
}

Keputusan akhir:

appendA &[1 2 3 4]
main [1 2 3 4]

Ia boleh didapati bahawa jika penunjuk hirisan diluluskan, Apabila menambahkan data menggunakan fungsi tambah, ia akan menjejaskan dunia luar.

Prinsip slice

Sebelum menganalisis tiga situasi di atas, mari kita fahami struktur data slice terlebih dahulu.

Melihat terus pada kod sumber, anda akan mendapati bahawa kepingan sebenarnya adalah struktur, tetapi ia tidak boleh diakses secara langsung.

Alamat kod sumber runtime/slice.go

Terdapat tiga atribut penting:

属性 含义
array 底层存放数据的数组,是一个指针。
len 切片长度
cap 切片容量 cap>=len

提到切片就不得不想到数组,可以这么理解:

切片是对数组的抽象,而数组则是切片的底层实现。

其实通过切片这个名字也不难看出,它就是从数组中切了一部分;相对于数组的固定大小,切片可以根据实际使用情况进行扩容。

所以切片也可以通过对数组"切一刀"获得:

x1:=[6]int{0,1,2,3,4,5}
x2 := x[1:4]
fmt.Println(len(x2), cap(x2))

其中 x1 的长度与容量都是6。

x2 的长度与容量则为3和5。

  • x2 的长度很容易理解。

  • 容量等于5可以理解为,当前这个切片最多可以使用的长度。

因为切片 x2 是对数组 x1 的引用,所以底层数组排除掉左边一个没有被引用的位置则是该切片最大的容量,也就是5。

同一个底层数组

以刚才的代码为例:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

在函数传递过程中,main 中的 x 与 appendA 函数中的 x 切片所引用的是同个数组。

所以在函数中对 x[0]=100,main函数中也能获取到。

本质上修改的就是同一块内存数据。

值传递带来的误会

在上述例子中,在 appendB 中调用 append 函数追加数据后会发现 main 函数中并没有受到影响,这里我稍微调整了一下示例代码:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
主要是修改了切片初始化方式,使得容量大于了长度,具体原因后续会说明。

输出结果如下:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5

main 函数中的数据看样子确实没有受到影响;但细心的朋友应该会注意到  appendB 函数中的 x 在 append() 之后长度 +1 变为了4。

而在 main 函数中长度又变回了3.

这个细节区别就是为什么 append() "看似" 没有生效的原因;至于为什么要说“看似”,再次调整了代码:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}

在刚才的基础之上,以 append 之后的 x 为基础再做了一个切片;该切片的范围为 x 所引用数组的全部数据。

再来看看执行结果如何:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
y [1 2 3 444 0] len=5,cap=5

会神奇的发现 y 将所有数据都打印出来,在 appendB 函数中追加的数据其实已经写入了数组中,但为什么 x 本身没有获取到呢?

看图就很容易理解了:

  • 在appendB中确实是对原始数组追加了数据,同时长度也增加了。

  • 但由于是值传递,所以 slice 这个结构体即便是修改了长度为4,也只是对复制的那个对象修改了长度,main 中的长度依然为3.

  • 由于底层数组是同一个,所以基于这个底层数组重新生成了一个完整长度的切片便能看到追加的数据了。

所以这里本质的原因是因为 slice 是一个结构体,传递的是值,不管方法里如何修改长度也不会影响到原有的数据(这里指的是长度和容量这两个属性)。

切片扩容

还有一个需要注意:

刚才特意提到这里的例子稍有改变,主要是将切片的容量设置超过了数组的长度;

如果不做这个特殊设置会怎么样呢?

func TestAppendB(t *testing.T) {
    x:=[]int{1,2,3}
    //x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}

输出结果:

appendB [1 2 3 444] len=4,cap=6
main [1 2 3] len=3,cap=3
y [1 2 3] len=3,cap=3

这时会发现 main 函数中的 y 切片数据也没有发生变化,这是为什么呢?

这是因为初始化 x 切片时长度和容量都为3,当在 appendB 函数中追加数据时,会发现没有位置了。

这时便会进行扩容:

  • 将老数据复制一份到新的数组中。

  • 追加数据。

  • 将新的数据内存地址返回给 appendB 中的 x .

同样的由于是值传递,所以 appendB 中的切片换了底层数组对 main 函数中的切片没有任何影响,也就导致最终 main 函数的数据没有任何变化了。

传递切片指针

有没有什么办法即便是在扩容时也能对外部产生影响呢?

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendC %v\n", x)
}

输出结果为:

appendC &[1 2 3 4]
main [1 2 3 4] len=4,cap=6

这时外部的切片就能受到影响了,其实原因也很简单;

刚才也说了,因为 slice 本身是一个结构体,所以当我们传递指针时,就和平时自定义的 struct 在函数内部通过指针修改数据原理相同。

最终在 appendC 中的 x 的指针指向了扩容后的结构体,因为传递的是 main 函数中 x 的指针,所以同样的 main 函数中的 x 也指向了该结构体。

总结

所以总结一下:

  • 切片是对数组的抽象,同时切片本身也是一个结构体。

  • 参数传递时函数内部与外部引用的是同一个数组,所以对切片的修改会影响到函数外部。

  • 如果发生扩容,情况会发生变化,同时扩容会导致数据拷贝;所以要尽量预估切片大小,避免数据拷贝。

  • Apabila menjana semula kepingan atau tatasusunan, kerana ia berkongsi tatasusunan asas yang sama, data akan mempengaruhi satu sama lain.

  • Slice juga boleh melepasi penunjuk, tetapi terdapat beberapa senario dan ia akan menyebabkan salah faham yang tidak perlu; banyak ingatan.

Saya percaya bahawa jika anda telah menggunakan slicing, anda akan mendapati bahawa ia sangat serupa dengan ArrayList dalam Java Ia juga berdasarkan pelaksanaan tatasusunan dan juga akan mengembangkan dan menyalin datanya nampaknya bahasa itu hanya pilihan untuk lapisan atas, beberapa Pelaksanaan asas umum adalah serupa dengan semua orang.

Pada masa ini, jika kita melihat []*T *[]T *[]*T dalam tajuk, kita akan mendapati bahawa tiada kaitan antara mereka, tetapi mereka kelihatan sangat serupa dan mudah untuk orang gertak.

Jika anda memerlukannya, anda boleh membaca tutorial golong

Atas ialah kandungan terperinci Apakah []*T dalam adegan kekeliruan berskala besar? *[]Apakah itu T? *[]*Tapa itu?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:segmentfault.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam