5.13.approches abstraites différentes
    pour l’implantation de listes de chaînes



    Plan du chapitre:

    Introduction

    1. Quelques méthodes d’implantation de ce TAD 
     

1.1 Niveau procédural
1.2 Niveau unité
1.3 Niveau classe à partir d’une unité existante
1.4 Niveau classe non visuelle avec héritage : TStringList
1.5 Niveau classe visuelle avec héritage : TListBox


2. Encore plus de réutilisation et d’abstraction 
  2.1 Un outil logiciel réutilisable : le composant liste visuelle
2.2 Un degré plus haut : une liste visuelle générique



Introduction

LesAPI\Lrndelfi.dif
 

Rappelons que le TAD liste linéaire de chaînes a déjà été étudié et implanté en pascal.
 
TAD Liste
utilise : N{entiers}, T0=chaîne de caractères , Place
Champs : (a1,.....,an) suite finie dans T0
opérations
 liste_vide : ® Liste
 acces : Liste x N ® Place
 contenu : Place ® T0
 kème : Liste x N ® T0
 long : Liste ® N
 supprimer : Liste x N ® Liste
 inserer : Liste x N x T0® Liste
 succ : Place ® Place

préconditions :
 acces(L,k) def_ssi 1 £ k £ long(L)
 supprimer(L,k) def_ssi 1 £ k £ long(L)
 inserer(L,k,e) def_ssi 1 £ k £ long(L)+1
 kème(L,k) def_ssi 1 £ k £ long(L)
Fin-Liste
 


 
 

1. Quelques méthodes d’implantation de ce TAD
 

Nous allons utiliser les possibilités de l’outil Delphi pour implanter ce genre de structure de données. Nous classerons nos implantations selon plusieurs niveaux d’abstraction. Ce qui nous permettra sur cet exemple de comparer les différents efforts consacrés à ces implantations et leurs résultats eu égard à la réutilisation et à la protection. Nous nous poserons à chaque fois la question suivante : " comment pouvons-nous nous servir du code développé dans un autre logiciel ? ".
 
 

1.1 Niveau procédural

Pascal.Unit_listlin\Plistchn.pas

     
Nous ne nous attarderons pas sur ce niveau qui est le plus bas ; il suffit de dire qu’à ce niveau l’on a construit un programme pascal P avec des procédures et des fonctions.
 

Effort d’implantation :
Cet effort est maximal puisque nous avons choisi de simuler la liste dans un tableau. Par exemple pour la procédure supprimer, nous pourrions avoir le code source suivant :
 
procedure supprimer (var L: liste; rang: integer);
var
 n: 0..max_elt;
 i: 1..max_elt;
begin
 if not Est_vide(L) then
 begin
  n := longueur(L);
  if rang in [1..n] then {le rang est correct}
  begin
   if (n=1)or(rang=n) then 
    L.long :=n-1 {un seul élément ou le dernier}
   else
   begin {n>1 et rang < n}
    for i := rang to n - 1 do
     L.suite[i] := L.suite[i + 1];
    L.long := n - 1
   end
  end
 end
end;{supprimer}

Réutilisation :
La réutilisation de cet effort est pratiquement inexistante. Nous pourrons uniquement recopier manuellement les types, les fonctions et les procédures de P dans le code source d’un autre logiciel.
 

Protection :
Le niveau de protection du code contre des erreurs de programmation est relativement faible. La seule protection que nous ayons est apportée par la visibilité du code en pascal (local, global, paramètres formels et effectifs).
 
 
 

1.2 Niveau unité

Pascal.Unit_listlin\Ulistchn.pas &  Delphi.ListeChaines \Liste0.Dlfi
 

C’est ainsi que nous avons précédemment implanté ce TAD par traduction du TAD en la partie interface d’une unité :
 
Unit UListchn;
interface
const
 max_elt = 1000;
type
 T0 = string;
 liste = record
   suite: array[1..max_elt] of T0;
   long: 0..max_elt;
   init_ok:char;
 end;

function Est_vide(L:liste):boolean;
function longueur (L: liste): integer;
procedure supprimer (var L: liste; rang: integer);
procedure inserer (var L: liste; rang: integer; x: T0);
function kieme (L: liste; rang: integer): T0;
procedure ajouter(var L:liste;x:T0);
procedure effacer(var L:liste);
function Test (L: liste; x: T0): boolean;
procedure Rechercher (L: liste; x: T0; var place: integer);
procedure init_liste(var L:liste);

implementation ...

Effort d’implantation :
Cet effort est identique au précédent car là aussi nous avons choisi de simuler la liste dans un tableau (par exemple le code source de l’opération supprimer reste identique à celui du paragraphe précédent).
 

Réutilisation :
La réutilisation existe et se fait par utilisation du code source à travers la clause uses de l’unité " Unit Ulistchn " dans le logiciel à développer.
 

Protection :
La protection est améliorée. Elle cumule le niveau précédent au masquage du code source dans la partie " implementation " (partie privée de l’unité).

Il reste un problème lié au fait qu’il est impossible d’empêcher des effets de bords entre plusieurs unités. Nous avons observé que certains étudiants pratiquaient par étourderie dans des unités, des redéfinitions de variables ou de types. Le compilateur n’a pas pu déceler l’erreur et a interprété cette erreur comme un masquage d’identificateur.

Remarquons qu’avec des langages comme Ada ou Java ce type d’erreur a été corrigé et n’est pas possible.
 
 

1.3 Niveau classe à partir d’une unité existante

Delphi.ListeChaines \Liste1Class.Dlfi
 

Nous reprenons l’unité précédente et nous pratiquons une réorganisation du code sous forme d’une classe Delphi.
 
const
 max_elt = 1000;
type
 T0 = string;
 liste =class
  public
   function Est_vide:boolean;
   function longueur : integer;
   procedure supprimer (rang: integer);
   procedure inserer (rang: integer; x: T0);
   function kieme (rang: integer): T0;
   procedure ajouter(x:T0);
   procedure effacer;
   function Test (x: T0): boolean;
   procedure Rechercher (x: T0; var place: integer);
   procedure init_liste;
   procedure creationListe(Box:TListBox);
  private
   suite: array[1..max_elt] of T0;
   long: 0..max_elt;
   init_ok:char;
   procedure Test_Recherche(x:T0;var trouve:boolean;
          var rang:integer);
 end;

Effort d’implantation :
Cet effort est identique au précédent car là encore nous avons choisi de simuler la liste dans un tableau. Soit à titre d’exemple le code source de l’opération supprimer :
 
procedure liste.supprimer (rang: integer);
var
 n: 0..max_elt;
 i: 1..max_elt;
begin
 if not self.Est_vide then
 begin
   n := self.longueur;
   if rang in [1..n] then {le rang est correct}
   begin
    if (n=1)or(rang=n) then 
     self.long :=n-1 {un seul élément ou le dernier}
    else
    begin{n>1 et rang < n}
     for i := rang to n - 1 do
      self.suite[i] := self.suite[i + 1];
     self.long := n - 1
    end
   end
 end
end;{supprimer}

Réutilisation :
La réutilisation est meilleure car au lieu d’utiliser du code source à travers la clause uses de l’unité " Unit Ulistchn ", nous pouvons nous servir de la classe " liste " comme d’un type pascal dans le logiciel à développer (var x : liste ; ...).
 

Protection :
La protection est accrue. Elle cumule ici aussi le niveau précédent avec le concept d’encapsulation de l’information dans la classe elle-même. L’étourderie signalée précédemment n’existe plus puisque des champs de deux classes C1 et C2 peuvent avoir le même nom (le principe de la référence uniforme C1.Monchamp et C2.Monchamp différencie les deux données Monchamp de chacune des deux classes C1 et C2.).
 

Nous allons maintenant utiliser pour nos listes de chaînes une autre démarche d’implantation. Nous cherchons s’il existe dans Delphi une classe qui effectue déjà ces actions sur les chaînes. La classe des TStringList qui est une classe interne importante de Delphi correspond en fait à notre attente.
 
 

     
1.4 Niveau classe non visuelle avec héritage : TStringList Delphi.ListeChaines \Liste2ObjBase.Dlfi
 

Reprenons un résumé de la documentation Borland sur cette classe.

Un objet TStringList gère une liste de chaînes.
 

Les principales méthodes du TStringList :
 

 
Deux attributs d’un TStringList :

Les attributs en Delphi se dénomment des propriétés.
 
property Count
property Strings

TStringList.Count

La propriété Count contient le nombre de chaînes dans la liste.

property Count: Integer;

TStringList.Strings

