Technologieaustausch

Ausführliche Erklärung der Go-Generika

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Einführung

Wenn wir eine Funktion schreiben möchten, um die Größen zweier Ganzzahlen bzw. Gleitkommazahlen zu vergleichen, müssen wir zwei Funktionen schreiben. wie folgt:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

func MinInt(x, y int) int {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Die beiden Funktionen haben bis auf die unterschiedlichen Datentypen genau die gleiche Verarbeitungslogik. Gibt es eine Möglichkeit, die obige Funktion mit einer Funktion zu erreichen? Ja, das ist generisch.

func min[T int | float64](x, y T) T {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Generika

Offizielles Website-Dokument:https://go.dev/blog/intro-generics
Generics fügen der Sprache drei wichtige neue Funktionen hinzu:

  • Typparameter für Funktionen und Typen.
  • Definieren Sie einen Schnittstellentyp als eine Reihe von Typen, einschließlich Typen ohne Methoden.
  • Typinferenz ermöglicht in vielen Fällen das Weglassen von Typparametern beim Aufrufen von Funktionen.

Geben Sie Parameter ein

Funktionen und Typen dürfen jetzt Typparameter haben. Eine Typparameterliste sieht ähnlich aus wie eine normale Parameterliste, verwendet jedoch eckige Klammern anstelle runder Klammern.
Fügen Sie hier eine Bildbeschreibung ein

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func GMin[T constraints.Ordered](x, y T) T {
	if x < y {
		return x
	}
	return y
}

func main() {
	x := GMin[int](2, 3)
	fmt.Println(x) // 输出结果为2
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Unter ihnen ist Constraints.Ordered ein benutzerdefinierter Typ (der Quellcode wird hier nicht angezeigt).
Wenn Sie es nicht verstehen, können Sie die Einschränkungen vorübergehend ersetzen. Bestellt mit ·int | float64

Das Bereitstellen eines Typparameters (in diesem Fall int) für einen GMin wird als Instanziierung bezeichnet. Die Instanziierung erfolgt in zwei Schritten.

  • Zunächst ersetzt der Compiler in der gesamten generischen Funktion oder im generischen Typ alle Typargumente durch ihre jeweiligen Typparameter.
  • Zweitens überprüft der Compiler, ob jeder Typparameter seine jeweiligen Einschränkungen erfüllt.
    Wir werden gleich sehen, was das bedeutet, aber wenn der zweite Schritt fehlschlägt, schlägt die Instanziierung fehl und das Programm ist ungültig.

Nach erfolgreicher Instanziierung haben wir eine nicht generische Funktion, die wie jede andere Funktion aufgerufen werden kann.Zum Beispiel in Code wie

fmin := GMin[float64]
m := fmin(2.71, 3.14)
  • 1
  • 2

Alle Codes sind

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func GMin[T constraints.Ordered](x, y T) T {
	if x < y {
		return x
	}
	return y
}

func main() {
	fmin := GMin[float64] // 相当于func GMin(x, y float64) float64{...}
	m := fmin(2.71, 3.14)
	fmt.Println(m) // 输出结果为2.71
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Die Instanziierung von GMin[float64] erzeugt tatsächlich unsere ursprüngliche Gleitkomma-Min-Funktion, die wir in Funktionsaufrufen verwenden können.

Typparameter können auch mit Typen verwendet werden.

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Hier speichert der generische Typ Tree den Wert des Typparameters T. Generische Typen können Methoden haben, wie in diesem Beispiel „Lookup“. Um generische Typen zu verwenden,Es muss instanziiert werden; Tree[string] ist ein Beispiel für die Instanziierung von Tree mithilfe des Typparameters string.

Typensätze

Jeder Typparameter in der Typparameterliste hat einen Typ. Da ein Typparameter selbst ein Typ ist, definiert der Typ des Typparameters die Menge der Typen.Dieser Metatyp heißtTypbeschränkungen
In der generischen Methode GMin werden Typeinschränkungen aus dem Constraints-Paket importiert. Geordnete Einschränkungen beschreiben alle Arten von Sammlungen, deren Werte geordnet oder mit anderen Worten mit dem &lt;-Operator (oder &lt;=, &gt; usw.) verglichen werden können. Diese Einschränkung stellt sicher, dass nur Typen mit sortierbaren Werten an GMin übergeben werden können. Dies bedeutet auch, dass im GMin-Funktionskörper der Wert dieses Typparameters zum Vergleich mit dem &lt;-Operator verwendet werden kann.
In Go müssen Typeinschränkungen Schnittstellen sein . Das heißt, Schnittstellentypen können als Werttypen oder Metatypen verwendet werden. Schnittstellen definieren Methoden, sodass wir natürlich Typbeschränkungen ausdrücken können, die das Vorhandensein bestimmter Methoden erfordern. Aber Constraints.Ordered ist auch ein Schnittstellentyp und der &lt;-Operator ist keine Methode.
Der doppelte Zweck von Schnittstellentypen ist tatsächlich ein wichtiges Konzept in der Go-Sprache. Lassen Sie uns die Aussage „Schnittstellentypen können als Werttypen und auch als Metatypen verwendet werden“ im Detail verstehen und anhand von Beispielen [1][2][3][4][5] veranschaulichen.

  1. Schnittstelle als Werttyp:

Wenn eine Schnittstelle als Werttyp verwendet wird, definiert sie eine Reihe von Methoden, sodass jeder Typ, der diese Methoden implementiert, der Schnittstellenvariablen zugewiesen werden kann. Dies ist die häufigste Verwendung von Schnittstellen.

Zum Beispiel:

type Stringer interface {
    String() string
}

type Person struct {
    Name string
}

func (p Person) String() string {
    return p.Name
}

var s Stringer = Person{"Alice"} // Person 实现了 Stringer 接口
fmt.Println(s.String()) // 输出: Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

In diesem Beispiel,Stringer Als Werttypen werden Schnittstellen verwendet,Person Der Typ implementiertString() Methode, sodass sie zugeordnet werden kannStringer Typvariable.

  1. Schnittstelle als Metatyp:

Wenn eine Schnittstelle als Metatyp verwendet wird, definiert sie eine Reihe von Typeinschränkungen für die Verwendung in der generischen Programmierung. Dies ist eine neue Verwendung nach der Einführung von Generika in Go 1.18.

Zum Beispiel:

type Ordered interface {
    int | float64 | string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

fmt.Println(Min(3, 5))       // 输出: 3
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("a", "b"))   // 输出: a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

In diesem Beispiel,Ordered Schnittstellen werden als Metatypen verwendet, die eine Reihe von Typen (Ganzzahlen, Gleitkommazahlen und Zeichenfolgen) definieren, die verglichen werden können.Min Funktionen nutzen diese Schnittstelle als Typbeschränkung und können jede akzeptierenOrdered Der Typ der Einschränkung als Argument.

Dieser doppelte Zweck macht die Schnittstellen von Go sehr leistungsstark und flexibel für die generische Programmierung. Sie können nicht nur das Verhalten von Objekten (als Werttypen) definieren, sondern auch Typensammlungen (als Metatypen) definieren und so die Ausdruckskraft und Wiederverwendbarkeit von Code erheblich verbessern und gleichzeitig die Einfachheit der Sprache beibehalten.

Bis vor Kurzem hieß es in der Go-Spezifikation, dass eine Schnittstelle einen Methodensatz definiert, bei dem es sich in etwa um den in der Schnittstelle aufgezählten Methodensatz handelt. Jeder Typ, der alle diese Methoden implementiert, implementiert diese Schnittstelle.
Fügen Sie hier eine Bildbeschreibung ein
Eine andere Sichtweise wäre jedoch, dass eine Schnittstelle eine Reihe von Typen definiert, d. h. Typen, die diese Methoden implementieren. Aus dieser Perspektive implementiert jeder Typ, der ein Element eines Schnittstellentypsatzes ist, diese Schnittstelle.
Fügen Sie hier eine Bildbeschreibung ein
Beide Ansichten führen zum gleichen Ergebnis: Für jeden Methodensatz können wir uns den entsprechenden Typsatz vorstellen, der diese Methoden implementiert, also den durch die Schnittstelle definierten Typsatz.

Für unsere Zwecke hat die Typsatzansicht jedoch einen Vorteil gegenüber der Methodensatzansicht: Wir können den Typsatz auf neue Weise steuern, indem wir der Sammlung explizit Typen hinzufügen.

Um dies zu erreichen, haben wir die Syntax der Schnittstellentypen erweitert. Beispielsweise definiert interface{ int|string|bool } einen Typsatz, der die Typen int, string und bool enthält.
Fügen Sie hier eine Bildbeschreibung ein
Anders ausgedrückt: Die Schnittstelle wird nur durch int, string oder bool erfüllt.

Schauen wir uns nun die eigentliche Definition von Einschränkungen an. Bestellt:

type Ordered interface {
    Integer|Float|~string
}
  • 1
  • 2
  • 3

Diese Deklaration gibt an, dass die Ordered-Schnittstelle eine Sammlung aller Ganzzahl-, Gleitkomma- und Zeichenfolgentypen ist. Vertikale Balken stellen Vereinigungen von Typen (oder in diesem Fall Typenmengen) dar. Integer und Float sind Schnittstellentypen, die ähnlich im Constraints-Paket definiert sind. Beachten Sie, dass die Ordered-Schnittstelle keine Methoden definiert.

Bei Typbeschränkungen kümmern wir uns normalerweise nicht um bestimmte Typen, wie zum Beispiel Strings; wir sind an allen String-Typen interessiert.Das ist~ Der Zweck des Tokens.Ausdruck~string Stellt eine Sammlung aller Typen dar, deren zugrunde liegender Typ ein String ist.Dazu gehören der Typstring selbst und alle mit Definitionen deklarierten Typen, z.B.type MyString string

Natürlich möchten wir weiterhin Methoden in der Schnittstelle angeben und abwärtskompatibel sein. In Go 1.18 kann eine Schnittstelle wie zuvor Methoden und eingebettete Schnittstellen enthalten, aber auch Nicht-Schnittstellentypen, Unions und Sätze zugrunde liegender Typen einbetten.

Die als Einschränkung verwendete Schnittstelle kann entweder benannt sein (z. B. Ordered) oder eine literale Schnittstelle inline in der Typparameterliste sein. Zum Beispiel:

[S interface{~[]E}, E interface{}]
  • 1

Hier muss S ein Slice-Typ sein und sein Elementtyp kann ein beliebiger Typ sein.

Da dies eine häufige Situation ist, kann für Schnittstellen, die Positionen einschränken, die geschlossene Schnittstelle {} weggelassen werden, und wir können einfach schreiben (Syntaxzucker von Generika in der Go-Sprache und vereinfachtes Schreiben von Typbeschränkungen):

[S ~[]E, E interface{}]
  • 1

Da leere Schnittstellen sowohl in Typparameterlisten als auch im normalen Go-Code häufig vorkommen, führt Go 1.18 einen neuen vordeklarierten Bezeichner „any“ als Alias ​​für leere Schnittstellentypen ein. Somit erhalten wir diesen idiomatischen Code:

[S ~[]E, E any]
  • 1

Typinferenz

Bei Typparametern müssen Typparameter übergeben werden, was zu ausführlichem Code führen kann. Zurück zu unserer allgemeinen GMin-Funktion: