Aller au menu - Aller au contenu

Icône TP types génériques

Mise à jour : 02/02/2012
Difficulté : Difficile Difficile Creative Commons BY-NC-SA
22 955 visites depuis 7 jours, dont 140 sur ce chapitre classé 15/786
Ahh, un peu de pratique histoire de vérifier que nous avons bien compris les génériques. C'est un concept assez facile à appréhender mais relativement difficile à mettre en œuvre. Quand en ai-je besoin ? Comment ?

Voici donc un petit exercice qui va vous permettre d'essayer de mettre en œuvre une classe générique.

Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Instructions pour réaliser la première partie du TP

Dans la première partie du TP, nous allons réaliser une liste chaînée. Il s’agit du grand classique des TP d’informatique en C. Je vous rappelle le principe.

La liste chaînée permet de naviguer d’élément en élément. Quand nous sommes sur le premier élément, le suivant est accessible par sa propriété Suivant. Lorsque nous accédons au suivant, l’élément précédent est accessible par la propriété Precedent et le suivant toujours accessible par la propriété Suivant. S’il n’y a pas de précédent ou pas de suivant, l’élément est null :

Image utilisateur


Si on insère un élément à la position 1, les autres se décalent :

Image utilisateur


Voilà, il faut donc créer une telle liste chaînée d’éléments. Le but est bien sûr de faire en sorte que l’élément soit générique.
N’hésitez pas à réfléchir un peu avant de vous lancer. Cela pourrait paraître un peu simpliste, mais en fait cela occasionne quelques nœuds au cerveau.

Toujours est-il que je souhaiterais disposer d’une propriété en lecture seule permettant d’accéder au premier élément ainsi qu’une autre propriété également en lecture seule permettant d’accéder au dernier élément. Bien sûr, il faut pouvoir naviguer d’élément en élément avec des propriétés précédent et suivant.

Il faut évidemment une méthode permettant d’ajouter un élément à la fin de la liste. Nous aurons également besoin d’une méthode permettant d’accéder à un élément à partir de son indice et enfin d’une méthode permettant d’insérer un élément à un indice, décalant tous les suivants. Voilà pour la création de la classe !

Ensuite, notre programme instanciera notre liste chaînée pour lui ajouter les entiers 5, 10 et 4. Puis nous afficherons les valeurs de cette liste en nous basant sur la première propriété et en naviguant d’élément en élément.
Nous afficherons ensuite les différents éléments en utilisant la méthode d’accès à un élément par son indice.
Enfin, nous insérerons la valeur 99 à la première position (position 0), puis la valeur 33 à la deuxième position et enfin la valeur 30 à nouveau à la deuxième position.
Puis nous afficherons tout ce beau monde.

Fin de l’énoncé, ouf ! :)

Pour ceux qui n’ont pas besoin d’aide, les explications sont terminées. Ouvrez vos Visual C# Express (ou vos Visual Studio si vous êtes riches ;) ) et à vos claviers.

Pour les autres, je vais essayer de vous guider un peu plus en essayant tout de même de ne pas trop vous donner d’indications non plus.

En fait, votre liste chainée n’est pas vraiment une liste, comme pourrait l’être la List<> que nous connaissons. Cette liste chainée possède un point d’entrée qui est le premier élément. L’ajout du premier élément est très simple, il suffit de mettre à jour une propriété. Pour ajouter l’élément suivant, il faut en fait brancher la propriété Suivant du premier élément à l’élément que nous sommes en train d’ajouter. Et inversement, la propriété Precedent de l’élément que nous souhaitons ajouter sera mise à jour avec le premier élément.

On se rend compte que l’élément est un peu plus complexe qu’un simple type. Nous allons donc avoir une classe générique possédant trois propriétés (Precedent, Suivant et Valeur). Et nous aurons également une classe du même type générique possédant la propriété Premier et la propriété Dernier et les méthodes d’ajout, d’obtention de l’élément et d’insertion.

Allez, je vous en ai assez dit. À vous de jouer ! ^^

Correction

Pas si facile hein ?
Mais bon, comme vous êtes super entrainés, cela n’a pas dû vous poser trop de problèmes.

Voici la correction que je propose.
La première chose à faire est de créer la classe générique permettant de stocker un élément :

Code : C#
1
2
3
4
5
6
public class Chainage<T>
{
    public Chainage<T> Precedent { get; set; }
    public Chainage<T> Suivant { get; set; }
    public T Valeur { get; set; }
}


C’est une classe générique toute simple qui possède une valeur du type générique et deux propriétés du même type que l’élément pour obtenir le précédent ou le suivant.
Peut-être que la plus grande difficulté réside ici, de bien modéliser la classe qui permet d’encapsuler l’élément.
Il faudra ensuite créer la liste générique et ses méthodes :

Code : C#
1
2
3
public class ListeChainee<T>
{
}


La liste chainée possède également un type générique. Nous créons sa propriété Premier :

Code : C#
1
2
3
4
public class ListeChainee<T>
{
    public Chainage<T> Premier { get; private set; }
}


Là, c’est très simple, il s’agit juste d’une propriété en lecture seule stockant le premier élément. C’est la méthode Ajouter() qui permettra de mettre à jour cette valeur. Notez quand même que nous utilisons le type générique comme type générique de la classe encapsulante.

Par contre, pour la propriété Dernier, c’est un peu plus compliqué. Pour la retrouver, nous allons parcourir tous les éléments à partir de la propriété Premier. Ce qui donne :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public Chainage<T> Dernier
    {
        get
        {
            if (Premier == null)
                return null;
            Chainage<T> dernier = Premier;
            while (dernier.Suivant != null)
            {
                dernier = dernier.Suivant;
            }
            return dernier;
        }
    }
}


On parcourt les éléments en bouclant sur la propriété Suivant, tant que celle-ci n’est pas nulle. Il s’agit là d’un parcours assez classique où on utilise une variable temporaire qui passe au suivant à chaque itération.

Nous pouvons à présent créer la méthode Ajouter :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public void Ajouter(T element)
    {
        if (Premier == null)
        {
            Premier = new Chainage<T> { Valeur = element };
        }
        else
        {
            Chainage<T> dernier = Dernier;
            dernier.Suivant = new Chainage<T> { Valeur = element, Precedent = dernier };
        }
    }
}


Cette méthode traite dans un premier temps le cas du premier élément. Il s’agit simplement de mettre à jour la propriété Premier. De même, grâce au calcul interne de la propriété Dernier, il sera facile d’ajouter un nouvel élément en se branchant sur la propriété Suivant du dernier élément.

Notez que vu que nous ne la renseignons pas, la propriété Suivant du nouvel élément sera bien à null.

Pour obtenir un élément à un indice donné, il suffira de reprendre le même principe que lors du parcours pour obtenir le dernier élément, sauf qu’il faudra s’arrêter au bon moment :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public Chainage<T> ObtenirElement(int indice)
    {
        Chainage<T> temp = Premier;
        for (int i = 1; i <= indice; i++)
        {
            if (temp == null)
                return null;
            temp = temp.Suivant;
        }
        return temp;
    }
}


Ici, plusieurs solutions. J’ai choisi d’utiliser une boucle for. Nous aurions très bien pu garder la boucle while comme pour la propriété Dernier.

Enfin, il ne reste plus qu’à insérer un élément :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public void Inserer(T element, int indice)
    {
        if (indice == 0)
        {
            Chainage<T> temp = Premier;
            Premier = new Chainage<T> { Suivant = temp, Valeur = element };
            temp.Precedent = Premier;
        }
        else
        {
            Chainage<T> elementAIndice = ObtenirElement(indice);
            if (elementAIndice == null)
                Ajouter(element);
            else
            {
                Chainage<T> precedent = elementAIndice.Precedent;
                Chainage<T> temp = precedent.Suivant;
                precedent.Suivant = new Chainage<T> { Valeur = element, Precedent = precedent, Suivant = temp };
            }
        }
    }
}


