• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_certifying

Got it!

Last updated on 3/10/17

Les génériques

Log in or subscribe for free to enjoy all this course has to offer!

Les génériques sont une fonctionnalité du framework .NET apparus avec la version 2. Vous vous en souvenez peut-être, nous avons cité le mot dans le cours précédent, dans le chapitre sur les tableaux et les listes. Les génériques permettent de créer des méthodes ou des classes qui sont indépendantes d'un type. Il est très important de connaître leur fonctionnement car c'est un mécanisme clé qui permet de faire beaucoup de choses, notamment en termes de réutilisabilité et d'amélioration des performances.

N'oubliez pas vos tubes d'aspirine et voyons à présent de quoi il retourne !

Qu'est-ce que les génériques ?

Avec les génériques, vous pouvez créer des méthodes ou des classes qui sont indépendantes d’un type. On les appellera des méthodes génériques et des types génériques.

Nous en avons déjà utilisé, rappelez-vous, avec la liste. La liste est une classe comme nous en avons déjà vu plein, sauf qu’elle a la capacité d’être utilisée avec n’importe quel autre type, comme les entiers, les chaînes de caractères, les voitures …

Cela permet d’éviter de devoir créer une classe ListeInt, une classe ListeString, une classe ListeVoiture, etc … On pourra utiliser cette classe avec tous les types grâce aux chevrons :

List<string> listeChaine = new List<string>();
List<int> listeEntier = new List<int>();
List<Voiture> listeVoiture = new List<Voiture>();

Nous indiquons entre les chevrons le type qui sera utilisé avec le type générique.

Oui mais, si nous voulons pouvoir mettre n’importe quel type d’objet dans une liste, il suffirait de créer une ListeObject ? Puisque tous les objets dérivent d'object

En fait, c’est le choix qui avait été fait dans la toute première version de .NET. On utilisait l’objet ArrayList qui possède une méthode Add prenant en paramètre un object. Cela fonctionne. Sauf que nous nous trouvions faces à des limitations :

  • Premièrement, nous pouvions mélanger n’importe quel type d’objet dans la liste, des entiers, des voitures, des chiens, etc. Cela devenait une classe fourre-tout et nous ne savions jamais ce qu’il y avait dans la liste.

  • Deuxièmement, même si nous savions qu’il n’y avait que des entiers dans la liste, nous étions obligés de le traiter en tant qu’object et donc d’utiliser le boxing et l’unboxing pour mettre les objets dans la liste ou pour les récupérer.

Cela engendrait donc confusion et perte de performance. Grâce aux génériques, il devenait donc possible de créer des listes de n’importe quel type et nous étions certains du type que nous allions récupérer dans la liste.

Les types génériques du framework .NET

Le framework .NET possède beaucoup de classes et d’interfaces génériques, notamment dans l’espace de nom System.Collections.Generic.

La liste est la classe générique que vous utiliserez sûrement le plus. Mais beaucoup d’autres sont à votre disposition. Citons par exemple la classe Queue<> qui permet de gérer une file d’attente style FIFO (first in, first out : premier entré, premier sorti) :

Queue<int> file = new Queue<int>();
file.Enqueue(3);
file.Enqueue(1);
int valeur = file.Dequeue(); // valeur contient 3
valeur = file.Dequeue(); // valeur contient 1

Citons encore le dictionnaire d’élément qui est une espèce d’annuaire où l’on accède aux éléments grâce à une clé :

Dictionary<string, Personne> annuaire = new Dictionary<string, Personne>();
annuaire.Add("06 01 02 03 04", new Personne { Prenom = "Nicolas"});
annuaire.Add("06 06 06 06 06", new Personne { Prenom = "Jeremie" });

Personne p = annuaire["06 06 06 06 06"]; // p contient la propriété Prenom valant Jeremie

Loin de moi l’idée de vous énumérer toutes les collections génériques du framework .NET, le but est de vous montrer rapidement qu’il existe beaucoup de classes génériques dans le framework .NET.

Créer une méthode générique

Nous commençons à cerner l’intérêt des génériques. Sachez qu’il est bien sûr possible de créer vos propres classes génériques ou vos propres méthodes.
Commençons par les méthodes, cela sera plus simple.

Il est globalement intéressant d’utiliser un type générique partout où nous pourrions avoir un object.
Nous avions créé une classe AfficheRepresentation() qui prenait un objet en paramètres, ce qui pourrait être :

public static class Afficheur
{
    public static void Affiche(object o)
    {
        Console.WriteLine("Afficheur d'objet :");
        Console.WriteLine("\tType : " + o.GetType());
        Console.WriteLine("\tReprésentation : " + o.ToString());
    }
}

Nous avons ici utilisé une classe statique permettant d’afficher le type d’un objet et sa représentation. Nous pouvons l’utiliser ainsi :

int i = 5;
double d = 9.5;
string s = "abcd";
Voiture v = new Voiture();
    
Afficheur.Affiche(i);
Afficheur.Affiche(d);
Afficheur.Affiche(s);
Afficheur.Affiche(v);

Rappelez-vous, à chaque fois qu’on passe dans cette méthode, l’objet est boxé en type object quand il s’agit d’un type valeur.
Nous pouvons améliorer cette méthode en créant une méthode générique. Regardons ce code :

public static class Afficheur
{
    public static void Affiche<T>(T a)
    {
        Console.WriteLine("Afficheur d'objet :");
        Console.WriteLine("\tType : " + a.GetType());
        Console.WriteLine("\tReprésentation : " + a.ToString());
    }
}

Cette méthode fait exactement la même chose mais avec les génériques.
Dans un premier temps, la méthode annonce qu’elle va utiliser un type générique représenté par la lettre T entre chevrons.

Cela veut dire que tout type utilisé dans cette méthode déclaré avec T sera du type passé à la méthode. Ainsi, la variable a est du type générique qui sera précisé lors de l’appel à cette méthode.
Comme a est un objet, nous pouvons appeler la méthode GetType() et la méthode ToString() sur cet objet.

Pour afficher un objet, nous pourrons faire :

int i = 5;
double d = 9.5;
string s = "abcd";
Voiture v = new Voiture();
    
Afficheur.Affiche<int>(i);
Afficheur.Affiche<double>(d);
Afficheur.Affiche<string>(s);
Afficheur.Affiche<Voiture>(v);

Dans le premier appel, nous indiquons que nous souhaitons afficher i dont le type générique est int. Ce qu’il se passe, c’est comme si le CLR créait la surcharge de la méthode Affiche, prenant un entier en paramètre :

public static void Affiche(int a)
{
    Console.WriteLine("Afficheur d'objet :");
    Console.WriteLine("\tType : " + a.GetType());
    Console.WriteLine("\tReprésentation : " + a.ToString());
}

De même pour l’affichage suivant, où l’on indique le type double entre les chevrons. C’est comme si le CLR créait la surcharge prenant un double en paramètre :

public static void Affiche(double a)
{
    Console.WriteLine("Afficheur d'objet :");
    Console.WriteLine("\tType : " + a.GetType());
    Console.WriteLine("\tReprésentation : " + a.ToString());
}

Et ceci pour tous les types utilisés, à savoir ici int, double, string et Voiture.

À noter que dans cet exemple, nous pouvons remplacer les quatre lignes suivantes :

Afficheur.Affiche<int>(i);
Afficheur.Affiche<double>(d);
Afficheur.Affiche<string>(s);
Afficheur.Affiche<Voiture>(v);

Par :

Afficheur.Affiche(i);
Afficheur.Affiche(d);
Afficheur.Affiche(s);
Afficheur.Affiche(v);

En effet, nul besoin de préciser quel type nous souhaitons traiter ici, le compilateur est assez malin pour le déduire du type de la variable. La variable i étant un entier, il est obligatoire que le type générique soit un entier. Il est donc facultatif ici de le préciser.

Une fois qu’il est précisé entre les chevrons, le type générique s’utilise dans la méthode comme n’importe quel autre type. Nous pouvons avoir autant de paramètres génériques que nous le voulons dans les paramètres et utiliser le type générique dans le corps de la méthode. Par exemple la méthode suivante :

public static void Echanger<T>(ref T t1, ref T t2)
{
    T temp = t1;
    t1 = t2;
    t2 = temp;
}

permet d’échanger le contenu de deux variables entre elles. C’est donc une méthode générique puisqu’elle précise entre les chevrons que nous pourrons utiliser le type T.

En paramètres de la méthode, nous passons deux variables de types génériques.

Dans le corps de la méthode, nous créons une variable du type générique qui sert de mémoire temporaire puis nous échangeons les références des deux variables.
Nous pourrons utiliser cette méthode ainsi :

int i = 5;
int j = 10;
Echanger(ref i, ref j);
Console.WriteLine(i);
Console.WriteLine(j);

Voiture v1 = new Voiture { Couleur = "Rouge" };
Voiture v2 = new Voiture { Couleur = "Verte" };
Echanger(ref v1, ref v2);
Console.WriteLine(v1.Couleur);
Console.WriteLine(v2.Couleur);

Qui donnera :

10
5
Verte
Rouge

Il est bien sûr possible de créer des méthodes prenant en paramètres plusieurs types génériques différents. Il suffit alors de préciser autant de types différents entre les chevrons qu’il y a de types génériques différents :

static void Main(string[] args)
{
    int i = 5;
    int j = 5;
    double d = 9.5;

    Console.WriteLine(EstEgal(i, j));
    Console.WriteLine(EstEgal(i, d));
}
public static bool EstEgal<T, U>(T t, U u)
{
    return t.Equals(u);
}

Ici, la méthode EstEgal() prend en paramètres potentiellement deux types différents. Nous l’appelons une première fois avec deux entiers et ensuite avec un entier et un double.

Créer une classe générique

Une classe générique fonctionne comme pour les méthodes. C’est une classe où nous pouvons indiquer de 1 à N types génériques. C’est comme cela que fonctionne la liste que nous avons déjà beaucoup manipulée.

En fait, la liste n’est qu’une espèce de tableau évolué. Nous pourrions très bien imaginer créer notre propre liste sur ce principe, sachant que c’est complètement absurde car elle sera forcément moins bien que cette classe, mais c’est pour l’étude.

Le principe est d’avoir un tableau plus ou moins dynamique qui grossit si jamais le nombre d’éléments devient trop grand pour sa capacité.
Pour déclarer une classe générique, nous utiliserons à nouveau les chevrons à la fin de la ligne qui déclare la classe :

public class MaListeGenerique<T>
{
}

Nous allons réaliser une implémentation toute basique de cette classe histoire de voir un peu à quoi ressemble une classe générique. Cette classe n’a d’intérêt que pour étudier les génériques, vous lui préférerez évidemment la classe List<> du framework .NET.

Nous avons besoin de trois variables privées. La capacité de la liste, le nombre d’éléments dans la liste et le tableau générique.

public class MaListeGenerique<T>
{
    private int capacite;
    private int nbElements;
    private T[] tableau;

    public MaListeGenerique()
    {
        capacite = 10;
        nbElements = 0;
        tableau = new T[capacite];
    }
}

Notons la déclaration du tableau. Il utilise le type générique. Concrètement, cela veut dire que quand nous utiliserons la liste avec un entier, nous aurons un tableau d’entiers. Lorsque nous utiliserons la liste avec un objet Voiture, nous aurons un tableau de Voiture, etc.

Nous initialisons ces variables membres dans le constructeur, en décidant complètement arbitrairement que la capacité par défaut de notre liste est de 10 éléments. La dernière ligne instancie le tableau en lui indiquant sa taille.

Il reste à implémenter l’ajout dans la liste :

public class MaListeGenerique<T>
{
    [Code enlevé pour plus de clarté]

    public void Ajouter(T element)
    {
        if (nbElements >= capacite)
        {
            capacite *= 2;
            T[] copieTableau = new T[capacite];
            for (int i = 0; i < tableau.Length; i++)
            {
                copieTableau[i] = tableau[i];
            }
            tableau = copieTableau;
        }
        tableau[nbElements] = element;
        nbElements++;
    }
}

Il s’agit simplement de mettre la valeur que l’on souhaite ajouter à l’emplacement adéquat dans le tableau. Nous le mettons en dernière position, c'est-à-dire à l’emplacement correspondant au nombre d’éléments.

Au début, nous avons commencé par vérifier si le nombre d’éléments était supérieur à la capacité du tableau. Si c’est le cas, alors nous devons augmenter la capacité du tableau, j’ai ici décidé encore complètement arbitrairement que je doublais la capacité. Il ne reste plus qu’à créer un nouveau tableau de cette nouvelle capacité et à copier les éléments du premier tableau dans celui-ci.

