Comenceu amb els genèrics a Go

Molts llenguatges de programació tenen el concepte de genèric funcions: codi que pot acceptar amb elegància un d’una varietat de tipus sense necessitat d’especialitzar-se per a cadascun, sempre que tots aquests tipus implementin determinats comportaments.

Els genèrics estalvien molt temps. Si teniu una funció genèrica per, per exemple, retornar la suma d’una col·lecció d’objectes, no cal que escriviu una implementació diferent per a cada tipus d’objecte, sempre que qualsevol dels tipus en qüestió admeti l’addició.

Quan es va introduir per primera vegada el llenguatge Go, no tenia el concepte de genèrics, com ho fan C++, Java, C#, Rust i molts altres llenguatges. El més semblant que Go tenia als genèrics era el concepte del interfícieque permet tractar de la mateixa manera diferents tipus, sempre que admetin un determinat conjunt de comportaments.

Tot i així, les interfícies no són el mateix que els veritables genèrics. Requereixen un bon control a temps d’execució per funcionar de la mateixa manera que una funció genèrica, en lloc de ser genèrica a compilar temps. Així, la pressió va augmentar perquè l’idioma Go afegeixi genèrics d’una manera similar a altres idiomes, on el compilador crea automàticament el codi necessari per gestionar diferents tipus en una funció genèrica.

Amb Go 1.18, els genèrics ara formen part del llenguatge Go, implementat mitjançant l’ús d’interfícies per definir grups de tipus. Els programadors de Go no només tenen relativament poca sintaxi o comportament nou per aprendre, sinó que la manera com funcionen els genèrics a Go és compatible enrere. El codi més antic sense genèrics encara es compilarà i funcionarà com s’ha previst.

Anar genèrics en breu

Una bona manera d’entendre els avantatges dels genèrics i com utilitzar-los és començar amb un exemple contrastat. En farem servir un adaptat del tutorial de la documentació de Go per començar amb els genèrics.

Aquí teniu un programa (no és bo, però us hauríeu de fer una idea) que suma tres tipus de llesques: una llesca de int8s (bytes), una porció de int64s, i un tros de float64s. Per fer-ho de la manera antiga i no genèrica, hem d’escriure funcions separades per a cada tipus:

package main

import ("fmt")

func sumNumbersInt8 (s []int8) int8 {
    var total int8
    for _, i := range s {
        total +=i
    }
    return total
}

func sumNumbersFloat64 (s []float64) float64 {
    var total float64
    for _, f := range s {
        total +=f
    }
    return total
}

func sumNumbersInt64 (s []int64) int64 {
    var total int64
    for _, i := range s {
        total += i
    }
    return total
}

func main() {
    ints := []int64{32, 64, 96, 128}    
    floats := []float64{32.0, 64.0, 96.1, 128.2}
    bytes := []int8{8, 16, 24, 32}  

    fmt.Println(sumNumbersInt64(ints))
    fmt.Println(sumNumbersFloat64(floats))    
    fmt.Println(sumNumbersInt8(bytes))
}

El problema amb aquest enfocament és bastant clar. Estem duplicant una gran quantitat de treball en tres funcions, el que significa que tenim més possibilitats d’equivocar-nos. El que és molest és que el cos de cadascuna d’aquestes funcions és essencialment el mateix. Només els tipus d’entrada i sortida varien.

Com que Go no té el concepte de macro, que es troba habitualment en altres idiomes, no hi ha manera de reutilitzar amb elegància el mateix codi menys de copiar i enganxar. I els altres mecanismes de Go, com les interfícies i la reflexió, només permeten emular comportaments genèrics amb molta comprovació del temps d’execució.

Tipus parametritzats per a genèrics Go

A Go 1.18, la nova sintaxi genèrica ens permet indicar quins tipus pot acceptar una funció i com s’han de passar elements d’aquests tipus per la funció. Una manera general de descriure els tipus que volem que accepti la nostra funció és amb el interface amable. Aquí teniu un exemple, basat en el nostre codi anterior:

type Number interface {
    int8 | int64 | float64
}

func sumNumbers[N Number](s []N) N {
    var total N
    for _, num := range s {
        total += num
    }
    return total
}

El primer que cal destacar és el interface declaració anomenada Number. Això té els tipus que volem que puguem passar a la funció en qüestió; en aquest cas, int8, int64, float64.

La segona cosa a tenir en compte és el lleuger canvi en la manera com es declara la nostra funció genèrica. Just després del nom de la funció, entre claudàtors, descrivim els noms utilitzats per indicar els tipus passats a la funció: el paràmetres de tipus. Aquesta declaració inclou un o més parells de noms:

  • El nom que farem servir per referir-nos a qualsevol tipus que es transmeti en un moment donat.
  • El nom de la interfície que utilitzarem per als tipus acceptats per la funció sota aquest nom.

Aquí, fem servir N per referir-se a qualsevol dels tipus a Number. Si invoquem sumNumbers amb un tros de int64s, doncs N en el context d’aquesta funció és int64; si invoquem la funció amb un tros de float64s, doncs N és float64etcètera.

Tingueu en compte que l’operació que fem sobre N (en aquest cas, +) ha de ser un que tots els valors Number donarà suport. Si aquest no és el cas, el compilador cridarà. Tanmateix, algunes operacions Go són compatibles amb tots els tipus.

També podem utilitzar la sintaxi que es mostra a la interfície per passar una llista de tipus directament. Per exemple, podríem utilitzar això:

func sumNumbers[N int8 | int64 | float64](s []N) N {
    var total N
    for _, num := range s {
        total += num
    }
    return total
}

Tanmateix, si voldríem evitar repetir-ho constantment int8 | int64 | float64 al llarg del nostre codi, podríem definir-los com una interfície i estalviar-nos molt d’escriure.

Exemple complet de funció genèrica a Go

Aquí teniu l’aspecte de tot el programa amb una funció genèrica en lloc de tres especialitzades en tipus:

package main

import ("fmt")

type Number interface {
    int8 | int64 | float64
}

func sumNumbers[N Number](s []N) N {
    var total N
    for _, num := range s {
        total += num
    }
    return total
}

func main() {
    ints := []int64{32, 64, 96, 128}    
    floats := []float64{32.0, 64.0, 96.1, 128.2}
    bytes := []int8{8, 16, 24, 32}  

    fmt.Println(sumNumbers(ints))
    fmt.Println(sumNumbers(floats))    
    fmt.Println(sumNumbers(bytes))
}

En lloc de cridar a tres funcions diferents, cadascuna especialitzada per a un tipus diferent, anomenem una funció que el compilador especialitza automàticament per a cada tipus permès.

Aquest enfocament té diversos avantatges. El més important és que només hi ha menys codi: és més fàcil donar sentit al que fa el programa i més fàcil mantenir-lo. A més, aquesta nova funcionalitat no es fa a costa de existents codificat. Els programes Go que utilitzen l’estil antic d’una funció per a un tipus encara funcionaran bé.

any restricció de tipus a Go

Una altra addició a la sintaxi de tipus a Go 1.18 és la paraula clau any. És essencialment un àlies per interface{}, una manera menys sorollosa sintàcticament d’especificar que es pot utilitzar qualsevol tipus a la posició en qüestió. Tingues en compte que any es pot utilitzar en lloc de interface{} tanmateix només en una definició de tipus. No pots utilitzar any en qualsevol altre lloc.

Aquí teniu un exemple d’ús anyadaptat d’un exemple del document de proposta de genèrics Go:

func Print[T any] (s []T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

Aquesta funció recull una porció on els elements són de qualsevol tipus i els formata i escriu cadascun a la sortida estàndard. Passant-hi rodanxes que continguin qualsevol tipus Print La funció hauria de funcionar, sempre que els elements dins siguin imprimibles (i a Go, la majoria de tots els objectes tenen una representació imprimible).

Definicions de tipus genèrics a Go

Una altra manera d’utilitzar els genèrics és emprar-los en paràmetres de tipus, com a forma de crear genèrics definicions de tipus. Un exemple:

type CustomSlice[T Number] []T

Això crearia un tipus de porció els membres del qual només es podrien extreure del Number interfície. Si utilitzem això a l’exemple anterior:

type Number interface {
    int8 | int64 | float64
}

type CustomSlice[T Number] []T

func Print[N Number, T CustomSlice[N]] (s T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

func main(){
    sl := CustomSlice[int64]{32, 32, 32}
    Print(sl)
}

El resultat és a Print funció que agafarà llesques de qualsevol Number tipus, però res més.

Fixeu-vos en com fem servir CustomSlice aquí. Sempre que en fem ús CustomSlicehem de instanciar it — hem d’especificar, entre parèntesis, quin tipus s’utilitza dins de la llesca. Quan creem el tall sl en main()especifiquem que ho és int64. Però quan fem servir CustomSlice en la nostra definició de tipus per Printl’hem d’instanciar d’una manera que es pugui utilitzar en una definició de funció genèrica.

Si acabes de dir T CustomSlice[Number], el compilador es queixaria de la interfície que conté restriccions de tipus, que és massa específica per a una operació genèrica. Hem de dir T CustomSlice[N] per reflectir-ho CustomSlice està pensat per utilitzar un tipus genèric.

Copyright © 2022 IDG Communications, Inc.

Leave a Comment

Your email address will not be published. Required fields are marked *