Partage de technologie

Explication détaillée des génériques Go

2024-07-12

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

Introduction

Si nous voulons écrire une fonction pour comparer respectivement les tailles de deux entiers et de nombres à virgule flottante, nous devons écrire deux fonctions. comme suit:

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

Les deux fonctions ont exactement la même logique de traitement à l'exception des différents types de données. Existe-t-il un moyen d'accomplir la fonction ci-dessus avec une seule fonction ? Oui, c'est générique.

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

Génériques

Document du site officiel :https://go.dev/blog/introduction-generiques
Les génériques ajoutent trois nouvelles fonctionnalités importantes au langage :

  • Tapez les paramètres pour les fonctions et les types.
  • Définissez un type d'interface comme un ensemble de types, y compris les types sans méthodes.
  • L'inférence de type permet dans de nombreux cas d'omettre les paramètres de type lors de l'appel de fonctions.

Paramètres de type

Les fonctions et les types peuvent désormais avoir des paramètres de type. Une liste de paramètres de type ressemble à une liste de paramètres normale, sauf qu'elle utilise des crochets au lieu de crochets.
Insérer la description de l'image ici

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

Parmi eux, les contraintes.Ordered est un type personnalisé (le code source n'est pas affiché ici).
Si vous ne comprenez pas, vous pouvez remplacer temporairement les contraintes.Commandé avec ·int | float64

Fournir un paramètre de type (int dans ce cas) à un GMin est appelé instanciation. L'instanciation se produit en deux étapes.

  • Tout d’abord, le compilateur remplace tous les arguments de type par leurs paramètres de type respectifs dans la fonction ou le type générique.
  • Deuxièmement, le compilateur vérifie que chaque paramètre de type satisfait ses contraintes respectives.
    Nous verrons ce que cela signifie sous peu, mais si la deuxième étape échoue, l'instanciation échouera et le programme sera invalide.

Après une instanciation réussie, nous disposons d’une fonction non générique qui peut être appelée comme n’importe quelle autre fonction.Par exemple, dans un code comme

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

Tous les codes sont

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

L'instanciation de GMin[float64] produit en fait notre fonction Min à virgule flottante originale, que nous pouvons utiliser dans les appels de fonction.

Les paramètres de type peuvent également être utilisés avec des types.

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

Ici, le type générique Tree stocke la valeur du paramètre de type T. Les types génériques peuvent avoir des méthodes, comme Lookup dans cet exemple. Pour utiliser des types génériques,Il doit être instancié; Tree[string] est un exemple d'instanciation de Tree à l'aide du paramètre de type string.

Jeux de caractères

Chaque paramètre de type dans la liste des paramètres de type a un type. Puisqu'un paramètre de type est lui-même un type, le type du paramètre de type définit l'ensemble des types.Ce métatype est appelécontraintes de type
Dans la méthode générique GMin, les contraintes de type sont importées du package de contraintes. Les contraintes ordonnées décrivent tous les types de collections dont les valeurs peuvent être ordonnées, ou en d'autres termes, comparées à l'opérateur &lt; (ou &lt;=, &gt;, etc.). Cette contrainte garantit que seuls les types avec des valeurs triables peuvent être transmis à GMin. Cela signifie également que dans le corps de la fonction GMin, la valeur de ce paramètre de type peut être utilisée à des fins de comparaison avec l'opérateur &lt;.
Dans Go, les contraintes de type doivent être des interfaces . Autrement dit, les types d’interface peuvent être utilisés comme types de valeur ou méta-types. Les interfaces définissent des méthodes, nous pouvons donc évidemment exprimer des contraintes de type qui nécessitent la présence de certaines méthodes. Mais contraintes.Ordered est aussi un type d’interface, et l’opérateur &lt; n’est pas une méthode.
La double finalité des types d’interface est en effet un concept important dans le langage Go. Comprenons en profondeur et illustrons par des exemples l'énoncé "les types d'interface peuvent être utilisés comme types de valeur et également comme métatypes" [1][2][3][4][5].

  1. Interface comme type de valeur :

Lorsqu'une interface est utilisée comme type valeur, elle définit un ensemble de méthodes que tout type qui implémente ces méthodes peut être affecté à la variable d'interface. Il s’agit de l’utilisation la plus courante des interfaces.

Par exemple:

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

Dans cet exemple,Stringer Les interfaces sont utilisées comme types de valeur,Person Le type implémenteString() méthode, afin qu'il puisse être affecté àStringer tapez une variable.

  1. Interface en tant que méta-type :

Lorsqu'une interface est utilisée comme métatype, elle définit un ensemble de contraintes de type à utiliser dans la programmation générique. Il s'agit d'un nouvel usage après l'introduction des génériques dans Go 1.18.

Par exemple:

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

Dans cet exemple,Ordered Les interfaces sont utilisées comme métatypes, qui définissent un ensemble de types (entiers, nombres à virgule flottante et chaînes) pouvant être comparés.Min Les fonctions utilisent cette interface comme contrainte de type et peuvent accepter n'importe quelOrdered Le type de contrainte comme argument.

Ce double objectif rend les interfaces de Go très puissantes et flexibles pour la programmation générique. Ils peuvent non seulement définir le comportement des objets (en tant que types valeur), mais également définir des collections de types (en tant que métatypes), améliorant ainsi considérablement l'expressivité et la réutilisabilité du code tout en conservant la simplicité du langage.

Jusqu'à récemment, la spécification Go indiquait qu'une interface définissait un ensemble de méthodes, qui correspond à peu près à l'ensemble des méthodes énumérées dans l'interface. Tout type qui implémente toutes ces méthodes implémente cette interface.
Insérer la description de l'image ici
Mais une autre façon de voir les choses est de dire qu’une interface définit un ensemble de types, c’est-à-dire des types qui implémentent ces méthodes. De ce point de vue, tout type qui est un élément d’un ensemble de types d’interface implémente cette interface.
Insérer la description de l'image ici
Les deux vues conduisent au même résultat : pour chaque ensemble de méthodes, on peut imaginer l'ensemble correspondant de types qui implémentent ces méthodes, c'est-à-dire l'ensemble de types définis par l'interface.

Pour nos besoins, cependant, la vue ensemble de types présente un avantage par rapport à la vue ensemble de méthodes : nous pouvons contrôler l'ensemble de types de nouvelles manières en ajoutant explicitement des types à la collection.

Nous avons étendu la syntaxe des types d'interface pour y parvenir. Par exemple, interface{ int|string|bool } définit un ensemble de types contenant les types int, string et bool.
Insérer la description de l'image ici
Une autre façon de dire cela est que l'interface n'est satisfaite que par int, string ou bool.

Examinons maintenant la définition réelle des contraintes.Ordonné :

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

Cette déclaration indique que l'interface Ordered est une collection de tous les types entiers, à virgule flottante et chaîne. Les barres verticales représentent des unions de types (ou des ensembles de types dans ce cas). Integer et Float sont des types d’interface définis de manière similaire dans le package de contraintes. Notez que l’interface Ordered ne définit aucune méthode.

Pour les contraintes de type, nous ne nous soucions généralement pas des types spécifiques, tels que les chaînes, nous nous intéressons à tous les types de chaînes.C'est~ Le but du jeton.expression~string Représente une collection de tous les types dont le type sous-jacent est une chaîne.Cela inclut la chaîne de type elle-même et tous les types déclarés avec des définitions, par ex.type MyString string

Bien sûr, nous souhaitons toujours spécifier des méthodes dans l’interface et nous voulons être rétrocompatibles. Dans Go 1.18, une interface peut contenir des méthodes et des interfaces intégrées comme auparavant, mais elle peut également intégrer des types non-interfaces, des unions et des ensembles de types sous-jacents.

L'interface utilisée comme contrainte peut être nommée (comme Ordered) ou être une interface littérale en ligne dans la liste des paramètres de type. Par exemple:

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

Ici, S doit être un type de tranche et son type d'élément peut être n'importe quel type.

Parce que c'est une situation courante, pour les interfaces qui contraignent les positions, l'interface fermée{} peut être omise, et on peut simplement écrire (sucre de syntaxe des génériques en langage Go et écriture simplifiée des contraintes de type) :

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

Étant donné que les interfaces vides sont courantes dans les listes de paramètres de type ainsi que dans le code Go normal, Go 1.18 introduit un nouvel identifiant prédéclaré any comme alias pour les types d'interface vides. On obtient ainsi ce code idiomatique :

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

Inférence de type

Avec les paramètres de type, les paramètres de type doivent être transmis, ce qui peut conduire à un code verbeux. Revenons à notre fonction générale GMin :