Nous traitons dans un premier temps le cas où l’on doit insérer l'en-tête. Il suffit de mettre à jour la valeur du premier en ayant au préalable décalé ce dernier d’un cran. Attention, si Premier est null, nous allons avoir un problème. Dans ce cas, soit nous laissons le problème, en effet, peut-on vraiment insérer un élément avant les autres s'il n'y en a pas ? Soit nous gérons le cas et décidons d'insérer l'élément en tant que Premier :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public void Inserer(T element, int indice)
    {
        if (indice == 0)
        {
            if (Premier == null)
                Premier = new Chainage<T> { Valeur = element };
            else
            {
                Chainage<T> temp = Premier;
                Premier = new Chainage<T> { Suivant = temp, Valeur = element };
                temp.Precedent = Premier;
            }
        }
        else
        {
            Chainage<T> elementAIndice = ObtenirElement(indice);
            if (elementAIndice == null)
                Ajouter(element);
            else
            {
                Chainage<T> precedent = elementAIndice.Precedent;
                Chainage<T> temp = precedent.Suivant;
                precedent.Suivant = new Chainage<T> { Valeur = element, Precedent = precedent, Suivant = temp };
            }
        }
    }
}


Pour les autres cas, si nous tentons d’insérer à un indice qui n’existe pas, nous insérons à la fin en utilisant la méthode Ajouter() existante. Sinon, on intercale le nouvel élément dans la liste en prenant soin de brancher le précédent sur notre nouvel élément et de brancher le suivant sur notre nouvel élément.

Voilà pour notre classe.

Reste à utiliser notre classe :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main(string[] args)
{
    ListeChainee<int> listeChainee = new ListeChainee<int>();
    listeChainee.Ajouter(5);
    listeChainee.Ajouter(10);
    listeChainee.Ajouter(4);
    Console.WriteLine(listeChainee.Premier.Valeur);
    Console.WriteLine(listeChainee.Premier.Suivant.Valeur);
    Console.WriteLine(listeChainee.Premier.Suivant.Suivant.Valeur);
    Console.WriteLine("*************");
    Console.WriteLine(listeChainee.ObtenirElement(0).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(1).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(2).Valeur);
    Console.WriteLine("*************");
    listeChainee.Inserer(99, 0);
    listeChainee.Inserer(33, 2);
    listeChainee.Inserer(30, 2);
    Console.WriteLine(listeChainee.ObtenirElement(0).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(1).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(2).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(3).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(4).Valeur);
    Console.WriteLine(listeChainee.ObtenirElement(5).Valeur);
}


Ce qui nous donnera :

Image utilisateur

Instructions pour réaliser la deuxième partie du TP

Bon, c’est très bien de pouvoir accéder à un élément par son indice. Mais une liste sur laquelle on ne peut pas faire un foreach, c’est quand même bien dommage.
Attaquons désormais la deuxième partie du TP. Toujours dans l’optique de manipuler les génériques, nous allons faire en sorte que notre liste chainée puisse être parcourable en utilisant un foreach.

Nous avons dit plus haut qu’il suffisait d’implémenter l’interface IEnumerable. En l’occurrence, nous allons implémenter sa version générique, vu que nous travaillons avec une classe générique.

Voilà le but de ce TP. Si vous vous le sentez, allez-y ! :)

Je pense par contre que vous allez avoir besoin d’être un peu guidés car c’est une opération un peu particulière.
Vous l’aurez deviné, il faut que notre liste implémente l’interface IEnumerable<T>. Le fait d’implémenter cette interface va vous forcer à implémenter deux méthodes GetEnumerator(), la version normale et la version explicite. Sachez dès à présent que les deux méthodes feront exactement la même chose.

Mais, qu’est-ce qu’il raconte ? Implémenter une interface explicitement ? On n’a jamais vu ça !


C’est vrai ! Allez, je vous en parle après la correction. Pour l’instant, cela ne devrait pas vous perturber car les deux méthodes font exactement la même chose. En l’occurrence, elles renverront un Enumerator personnalisé.

Il va donc falloir créer cet Enumerator qui va s’occuper de la mécanique permettant de naviguer dans notre liste. Il s’agit d’une nouvelle classe qui va devoir implémenter l’interface IEnumerator<T>, c’est-à-dire :