Vous aurez noté que le paramètre de la méthode Ajouter est bien du type générique.
Pour le plaisir, rajoutons enfin une méthode permettant de récupérer l’élément à un indice donné :

public class MaListeGenerique<T>
{
    [Code enlevé pour plus de clarté]

    public T ObtenirElement(int indice)
    {
        return tableau[indice];
    }
}

Il s’agit juste d’accéder au tableau pour renvoyer la valeur à l’indice concerné. L’élément intéressant ici est de constater que le type de retour de la méthode est bien du type générique.

Cette liste peut s’utiliser de la manière suivante :

MaListeGenerique<int> maListe = new MaListeGenerique<int>();
maListe.Ajouter(25);
maListe.Ajouter(30);
maListe.Ajouter(5);

Console.WriteLine(maListe.ObtenirElement(0));
Console.WriteLine(maListe.ObtenirElement(1));
Console.WriteLine(maListe.ObtenirElement(2));

for (int i = 0; i < 30; i++)
{
    maListe.Ajouter(i);
}

Ici, nous utilisons la liste avec un entier, mais elle fonctionnerait tout aussi bien avec un autre type.
N’hésitez pas à passer en debug dans la méthode Ajouter() pour observer ce qu’il se passe exactement lors de l’augmentation de capacité.
Voilà comment on crée une classe générique !

Une fois qu’on a compris que le type générique s’utilise comme n’importe quel autre type, cela devient assez facile.
Rappelez-vous, toute classe qui manipule des object est susceptible d’être améliorée en utilisant les génériques.

La valeur par défaut d’un type générique

Vous aurez remarqué dans l’implémentation de la liste du dessus que si nous essayons d’obtenir un élément du tableau à un indice qui n’existe pas, nous aurons une erreur. Ce comportement est une bonne chose, il est important de gérer les cas limites. En l’occurrence ici, on délègue au tableau la gestion du cas limite.

On pourrait envisager de gérer nous-mêmes ce cas limite en affichant un message et en renvoyant une valeur nulle. Seulement, pour un objet Voiture, qui est un type référence, il est tout à fait pertinent d’avoir null. Mais pour un int, qui est un type valeur, cela n’a pas de sens.

C’est là qu’intervient le mot-clé default.

Comme son nom l’indique, il renvoie la valeur par défaut du type. Pour un type référence, c’est null, pour un type valeur c’est 0. Ce qui donnerait :

public T ObtenirElement(int indice)
{
    if (indice < 0 || indice >= nbElements)
    {
        Console.WriteLine("L'indice n'est pas bon");
        return default(T);
    }
    return tableau[indice];
}

Les interfaces génériques

Les interfaces peuvent aussi être génériques. D’ailleurs, ça me fait penser que plus haut, je vous ai indiqué qu’un code était moche et que j’en parlerai plus tard…
Il s’agissait du chapitre sur les interfaces où nous avions implémentés l’interface IComparable.
Nous souhaitions comparer des voitures entre elles et nous avions obtenu le code suivant :

public class Voiture : IComparable
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }

    public int CompareTo(object obj)
    {
        Voiture voiture = (Voiture)obj;
        return Vitesse.CompareTo(voiture.Vitesse);
    }
}

Je souhaite pouvoir comparer des voitures entre elles, mais le framework .NET me fournit un object en paramètres de la méthode CompareTo(). Quelle idée ! Comme si je voulais pouvoir comparer des voitures avec des chats ou des chiens.
Cela me force en plus à faire un cast. Pourquoi il ne me passe pas directement une Voiture en paramètres ?

Vous le sentez venir et vous avez raison. Un object ! Berk, oserais-je dire ! Et sans parler des performances.

C’est là où les génériques vont pouvoir voler à notre secours. L’interface IComparable date de la première version du framework .NET. Le C# ne possédait pas encore les types génériques.
Depuis leur apparition, il est possible d’implémenter la version générique de cette interface.

Pour cela, nous faisons suivre l’interface du type que nous souhaitons utiliser entre les chevrons. Cela donne :

public class Voiture : IComparable<Voiture>
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }

    public int CompareTo(Voiture obj)
    {
        return Vitesse.CompareTo(obj.Vitesse);
    }
}

Nous devons toujours implémenter la méthode CompareTo() sauf que nous avons désormais un objet Voiture en paramètres, ce qui nous évite de le caster.

Les restrictions sur les types génériques

Une méthode ou une classe générique c’est bien, mais peut-être voulons-nous qu’elles ne fonctionnent pas avec tous les types.
Aussi, le C# permet de définir des restrictions sur les types génériques. Pour ce faire, on utilise le mot-clé where.

Il existe 6 types de restrictions :

Contrainte

Description

where T : struct

Le type générique doit être un type valeur

where T : class

Le type générique doit être un type référence

where T : new()

Le type générique doit posséder un constructeur par défaut

where T : IMonInterface

Le type générique doit implémenter l’interface IMonInterface

where T : MaClasse

Le type générique doit dériver de la classe MaClasse

where T1 : T2

Le type générique doit dériver du type générique T2

Par exemple, nous pouvons définir une restriction sur une méthode générique afin qu’elle n’accepte en type générique que des types qui implémentent une interface.

Soit l’interface suivante :

public interface IVolant
{
    void DeplierLesAiles();
    void Voler();
}

Qui est implémentée par deux objets :

public class Avion : IVolant
{
    public void DeplierLesAiles()
    {
        Console.WriteLine("Je déplie mes ailes mécaniques");
    }

    public void Voler()
    {
        Console.WriteLine("J'allume le moteur");
    }
}

public class Oiseau : IVolant
{
    public void DeplierLesAiles()
    {
        Console.WriteLine("Je déplie mes ailes d'oiseau");
    }

    public void Voler()
    {
        Console.WriteLine("Je bats des ailes");
    }
}

Nous pouvons créer une méthode générique qui s’occupe d’instancier ces objets et d’appeler les méthodes de l’interface :

public static T Creer<T>() where T : IVolant, new()
{
    T t = new T();
    t.DeplierLesAiles();
    t.Voler();
    return t;
}

Ici, la restriction se porte sur deux niveaux. Il faut dans un premier temps que le type générique implémente l’interface IVolant et possède également un constructeur, bref qu’il soit instanciable.

Nous pouvons donc utiliser cette méthode de cette façon :

Oiseau oiseau = Creer<Oiseau>();
Avion a380 = Creer<Avion>();

Nous appelons la méthode Créer() avec le type générique Oiseau, qui implémente bien IVolant et qui est aussi instanciable. Grâce à cela, nous pouvons utiliser l’opérateur new pour créer notre type générique, appeler les méthodes de l’interface et renvoyer l’instance.

Ce qui donne :

Je déplie mes ailes d’oiseau
Je bats des ailes
Je déplie mes ailes mécaniques
J’allume le moteur

Si nous tentons d’utiliser la méthode avec un type qui n’implémente pas l’interface IVolant, comme :

Voiture v = Creer<Voiture>();

Nous aurons l’erreur de compilation suivante :

Le type 'MaPremiereApplication.Voiture' ne peut pas être utilisé comme paramètre de type 'T' dans le type ou la méthode générique 'MaPremiereApplication.Program.Creer<T>()'. Il n'y a pas de conversion de référence implicite de 'MaPremiereApplication.Voiture' en 'MaPremiereApplication.IVolant'.

Globalement, il nous dit que l’objet Voiture n’implémente pas IVolant.

Oui mais dans ce cas, plutôt que d’utiliser une méthode générique, pourquoi la méthode ne renvoie pas IVolant ?

C’est une judicieuse remarque. Mais qui nécessite quelques modifications de code. En effet, il faudrait indiquer quel type instancier.

Nous pourrions par exemple faire :

public enum TypeDeVolant
{
    Oiseau,
    Avion
}

public static IVolant Creer(TypeDeVolant type)
{
    IVolant volant;
    switch (type)
    {
        case TypeDeVolant.Oiseau:
            volant = new Oiseau();
            break;
        case TypeDeVolant.Avion:
            volant = new Avion();
            break;
        default:
            return null;
    }
    volant.DeplierLesAiles();
    volant.Voler();
    return volant;
}

Et instancier nos objets de cette façon :

Oiseau oiseau = (Oiseau)Creer(TypeDeVolant.Oiseau);
Avion a380 = (Avion)Creer(TypeDeVolant.Avion);

Ce qui complique un peu les choses et rajoute des cast dont on pourrait volontiers se passer. De plus, si nous créons un nouvel objet qui implémente cette interface, il faudrait tout modifier.
Avouez qu’avec les types génériques, c’est quand même plus propre. :p

Nous pouvons bien sûr avoir des restrictions sur les types génériques d’une classe.
Pour le montrer, nous allons créer une classe dont l’objectif est d’avoir des types valeur qui pourraient ne pas avoir de valeur. Pour les types référence, il suffit d’utiliser le mot-clé null. Mais pour les types valeur comme les entiers, nous n’avons rien pour indiquer que ceux-ci n’ont pas de valeur.

Par exemple :

public class TypeValeurNull<T> where T : struct
{
    private bool aUneValeur;
    public bool AUneValeur
    {
        get { return aUneValeur; }
    }

    private T valeur;
    public T Valeur
    {
        get
        {
            if (aUneValeur)
                return valeur;
            throw new InvalidOperationException();
        }
        set
        {
            aUneValeur = true;
            valeur = value;
        }
    }
}

Ici, nous utilisons une classe possédant un type générique qui sera un type valeur, grâce à la condition where T : struct. Cette classe encapsule le type générique pour indiquer avec un booléen si le type a une valeur ou pas.
Ne faites pas attention à la ligne :

throw new InvalidOperationException();

qui permet juste de renvoyer une erreur, nous étudierons les exceptions un peu plus loin.
Elle pourra s’utiliser ainsi :

TypeValeurNull<int> entier = new TypeValeurNull<int>();
if (!entier.AUneValeur)
{
    Console.WriteLine("l'entier n'a pas de valeur");
}
entier.Valeur = 5;
if (entier.AUneValeur)
{
    Console.WriteLine("Valeur de l'entier : " + entier.Valeur);
}

Et si nous souhaitons avoir pareil pour un autre type valeur, il n’y a rien à faire de plus :

TypeValeurNull<double> valeur = new TypeValeurNull<double>();

C’est quand même super pratique comme classe !! Mais ne rêvons-pas, cette idée ne vient pas de moi. C’est en fait une fonctionnalité du framework .NET : les types nullables.

Les types nullables

En fait, la classe que nous avons vue au-dessus existe déjà dans le framework .NET, et en mieux ! Évidemment. Elle permet de faire exactement ce que j’ai décrit, c'est-à-dire permettre à un type valeur d’avoir une valeur nulle.
Elle est mieux faite dans la mesure où elle tire parti de certaines fonctionnalités du framework .NET qui en simplifie l’écriture. Il s’agit de la classe Nullable<>.

Aussi, nous pourrons créer un entier pouvant être null grâce au code suivant :

Nullable<int> entier = null;
if (!entier.HasValue)
{
    Console.WriteLine("l'entier n'a pas de valeur");
}
entier = 5;
if (entier.HasValue)
{
    Console.WriteLine("Valeur de l'entier : " + entier.Value);
}

Le principe est grosso modo le même sauf que nous pouvons utiliser le mot-clé null ou affecter directement la valeur à l’entier en utilisant l'opérateur d'affectation, sans passer par la propriété Valeur. Il peut aussi être comparé au mot-clé null ou être utilisé avec l’opérateur +, etc. Ceci est possible grâce à certaines fonctionnalités du C# que nous n’avons pas vues et qui sortent de l’étude de ce tutoriel.

Cette classe est tellement pratique que le compilateur a été optimisé pour simplifier son écriture. En effet, utiliser Nullable<> est un peu long pour nous autres informaticiens qui sommes des paresseux. :)
Aussi, l’écriture :

Nullable<int> entier = null;

peut se simplifier en :

int? entier = null;

c’est le point d’interrogation qui remplace la déclaration de la classe Nullable<>.

En résumé
  • Avec les génériques, vous pouvez créer des méthodes ou des classes qui sont indépendantes d'un type. On les appellera des méthodes génériques et des types génériques.

  • On utilise les chevrons <> pour indiquer le type d'une classe ou d'une méthode générique.

  • Les interfaces peuvent aussi être génériques, comme l'interface IEnumerable<>.

  • Les types nullables constituent un exemple d'utilisation très pratique des classes génériques.

Example of certificate of achievement
Example of certificate of achievement