Heim  >  Artikel  >  Backend-Entwicklung  >  Was ist []*T in der groß angelegten Verwirrungsszene? *[]Was ist T? *[]*TWas ist das?

Was ist []*T in der groß angelegten Verwirrungsszene? *[]Was ist T? *[]*TWas ist das?

醉折花枝作酒筹
醉折花枝作酒筹nach vorne
2021-08-02 09:19:273182Durchsuche

Kürzlich habe ich einen sehr seltsamen Code gesehen, darunter „[]*T“, „*[]T“ und „*[]*T“. Auf den ersten Blick sehen sie alle gleich aus, aber wenn wir genau hinschauen, erkennen wir Wir finden, dass sie unterschiedlich sind. Heute stellen wir Golongs „[]*T“, „*[]T“ und „*[]*T“ vor, um die Unterschiede zwischen ihnen zu verstehen. Werfen wir einen Blick darauf. Schauen wir uns als Neuling in der Go-Sprache um Sie werden neugierig sein, wenn Sie einen „seltsamen“ Code sehen; der Pseudocode ist zum Beispiel wie folgt:

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

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

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

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

Ich glaube, dass die meisten Anfänger, die gerade mit Go begonnen haben, verwirrt sein werden, wenn sie solchen sehen Code, und das Verwirrendste ist, worüber die Leute verwirrt sind:

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

Für eine solche Deklaration von Slices schauen wir uns nicht die letzten beiden Schreibweisen an; es ist leicht zu verstehen, wenn man sich []*T allein ansieht : ​​​​Die Speicheradressen aller T werden in diesem Slice gespeichert, was besser ist als das Speichern von T selbst und mehr Platz spart. Gleichzeitig kann []*T den Wert von T innerhalb der Methode ändern, aber []T kann nicht.

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
}

Zum Beispiel gibt das obige Beispiel

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

Nur durch Ändern der Methode in

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

kann der Wert von T geändert werden:

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

Beispiel

Konzentrieren wir uns auf den Unterschied zwischen []*T und *[] T, hier habe ich zwei Anhängefunktionen geschrieben:

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)
}

Schauen wir uns zuerst die erste an. Die Ausgabe lautet:

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

Das bedeutet, dass sich Änderungen innerhalb der Funktion während des Funktionsübertragungsprozesses auf die Außenseite auswirken können.

Schauen wir uns ein anderes Beispiel an:

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

Das Endergebnis ist:

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

hat nach außen keine Auswirkung.

Und wenn wir es erneut anpassen, werden wir etwas anderes finden:

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)
}

Das Endergebnis:

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

Sie können feststellen, dass sich die Verwendung der Append-Funktion zum Anhängen von Daten auf die Außenseite auswirkt, wenn Sie den Zeiger des Slice übergeben.

Slice-Prinzip

Bevor wir die oben genannten drei Situationen analysieren, wollen wir zunächst die Datenstruktur von Slice verstehen.

Wenn Sie sich den Quellcode direkt ansehen, werden Sie feststellen, dass es sich bei Slice tatsächlich um eine Struktur handelt, auf die jedoch nicht direkt zugegriffen werden kann.

Quellcode-Adresse runtime/slice.go

Es gibt drei wichtige Attribute:

Attribut Bedeutung ArrayDas zugrunde liegende Array, das Daten speichert, ist ein Zeiger. lenSlice lengthcapSlice Capacity 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 也指向了该结构体。

总结

所以总结一下:

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

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

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

  • Bei der Neugenerierung von Slices oder Arrays beeinflussen sich die Daten gegenseitig, da sie dasselbe zugrunde liegende Array verwenden. Dies muss beachtet werden.

  • Slices können auch Zeiger übergeben, aber es gibt nur wenige Szenarien, die zu unnötigen Missverständnissen führen. Es wird empfohlen, Werte nur nach Wert zu übergeben, da Länge und Kapazität nicht viel Speicher beanspruchen.

Wenn Sie Slicing verwendet haben, werden Sie feststellen, dass es ArrayList in Java sehr ähnlich ist und auch Daten erweitert und kopiert für die obere Schicht, und einige gemeinsame zugrunde liegende Implementierungen sind allen ähnlich.

Wenn wir uns zu diesem Zeitpunkt []*T *[]T *[]*T im Titel ansehen, werden wir feststellen, dass diese nicht miteinander verwandt sind, aber sie sehen sehr ähnlich aus und es ist leicht, Leute zu täuschen.

Wenn Sie es brauchen, können Sie sich das golongTutorial

ansehen

Das obige ist der detaillierte Inhalt vonWas ist []*T in der groß angelegten Verwirrungsszene? *[]Was ist T? *[]*TWas ist das?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen