Pourquoi les génériques
Vous tombez vite sur des types qui devraient fonctionner pour plus d'un type d'élément. Une « paire » de deux valeurs ne se soucie pas que les valeurs soient des entiers, des chaînes, ou un shape défini par l'utilisateur. Les génériques vous permettent d'écrire le type une seule fois et de l'instancier avec les types d'élément dont l'appelant a besoin.
L'alternative — écrire IntPair, StringPair, BytePair, etc. — devient vite lassante et ne se compose pas. Les génériques en Zero sont l'outil standard pour ça.
Fonctions génériques
Déclarez les paramètres de type entre chevrons, entre le nom de la fonction et la liste des paramètres :
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T et U sont des types placeholders — l'appelant décide ce qu'ils sont.
Les appels n'ont généralement pas besoin de les épeler ; le compilateur infère depuis les types des arguments :
let pair = makePair(40, 2_u8)
Ici, T est inféré à i32 (le défaut pour un littéral entier sans suffixe) et U à u8 (depuis le suffixe _u8). La liaison résultante pair est de type Pair<i32, u8>.
Si l'inférence choisirait les mauvais types — par exemple parce que les littéraux sont ambigus — vous pouvez fixer les paramètres sur le site d'appel :
let pair = makePair<u8, u8>(1, 2)
(La syntaxe d'appel à chevrons exacte peut varier selon les versions de Zero ; vérifiez la doc actuelle pour l'orthographe précise. Le comportement d'inférence par défaut est la partie stable.)
Shapes génériques
Les shapes prennent des paramètres de type de la même façon :
shape Pair<T, U> {
left: T,
right: U,
}
Le type de chaque champ peut mentionner les paramètres. Les instances les fixent :
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
Un exemple complet combinant un shape générique et une fonction générique — cliquez sur Run pour voir l'inférence à l'œuvre :
Une déclaration de shape générique, une fonction générique, un site d'appel fortement typé. Pas de boilerplate IntBytePair.
Alias de type
Quand le même type paramétré revient encore et encore, donnez-lui un nom avec type :
type BytePair = Pair<u8, u8>
Maintenant BytePair est interchangeable avec Pair<u8, u8> partout où vous pouvez écrire un type :
Un alias n'est qu'une fonctionnalité de nommage — il ne crée pas un type distinct. Une fonction qui prend un BytePair acceptera volontiers une valeur de type Pair<u8, u8> (et inversement).
Génériques dans la bibliothèque standard
Le même mécanisme alimente une grande partie de la bibliothèque standard. Quelques-uns que vous verrez dans du vrai code Zero :
Maybe<T>— une valeur optionnelle, contenant soit unT, soit rien.Span<T>— une tranche empruntée sur des valeursT.Span<u8>est la vue canonique sur un tampon d'octets.ref<T>etmutref<T>— des types de référence explicites pour les cas où vous devez partager des données sans copier.
Vous n'avez pas à tous les apprendre d'un coup. L'intérêt des génériques, c'est que le même shape fonctionne pour le type d'élément que vous avez sous la main.
Quand les génériques paient (et quand ils ne paient pas)
Recourez aux génériques quand vous vous retrouvez à écrire la même fonction ou le même shape deux fois avec des types d'élément différents. Recourez à un type concret quand :
- La logique de la fonction n'a de sens que pour un type précis (un parseur pour des
String, par exemple). - Vous voulez que le type apparaisse dans les messages d'erreur pour faciliter le débogage.
- Les caractéristiques de performance dépendent d'une taille mémoire précise.
Le coût des génériques est réel — binaires plus gros (chaque instanciation génère un nouveau code) et temps de compilation un peu plus longs. Pour la plupart du code applicatif, ce coût est négligeable, mais c'est bon à savoir quand vous construisez du code compact et embarqué où la taille du binaire compte.
Une note sur les contraintes
Certains systèmes génériques permettent de contraindre un paramètre de type (« T doit supporter == », « T doit implémenter Iterator »). L'histoire des contraintes en Zero pré-1.0 évolue encore — les exemples dans le dépôt officiel utilisent les génériques sous leur forme simple, sans bornes élaborées. À mesure que le langage se stabilisera, attendez-vous à ce qu'une syntaxe de contraintes apparaisse sous une forme petite et régulière, cohérente avec le reste du langage. Pour l'instant, écrivez des génériques qui fonctionnent pour tout T que vous leur passez réellement, et laissez le compilateur vous dire quand une opération n'est pas supportée.
La suite : les enums
Les génériques vous permettent de paramétrer sur des types. La prochaine brique est à l'autre bout du spectre — les enums, le type d'énumération simple de Zero pour les cas où les variantes ne portent pas de données supplémentaires.
Questions fréquentes
Comment fonctionnent les génériques en Zero ?
Déclarez les paramètres de type entre chevrons après le nom de la fonction ou du shape : fun makePair<T, U>(left: T, right: U) -> Pair<T, U> ou shape Pair<T, U> { left: T, right: U }. Les appelants soit fixent les paramètres explicitement (Pair<i32, u8>), soit laissent le compilateur les inférer depuis les arguments de l'appel.
Les shapes peuvent-ils être génériques en Zero ?
Oui. Un shape peut prendre des paramètres de type avec la même syntaxe à chevrons utilisée sur les fonctions : shape Pair<T, U> { left: T, right: U }. Chaque champ peut utiliser les paramètres dans son type. Les instances se forment en écrivant le type paramétré — Pair<i32, u8> par exemple.
Faut-il préciser les paramètres de type quand on appelle une fonction générique ?
Généralement non. Le compilateur les infère depuis les types des arguments. Appeler makePair(40, 2_u8) suffit — T devient i32 et U devient u8. Vous pouvez fixer les paramètres explicitement quand l'inférence choisirait le mauvais type ou quand vous voulez que le site d'appel les documente.
Qu'est-ce qu'un alias de type en Zero ?
Un alias de type est un nom raccourci pour une expression de type plus longue. type BytePair = Pair<u8, u8> vous permet d'écrire BytePair partout où vous écririez sinon Pair<u8, u8>. L'alias est une pure fonctionnalité de nommage — il n'introduit pas un nouveau type, juste un raccourci pour se référer à un type existant.
Où apparaissent les génériques dans la bibliothèque standard de Zero ?
Partout — partout où un type doit contenir ou opérer sur un type d'élément arbitraire. Maybe<T> pour les valeurs optionnelles, Span<u8> pour les tranches d'octets, des types conteneurs paramétrés sur leur type d'élément. Le même mécanisme de génériques gère les types utilisateur et ceux de la bibliothèque standard.