Code : C#
1
2
3
public class ListeChaineeEnumerator<T> : IEnumerator<T>
{
}


Cette interface permet d’indiquer que notre enumérateur va respecter le contrat lui permettant de fonctionner avec un foreach.
Avec cette interface, vous allez devoir implémenter :
  • La propriété Current.
  • La propriété explicite Current (qui sera la même chose que la précédente).
  • La méthode MoveNext qui permet de passer à l’élément suivant.
  • La méthode Reset, qui permet de revenir au début de la liste.
  • Et la méthode Dispose.


La méthode Dispose est en fait héritée de l’interface IDisposable dont hérite l’interface IEnumerator<T>. C’est une interface particulière qui offre l’opportunité de faire tout ce qu’il faut pour nettoyer la classe, c’est-à-dire libérer les variables qui en auraient besoin. En l’occurrence, ici nous n’aurons rien à faire mais il faut quand même que la méthode soit présente. Elle sera donc vide.

Pour implémenter les autres méthodes, il faut que l’énumérateur connaisse la liste qu’il doit énumérer. Il faudra donc que la classe ListeChaineeEnumerator prenne en paramètre de son constructeur la liste à énumérer. Dans ce constructeur, on initialise la variable membre indice qui contient l’indice courant.
La propriété Current renverra l’élément à l’indice courant.
La méthode MoveNext passe à l’élément suivant et renvoie faux s’il n’y a plus d’éléments, vrai sinon.
Enfin la méthode Reset repasse l’indice à sa valeur initiale.

À noter que la valeur initiale de l’indice est -1, car la boucle foreach commence par appeler la méthode MoveNext qui commence par aller à l’élément suivant, c’est-à-dire à l’élément 0.
Il ne reste plus qu’à vous dire exactement quoi mettre dans les méthodes GetEnumerator de la liste chainée, car vous ne trouverez peut-être pas du premier coup :

Code : C#
1
2
3
4
5
6
7
8
9
public IEnumerator<T> GetEnumerator()
{
    return new ListeChaineeEnumerator<T>(this);
}

IEnumerator IEnumerable.GetEnumerator()
{
    return new ListeChaineeEnumerator<T>(this);
}


C’est à vous de jouer pour la suite.

Correction

Encore moins facile. Tant qu’on ne l’a pas fait une première fois, implémenter l’interface IEnumerable est un peu déroutant. Après, c’est toujours pareil.

Voici donc ma correction. Tout d’abord, la liste chaînée doit implémenter IEnumerable<T>, ce qui donne :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ListeChainee<T> : IEnumerable<T>
{
    […Code identique au TP précédent…]

    public IEnumerator<T> GetEnumerator()
    {
        return new ListeChaineeEnumerator<T>(this);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ListeChaineeEnumerator<T>(this);
    }
}


Là, c’est du tout cuit vu que je vous avais donné la solution un peu plus tôt ! :) J’espère que vous avez au moins réussi ça. ^^

Maintenant, il faut donc créer un nouvel énumérateur personnalisé en lui passant notre liste chainée en paramètres.
Cet énumérateur doit implémenter l’interface IEnumerator, ce qui donne :

Code : C#
1
2
3
public class ListeChaineeEnumerator<T> : IEnumerator<T>
{
}


Comme prévu, il faut donc un constructeur qui prend en paramètre la liste chainée :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ListeChaineeEnumerator<T> : IEnumerator<T>
{
    private int indice;
    private ListeChainee<T> listeChainee;
    public ListeChaineeEnumerator(ListeChainee<T> liste)
    {
        indice = -1;
        listeChainee = liste;
    }

    public void Dispose()
    {
    }
}