La propriété Strings permet d'accéder(lire ou modifier) à une chaîne particulière à partir de sa position (indice) dans la liste, la première chaîne étant référencée par l’indice 0. 

property Strings[Index: Integer]: string;

 
Exemple :
MonStringList.Strings[0] := 'la première chaîne';
MonStringList.Strings[1] := 'la deuxième chaîne';
 

Un TStringList est sensible à deux événements :

L’objet réagit aux deux événements OnChange et OnChanging dont on rappelle qu’en Delphi ce sont des propriétés (des attributs) :
 
property OnChange: TNotifyEvent;
property OnChanging: TNotifyEvent;

 

TStringList.OnChange et TStringList.OnChanging
 
OnChange se produit immédiatement après une modification de la liste de chaînes.

OnChanging se produit juste avant une modification de la liste de chaînes.

Dès lors que des chaînes de la liste sont ajoutées, supprimées, déplacées ou modifiées, trois événements ont lieu dans l’ordre suivant :
 
L’événement OnChanging se produit avant les modifications.

Les chaînes sont ajoutées, supprimées, déplacées ou modifiées.

L’événement OnChange se produit ensuite.


 
 

Notre classe de liste héritant du TStringList :
 
type
 T0 = string;
 liste =class(TStringList) //héritage de la classe TStringList
  public;
   function longueur : integer;
   procedure supprimer (k: integer);
   procedure inserer (k: integer; x: T0);
   function kieme (n: integer): T0;
   function Test (x: T0): boolean;
   procedure Rechercher (x: T0; var place: integer);
   procedure Ajouter(x:T0);
   procedure effacer;
  private
   function Est_vide:boolean;
end;

Effort d’implantation :
Cet effort est très faible car nous héritons des propriétés et des méthodes de la classe TStringList. Il nous suffit d’encapsuler les méthodes de TStringList pour construire nos propres méthodes. Soit par exemple le code source de supprimer, inserer, kieme :
 
implementation

procedure liste.supprimer (k: integer);
begin
 self.Delete(k-1); //méthode de l’ancêtre
end;

procedure liste.inserer (k: integer; x: T0);
begin
 self.Insert(k-1,x); //méthode de l’ancêtre
end;

function liste.kieme (n: integer): T0;
begin
 kieme:=self.strings[n-1] //méthode de l’ancêtre
end;

Le constat est clair :écrire ce code a demandé peu de temps, car ici nous encapsulons des méthodes existantes !
 

Réutilisation :
La réutilisation est la même que pour le niveau précédent. Nous pouvons nous servir de la classe " liste " comme d’un type pascal dans le logiciel à développer (var x : liste ; ...).
 

Protection :
La protection est très améliorée par rapport au niveau précédent. En effet la classe ancêtre TStringList possède des protections intrinsèques dont nous avons hérité sans avoir à les développer nous mêmes.
 

Nous sommes ici au sommet de ce que nous pouvons espérer, dans ce cas et avec cet outil à tous égards.
 
 

Amélioration possible quant au chargement des données :

Telle qu’elle est définie dans le paragraphe précédent la classe liste bénéficie de la méthode de chargement des chaînes LoadFromFile de son ancêtre.

Etant en environnement visuel, il serait dommage de ne pas en profiter ; c’est pourquoi nous ajoutons dans la partie " public " de la classe pour l’exemple, une nouvelle méthode de chargement CreationListe : elle permet de recopier le contenu des données d’un objet de la classe visuelle des TListBox dans notre liste.
 

Voici le code correspondant à cette méthode :
 
liste =class(TStringList) //héritage de la classe TStringList
 public;
   function longueur : integer;
   procedure creationListe(Box:TListBox);
   ....... le reste identique

implementation

.....

procedure liste.creationListe(Box:TListBox);
{création de la liste (le ListBox est déjà chargé manuellement}
begin
 self.AddStrings(Box.Items);
end;


Vous trouverez dans le dossier ci-dessous, un exemple d’utilisation de la classe liste basée sur le TstringList.

Delphi.ListeChaines \Liste3PiLifo.Dlfi
 

Il s’agit de l’implantation du TAD pile LIFO fondé sur l’implantation d’une liste. Rappel du TAD Pile LIFO :
 
TAD: PiLifo

Utilise:
 {Vrai,Faux}
 T0 = chaînes de caractères
Opérations:
 fond : à PiLifo
 Est_Vide : PiLifo à {Vrai,Faux}
 Empiler : PiLifo x T0 à PiLifo 
 Depiler : PiLifo à PiLifo x T0 
 premier : PiLifo à T0
Préconditions:
 Depiler ( P ) def_ssi Est_vide ( P ) = Faux
 premier ( P ) def_ssi Est_vide ( P ) = Faux
FinTAD_Pilifo

Voici la classe PiLifo dérivée de la classe liste précédente :
 
type
 T0 = string;
 piLifo =class(liste)
  public
   function Est_Vide_Pile: boolean;
   procedure Empiler (elt: T0);
   procedure Depiler (var elt: T0);
   function premier: T0;
   procedure creationPiLifo(Box:TListBox);
end;



     

    1.5 Niveau classe visuelle avec héritage : TListBox

Delphi.ListeChaines \Liste4Visuelle.Dlfi
 

Nous savons qu’il est toujours ennuyeux de devoir écrire des lignes de code uniquement afin de " voir " ce qui se passe à l’intérieur de notre structure de donnée. C’est à la fois un besoin pédagogique (observer et scruter pas à pas le fonctionnement de la liste et des actions qui y ont lieu) et une nécessité de test. Dans le cas des tests, le code supplémentaire sert à visualiser les états des données en mode mise au point pour des programmes utilisant la classe liste.

Comme la classe visuelle des TlistBox dispose d’un champ items qui hérite des propriétés des TStringList (en fait TStrings semblable aux TStringList), nous allons utiliser ce champ comme base de notre liste visuelle, le reste du TlistBox nous servira aux nécessités pédagogiques et de test.
 
type
 T0 = string;
 liste =class(TListBox) {héritage de la classe TListBox }
  public
   function longueur : integer;
   procedure supprimer (k: integer);
   procedure inserer (k: integer; x: T0);
   function kieme (n: integer): T0;
   function test (x: T0): boolean;
   procedure rechercher (x: T0; var place: integer);
   procedure ajouter(x:T0);
   procedure effacer;
  private
   function est_vide:boolean;
end;

implementation
procedure liste.supprimer (k: integer);
begin
 self.items.Delete(k-1);
end;

procedure liste.inserer (k: integer; x: T0);
begin
 self.items.Insert(k-1,x);
end ;

function liste.kieme (n: integer): T0;
begin
 kieme:=self.items.strings[n-1]
end;
......

Cette approche par dérivation d’un objet visuel du RAD s’est révélée à l’usage être un bon compromis pour une utilisation pédagogique de l’outil visuel en initiation. Encore une fois l’aspect visuel renforce la compréhension du mécanisme de dérivation d’une classe à partir d’une autre classe.
 

Effort d’implantation :
L’effort est équivalent à celui produit dans le cas de la classe non visuelle héritant du TStringList. Une amélioration est à noter quant à l’effort de conception qui est facilité par le support visuel du composant TListBox.

 
Réutilisation :
La réutilisation est la même que pour la classe non visuelle héritant du TStringList.
 

Protection :
La protection est très améliorée par rapport à la classe non visuelle héritant du TStringList. En effet nous héritons d’encore plus de protection fournie par le TListBox relativement à la gestion visuelle des données.
 
 

 


2. Encore plus de réutilisation et d’abstraction
 

Nous construisons dans ce paragraphe un composant réutilisable au même niveau que les objets visuels de base du RAD. La méthode générale et pratique de développement de ce genre de composant n'est pas donnée ici, mais se trouve au chapitre 7 consacré aux outils logiciels. Nous utilisons la facilité d’extensibilité de l’environnement de développement Delphi (rappelons que l’extensibilité est l’un des facteurs de qualité d’un logiciel). D’ailleurs une quantité importante de tels composants est proposée par des particuliers ou des professionnels en démonstration, en shareware ou en freeware sur le web.
 
 

2.1 Un outil logiciel réutilisable : le composant liste visuelle

Delphi.ListeChaines \Liste4Visuellecompos.Dlfi
 

Nous reprenons strictement le niveau classe visuelle avec héritage du TListBox, auquel il suffit d’ajouter une méthode constructeur permettant de déposer le futur composant visuel. En fait le constructeur de TListBox est create, c’est celui de son ancêtre Tobject. Ce constructeur est surchargeable1 dans tous les descendants de Tobject. Afin de pouvoir utiliser ses propriétés intrinsèques dans le descendant le RAD Delphi dispose de la clause inherited. Cette clause permet dans un descendant, d’hériter automatiquement des actions d’une méthode ancêtre surchargée.
 

Voici une partie du code correspondant au composant :
 
interface
type
 T0 = string;
 liste =class(TListBox) //héritage de la classe TListBox 
  public
   constructor create(Aowner:Tcomponent);override;
   function longueur : integer;
   procedure supprimer (k: integer);
   procedure inserer (k: integer; x: T0);
   function kieme (n: integer): T0;
   function test (x: T0): boolean;
   procedure rechercher (x: T0; var place: integer);
   procedure ajouter(x:T0);
   procedure effacer;
  private
   function est_vide:boolean;
end;

procedure register; 
//permet d’enregistrer le composant dans la palette de Delphi

implementation

procedure register;
begin
 RegisterComponents('Perso',[liste])
 //le composant est enregistré dans l’onglet Perso de la palette
end;

....code identique à la liste visuelle

constructor liste.create(Aowner:Tcomponent);
begin
 inherited create(Aowner);//appel au constructeur de TListBox
 setbounds(50,50,100,150);//initialisation de positionnement
end;

Après ces opérations nous disposons d’un nouveau composant visuel pour développer des programmes utilisant des listes de chaînes.
 

Voici un aspect de la palette des composants après enregistrement


 

Effort d’implantation :
L’effort est légèrement supérieur à celui produit dans le cas de la classe visuelle héritant du TlistBox à cause du constructeur à définir.

  Réutilisation :
Nous obtenons :
Delphi.ListeChaines \Liste5VisuGeneric.Dlfi

Nous terminons cet ensemble de remarques sur le TAD liste de chaînes en montrant qu’il est possible de parler de généricité à un débutant avec l’outil Delphi. Nous allons construire une liste visuelle qui s’adaptera à l’utilisateur (un programmeur) et dont les données pourront être soit des entiers, soit des réels, soit des chaînes, soit des booléens, soit des objets. Nous allons voir qu’il n’est pas très difficile de construire un objet polymorphe à partir du TAD de liste générale dans lequel le type T0 de données internes à la liste n’est pas défini à l’avance mais se trouve passé comme un " paramètre ".

Nous souhaitons que ce composant soit à la disposition du programmeur et qu’il puisse lors de la conception se servir de cette liste polymorphe. Nous souhaitons enfin qu’à partir de cette classe le programmeur puisse utiliser des objets instanciés de cette liste sous forme de liste d’entiers ou de liste de chaînes etc...

Nous montons donc plus haut dans l’abstraction ce qui est le propre de la généricité.

Nous pouvons simplement construire notre composant parce que Delphi dispose déjà d’un objet générique, le type Variant qui est très intéressant, mais dangereux s’il est mal utilisé par un débutant1. Dans le cas présent nous verrouillons le type variant en ne l’utilisant que pour ses propriétés de généricité et seulement pour celles-ci, en gérant nous-mêmes le transtypage. Nous avons le contrôle de toutes les opérations internes ce qui nous assure le minimum de " dérapage " possible.
 

Remarque :
Le type variant possède une fonction lui permettant d’indiquer quel est le type actuellement supporté par le variant :

function VarType(x :Variant)TypedeVariant


 

Voici l’interface de la unit contenant le composant TlisteVisuGen.
 
unit UcompoListeVisugeneric;

interface
Uses Classes,StdCtrls;
type
 ElementListe=(entier,reel,chaine,booleen,objet);
 T0 = variant;
 TlisteVisuGen =class(TListBox)
  public
   constructor create(Aowner:Tcomponent);override;
   function longueur : integer;
   procedure supprimer (k: integer);
   procedure inserer (k: integer; x: T0);
   function kieme (n: integer): T0;
   function Test (x: T0): boolean;
   procedure Rechercher (x: T0; var place: integer);
   procedure Ajouter(x:T0);
   procedure effacer;
  private
   FTypeObjet:integer;
   function Est_vide:boolean;
   procedure SetTypeObjet(x:ElementListe);
   function GetTypeObjet:ElementListe;
  published
   property TypeObjet:ElementListe read GetTypeObjet write SetTypeObjet;
end;

procedure register;

implementation
.....

après enregistrement dans l’onglet perso de la palette des composants.


 
 

Examinons chaque partie de la classe TlisteVisuGen

Le type d’élément T0 est un variant.
 

la partie public
Rien de particulier, elle est identique à la partie public de la classe visuelle non générique.

  La partie private
La variable FTypeObjet:integer est un champ privé contenant le code (un entier) du type d’objet contenu dans la liste.   La procedure SetTypeObjet(x:ElementListe) sert à transtyper une variable de type énuméré ElementListe en son code compatible avec le code associé dans un variant.
 
procedure TlisteVisuGen.SetTypeObjet(x:ElementListe);
begin
 case x of
  entier :FTypeObjet:=varInteger;
  reel :FTypeObjet:=varCurrency;
  booleen :FTypeObjet:=varBoolean;
  chaine :FTypeObjet:=varString;
  objet :FTypeObjet:=varvariant;
 end
end;

Remarque : varInteger,…,varvariant sont des constantes symboliques prédéfinies de Delphi.
 

La function GetTypeObjet:ElementListe sert à transtyper la variable FTypeObjet en le résultat de type énuméré ElementListe associé.
 
function TlisteVisuGen.GetTypeObjet:ElementListe;
begin
 case FTypeObjet of
  varInteger :result:=entier;
  varCurrency :result:=reel;
  varBoolean :result:=booleen;
  varString :result:=chaine;
  varvariant :result:=objet;
 end
end;

  La partie published
Elle contient une seule propriété property TypeObjet:ElementListe qui sera donc accessible au programmeur en mode conception visuel à travers l’inspecteur d’objet. Voici le résultat d’un examen à l’inspecteur d’objet, d’un objet de la classe TlisteVisuGen et de sa propriété published supplémentaire TypeObjet.
 
 
Les propriétés du parent restent visibles lors de la conception.

Une propriété supplémentaire par rapport au TListBox est affichée : TypeObjet.

Le programmeur peut sélectionner dans la partie droite de la propriété une des cinq valeurs possibles que nous avons prévues pour cette classe.

Ici il a sélectionné la valeur chaine. L’objet est donc une liste de chaînes pendant toute l’exécution du futur programme. 


 

Dans la partie implementation nous trouverons :
 

 
implementation

procedure register;
begin
 RegisterComponents('Perso',[TlisteVisuGen])
end;

le code déjà indiqué de function GetTypeObjet
et de procedure SetTypeObjet
 

constructor TlisteVisuGen.create(Aowner:Tcomponent);
begin
 inherited create(Aowner);
 TypeObjet:=objet;
 setbounds(50,50,100,150);
end;

......

procedure TlisteVisuGen.supprimer (k: integer);
begin
 self.items.Delete(k-1);
end;

procedure TlisteVisuGen.inserer (k: integer; x: T0);
begin
 if vartype(x)=FTypeObjet then 
  self.items.Insert(k-1,x);
end;
{pour des raisons de sécurité on n’insère qu’une donnée du même type que celui des éléments de la liste}

function TlisteVisuGen.kieme (n: integer): T0;
begin
 kieme:=self.items.strings[n-1]
end

....


 

Nous avons réussi notre contrat qui consistait à mettre à la disposition du programmeur une classe polymorphe de listes se rapprochant du TAD tel qu’il a été défini. Il ne nous a pas fallu ajouter beaucoup de code à la liste visuelle non générique. Tous les opérateurs sont les mêmes, seule la partie transtypage et nouvelle propriété sont des nouveautés.
 

Remarque :
Ce composant n’est qu’une ébauche de liste générique. Il est sécurisé pour les opérateurs que nous avons définis. Il ne faut donc pas utiliser des méthodes internes aux champs que le RAD Delphi ne sait pas masquer directement. Par exemple, le champ items de type Tstrings n’est pas sécurisé vis à vis du type de donnée présent dans la liste. Si l’utilisateur connaît la syntaxe d’une méthode comme LoadFromFile, il peut détourner la protection car cette méthode reste accessible à l’utilisateur. Il lui est donc possible de stocker des données non cohérentes avec le type actuel dans la liste.

Ce qui veut dire que notre liste générique n’est utilisable d’une manière sécurisée qu’à partir des opérateurs que nous avons définis.

Le lecteur enrichira l’exemple avec ses propres opérateurs.