Cette liste sera enregistrée dans une variable membre de la classe. Tant que nous y sommes, nous ajoutons un indice privé que nous initialisons à -1, comme déjà expliqué.
Notez également que la méthode Dispose() est vide. Reste à implémenter le reste des méthodes :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ListeChaineeEnumerator<T> : IEnumerator<T>
{
    private int indice;
    private ListeChainee<T> listeChainee;
    public ListeChaineeEnumerator(ListeChainee<T> liste)
    {
        indice = -1;
        listeChainee = liste;
    }

    public void Dispose()
    {
    }

    public bool MoveNext()
    {
        indice++;
        Chainage<T> element = listeChainee.ObtenirElement(indice);
        return element != null;
    }

    public T Current
    {
        get 
        {
            Chainage<T> element = listeChainee.ObtenirElement(indice);
            if (element == null)
                return default(T);
            return element.Valeur; 
        }
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public void Reset()
    {
        indice = -1;
    }
}


Commençons par la méthode MoveNext(). Elle passe à l’indice suivant et renvoie faux ou vrai en fonction de si on arrive au bout de la liste ou pas. N’oubliez pas que c’est la première méthode qui sera appelée dans le foreach, donc pour passer à l’élément suivant, on incrémente l’indice pour le positionner à l’élément 0. C’est pour cela que l’indice a été initialisé à -1. On utilise ensuite la méthode existante de la liste pour obtenir l’élément à un indice afin de savoir si notre liste peut continuer à s’énumérer.

La propriété Current renvoie l’élément à l’indice courant, pour cela on utilise l’indice pour accéder à l’élément courant, en utilisant les méthodes de la liste. L’autre propriété Current fait la même chose, il suffit d’appeler la propriété Current.

Enfin, la méthode Reset permet de réinitialiser l’énumérateur en retournant à l’indice initial.

Finalement, ce n’est pas si compliqué que ça. Mais il faut avouer que la première fois, c’est un peu déroutant.

À mon sens, c’est un bon exercice pratique. Peut-être que mes explications ont suffi à vous guider. Sans doute avez-vous dû regarder un peu la documentation de IEnumerable sur internet. Dans tous les cas, devoir implémenter une interface du framework .NET est une situation que vous allez fréquemment devoir rencontrer. Il est bon de s’y entrainer !

Aller plus loin

Vous me direz qu’il fallait le deviner qu’on avait besoin d’une classe indépendante qui permettait de gérer l’énumérateur.

En fait, ce n’est pas obligatoire. On peut très bien faire en sorte que notre classe gère la liste chainée et son énumérateur. Il suffit de faire en sorte que la liste chainée implémente également IEnumerator<T> et de gérer la logique à l’intérieur de la classe.

Par contre, ce n'est pas recommandé. D'une manière générale il est bien qu'une classe n'ait à s'occuper que d'une seule chose. On appelle cela le principe de responsabilité unique (en anglais SRP : Single Responsibility Principle). Plus une classe fait de choses et plus une modification impacte les autres choses. Ici, il est judicieux de garder le découplage des deux classes.

Il y a quand même une chose que l'on peut améliorer dans le code de la correction. En effet, cette liste n’est pas extrêmement optimisée car lorsque nous obtenons un élément, nous re-parcourons toute la liste depuis le début, notamment dans le cas de la gestion de l’énumérateur. Il pourrait être judicieux qu’à chaque foreach, nous ne parcourions pas tous les éléments et qu'on évite d'appeler continuellement la méthode ObtenirElement().
Cela pourrait se faire en éliminant l’indice et en utilisant une variable de type Chainage<T>, par exemple :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ListeChaineeEnumerator<T> : IEnumerator<T>
{
    private Chainage<T> courant;
    private ListeChainee<T> listeChainee;
    public ListeChaineeEnumerator(ListeChainee<T> liste)
    {
        courant = null;
        listeChainee = liste;
    }

    public void Dispose()
    {
    }

    public bool MoveNext()
    {
        if (courant == null)
            courant = listeChainee.Premier;
        else
            courant = courant.Suivant;

        return courant != null;
    }

    public T Current
    {
        get
        {
            if (courant == null)
                return default(T);
            return courant.Valeur;
        }
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public void Reset()
    {
        courant = null;
    }
}


Ici, c’est la variable courant qui nous permet d’itérer au fur et à mesure de la liste chainée. C’est le même principe que dans la méthode ObtenirElement, sauf qu’on ne re-parcoure pas toute la liste à chaque fois. Dans cet exemple, l'optimisation est négligeable. Elle peut s’avérer intéressante si notre liste grossit énormément. Dans tous les cas, ça ne fait pas de mal d’aller plus vite. :)

Remarquons avant de terminer qu'il est possible de simplifier encore la classe grâce à un mot-clé que nous découvrirons dans la partie suivante : yield. Il permet de créer facilement des énumérateurs. Ce qui fait que le code complet de la liste chainée pourra être :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class ListeChainee<T> : IEnumerable<T>
{
    public Chainage<T> Premier { get; private set; }

    public Chainage<T> Dernier
    {
        get
        {
            if (Premier == null)
                return null;
            Chainage<T> dernier = Premier;
            while (dernier.Suivant != null)
            {
                dernier = dernier.Suivant;
            }
            return dernier;
        }
    }

    public void Ajouter(T element)
    {
        if (Premier == null)
        {
            Premier = new Chainage<T> { Valeur = element };
        }
        else
        {
            Chainage<T> dernier = Dernier;
            dernier.Suivant = new Chainage<T> { Valeur = element, Precedent = dernier };
        }
    }

    public Chainage<T> ObtenirElement(int indice)
    {
        Chainage<T> temp = Premier;
        for (int i = 1; i <= indice; i++)
        {
            if (temp == null)
                return null;
            temp = temp.Suivant;
        }
        return temp;
    }

    public void Inserer(T element, int indice)
    {
        if (indice == 0)
        {
            if (Premier == null)
                Premier = new Chainage<T> { Valeur = element };
            else
            {
                Chainage<T> temp = Premier;
                Premier = new Chainage<T> { Suivant = temp, Valeur = element };
                temp.Precedent = Premier;
            }
        }
        else
        {
            Chainage<T> elementAIndice = ObtenirElement(indice);
            if (elementAIndice == null)
                Ajouter(element);
            else
            {
                Chainage<T> precedent = elementAIndice.Precedent;
                Chainage<T> temp = precedent.Suivant;
                precedent.Suivant = new Chainage<T> { Valeur = element, Precedent = precedent, Suivant = temp };
            }
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        Chainage<T> courant = Premier;
        while (courant != null)
        {
            yield return courant.Valeur;
            courant = courant.Suivant;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}


Remarquons que nous n'avons plus besoin de la classe ListeChaineeEnumerator.
L'implémentation devient très facile. Nous reviendrons sur ce mot-clé dans la partie suivante.

Implémenter une interface explicitement

J’en profite ici pour faire un aparté sur l’implémentation d’interface explicite.

J’ai choisi délibérément de ne pas le mettre dans le chapitre des interfaces car c’est un cas relativement rare mais qui se produit justement quand on implémente l’interface IEnumerable<T>.
Cela vient du fait que l’interface IEnumerable, non générique, expose une propriété Current. De même, l’interface IEnumerable<T>, générique, qui hérite de IEnumerable, expose également une propriété Current.

Il y a donc une ambiguïté car les deux propriétés portent le même nom, mais ne renvoient pas la même chose. Ce qui est contraire aux règles que nous avons déjà vues. Pour faire la différence, il suffira de préfixer la propriété par le nom de l’interface et de ne pas mettre le mot-clé public.

L’implémentation explicite a également un intérêt dans le code suivant :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface ICarnivore
{
    void Manger();
}

public interface IFrugivore
{
    void Manger();
}

public class Homme : ICarnivore, IFrugivore
{
    public void Manger()
    {
        Console.WriteLine("Je mange");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Homme homme = new Homme();
        homme.Manger();
        ((ICarnivore)homme).Manger();
        ((IFrugivore)homme).Manger();
    }
}


Ici, ce code compile car la classe Homme implémente la méthode Manger qui est commune aux deux interfaces. Par contre, il n’est pas possible de faire la distinction entre le fait de manger en tant qu’homme, en tant que ICarnivore ou en tant que IFrugivore.

Ce code affichera :

Code : Console
Je mange
Je mange
Je mange


Si c’est le comportement attendu, tant mieux. Si ce n’est pas le cas, il va falloir implémenter au moins une des interfaces de manière explicite :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Homme : ICarnivore, IFrugivore
{
    public void Manger()
    {
        Console.WriteLine("Je mange");
    }

    void IFrugivore.Manger()
    {
        Console.WriteLine("Je mange en tant que IFrugivore");
    }

    void ICarnivore.Manger()
    {
        Console.WriteLine("Je mange en tant que ICarnivore");
    }
}


Avec ce code, notre exemple affichera :

Code : Console
Je mange
Je mange en tant que ICarnivore
Je mange en tant que IFrugivore


Si vous vous rappelez, nous avions vu au moment du chapitre sur les interfaces que Visual C# Express nous proposait de nous aider dans l’implémentation de l’interface. Par le bouton droit, vous aviez également accès à sous menu « implémenter l’interface explicitement ». Vous pouvez vous en servir dans ce cas précis.

Je m’arrête là sur l’implémentation d’interface explicite, même s’il y aurait d’autres points à voir. Globalement dans la vraie vie, ils ne vous serviront jamais.
Voilà pour ce TP. Nous avons créé une classe générique permettant de gérer les listes chainées. Ceci nous a permis de manipuler ces types ô-combien indispensables et de nous entrainer à la généricité.
Nous en avons même profité pour voir comment faire en sorte qu’une classe soit énumérable, en implémentant la version générique de IEnumerable.

Notez bien sûr que cette classe est fonctionnellement incomplète. Il aurait été judicieux de rajouter une méthode permettant de supprimer un élément par exemple. D’ailleurs, n’hésitez pas à la créer et à la proposer si vous souhaitez continuer à vous entrainer. D’autres méthodes pourraient être intéressantes, comme vider la liste d’un seul coup…

J’espère que ce TP n’a pas été trop compliqué à réaliser. ^^

À noter qu’une classe qui fait à peu près le même travail existe dans le framework .NET, elle s’appelle LinkedList.
Chapitre précédent Sommaire Chapitre suivant

Partager

4 commentaires pour "TP types génériques"
Note moyenne : 3.05 / 4 (230 votes)
Pseudo Commentaire
Hors ligne AH_Design # Posté le 05/03/2012 à 21:34:17

salut nico.pyright
j'ai juste une petite correction a faire sur ton TP
tu as écris dans la premiere partie du TP pour la méthode Insérer

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public void Inserer(T element, int indice)
    {
        if (indice == 0)
        {
            if (Premier == null)
                Premier = new Chainage<T> { Valeur = element };
            else
            {
                Chainage<T> temp = Premier;
                Premier = new Chainage<T> { Suivant = temp, Valeur = element };
                temp.Precedent = Premier;
            }
        }
        else
        {
            Chainage<T> elementAIndice = ObtenirElement(indice);
            if (elementAIndice == null)
                Ajouter(element);
            else
            {
                Chainage<T> precedent = elementAIndice.Precedent;
                Chainage<T> temp = precedent.Suivant;
                precedent.Suivant = new Chainage<T> { Valeur = element, Precedent = precedent, Suivant = temp };
            }
        }
    }
}


maintenant si tu testes ce code la dans Program.cs:

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Program
{
    static void Main(string[] args)
    {
        ListeChainee<int> listeChainee = new ListeChainee<int>();
        listeChainee.Ajouter(5);
        listeChainee.Ajouter(10);
        listeChainee.Ajouter(4);
        Console.WriteLine("**********");
        linkedList.Ajouter(99, 1);
        Console.WriteLine(linkedList.GetItem(1).Previous.Value);
        Console.WriteLine(linkedList.GetItem(1).Value);
        Console.WriteLine(linkedList.GetItem(1).Next.Value);
        // affiche 5 - 99 - 10, pour l'instant ca va
        Console.WriteLine("**********");
        Console.WriteLine(linkedList.GetItem(2).Previous.Value);
        Console.WriteLine(linkedList.GetItem(1).Value);
        // affiche 5 - 99 !!!!! alors que ça devrait être la même valeur

        Console.ReadLine();
    }
}


pour corriger cette erreur, la méthode Insérer doit être:

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public void Inserer(T element, int indice)
    {
        if (indice == 0)
        {
            if (Premier == null)
                Premier = new Chainage<T> { Valeur = element };
            else
            {
                Chainage<T> temp = Premier;
                Premier = new Chainage<T> { Suivant = temp, Valeur = element };
                temp.Precedent = Premier;
            }
        }
        else
        {
            Chainage<T> elementAIndice = ObtenirElement(indice);
            if (elementAIndice == null)
                Ajouter(element);
            else
            {
                Chainage<T> precedent = elementAIndice.Precedent;
                Chainage<T> temp = precedent.Suivant;
                // j'ai aussi relie elementAIndice.Precedent avec le nouvelle element
                precedent.Suivant = elementAIndice.Precedent = new Chainage<T> { Valeur = element, Precedent = precedent, Suivant = temp };
            }
        }
    }
}


je profites de l'occasion pour te remercier pour cette excellent tuto!
AH_Design
Hors ligne ParkKatt # Posté le 05/04/2012 à 08:04:28

Avis : Très bon

Hello,

J'ai également une question sur ce TP. J'ai bien compris la remarque de AH_Design, mais n'y a-t-il pas moyen de simplifier notre méthode insérer :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ListeChainee<T>
{
    […Code supprimé pour plus de clarté…]

    public void Inserer(T element, int indice)
    {
        if (indice == 0)
        {
            if (Premier == null)
                Premier = new Chainage<T> { Valeur = element };
            else
            {
                Chainage<T> temp = Premier;
                Premier = new Chainage<T> { Suivant = temp, Valeur = element };
                temp.Precedent = Premier;
            }
        }
        else
        {
            Chainage<T> elementAIndice = ObtenirElement(indice);
            if (elementAIndice == null)
                Ajouter(element);
            else
            {
                Chainage<T> precedent = elementAIndice.Precedent;
                Chainage<T> temp = precedent.Suivant;//Ligne inutile ??
                precedent.Suivant = elementAIndice.Precedent = new Chainage<T> { Valeur = element, Precedent = precedent, Suivant = elementAIndice };//Je change temp en elementAIndice
            }
        }
    }
}


Pr résumer je me passe juste de l'objet temp, et utilise elementAIndice à la place. Il me semble que c'est la même chose. Est-ce possible ?

Merci.
Hors ligne gfox78 # Posté le 30/04/2012 à 16:45:51
Avatar

Avis : Très bon

Ville : Marcq
Pays : France métropolitaine

Bonjour,
La première partie du TP sur les listes chainées génériques ne m'a pas posé trop de problèmes. Par contre, la deuxième partie, qui ne peut se faire que par copie du corrigé par un débutant, m'a posé problème dès le début, juste après la phrase:

Citation : nico.pyright
Là, c’est du tout cuit vu que je vous avais donné la solution un peu plus tôt ! J’espère que vous avez au moins réussi ça.


L'addition du code proposé génère une erreur avec un message d'erreur peu compréhensible par un débutant: "L'utilisation du type 'System.Collections.Generic.IEnumerable<T>' générique requiert les arguments de type 1.".
J'y ai perdu beaucoup de temps!!
Je pense que l'auteur devrait signaler ce risque d'erreur et encourager le débutant à consulter la 'library' sur 'IEnumerator, interface' ou plus précisément d'ajouter un 'using'.
Secret (cliquez pour afficher)
Code : PHP
1
using System.Collections;


Pendant que j'y suis, je voudrais signaler une petite maladresse dans le style, un peu après le premier code de 'ListeChaineeEnumerator' (avant 'ALLEZ PLUS LOIN'):

Citation
Commençons par la méthode MoveNext(). Elle passe à l’indice suivant et renvoie faux ou vrai en fonction de si on arrive au bout de la liste ou pas.

Je pense que les mots soulignés sont de trop.
Amicalement
Hors ligne Eagleseb # Posté le 02/05/2012 à 13:01:34
Avatar

Avis : Décevant

En implémentant l'interface IList, ce tp aurait peut être plus simple pour les débutant cela aurait également permis d'illustrer la puissance des interfaces et du framework.NET. Je n'ai donc pas du tout géré ce tp de la même façon que l'auteur.

Voir tous les commentaires