Guide Pascal et Delphi


précédentsommairesuivant

VII. Types de données avancés de Pascal Objet

Ce chapitre, plutôt théorique, est consacré à la suite de l'étude du langage Pascal Objet. Delphi ne sera utilisé ici que pour vous permettre de manipuler les notions nouvelles.

Les types de données que vous connaissez actuellement, à savoir nombres entiers, à virgule, caractères et chaînes de caractères, énumérés et booléens, suffisent pour de petits programmes, mais deviendront insuffisants dans de nombreuses situations. D'autres types de données plus élaborés devront alors être utilisés. Le début de ce chapitre aborde des types standards de Delphi non encore connus de vous, tandis que la fin du chapitre vous présentera l'une des deux manières pour construire son type personnalisé à partir d'autres types (l'autre manière, nettement plus complexe et puissante, sera vue, mais nettement plus tard, dans le guide).

VII-A. Création de nouveaux types

A partir de ce point, il va vous falloir connaître un nouveau bloc Pascal : le bloc de déclaration de type. Ce bloc permet de définir des nouveaux types, comme les blocs de constantes et de variables définissaient des constantes et des variables.

Un bloc de déclaration de type peut se situer aux mêmes endroits que les blocs const et var. Le bloc commence par contre par le mot Pascal réservé type et possède la syntaxe suivante :

 
Sélectionnez

type
  Déclaration_de_type;
  { ... }
  Déclaration_de_type;

Chaque déclaration de type est de la forme :

Identificateur = Specification_du_type 

Identificateur est un identificateur non encore utilisé qui désignera le nouveau type. Spécification du type peut prendre diverses formes et au fur et à mesure des paragraphes ci-dessous, nous en verrons un certain nombre. Une habitude veut que tous les identificateurs de types personnalisés commencent par la lettre T majuscule suivi d'un nom descriptif (toutefois, les types pointeurs que nous étudierons au chapitre 9 font exception à cette convention en utilisant la lettre P).

Il est possible de donner pour Spécification du type un type standard, tel 'integer' ou 'byte', ou n'importe quel type connu.

Les types ainsi définis peuvent être utilisés ensuite dans les blocs const et var de la manière suivante :

 
Sélectionnez

type
  TChaineCourte = String[10];
const
  CBonj: TChaineCourte = 'Bonjour';
var
  InitialesNom: TChaineCourte;

Ces types peuvent également être utilisés partout où un type est requis, par exemple dans la liste des paramètres d'une procédure ou d'une fonction ou comme type de résultat d'une fonction. Voici un exemple :

 
Sélectionnez

function Maj(Ch: TChaineCourte): TChaineCourte;
begin
  result := UpperCase(Ch);
end;

(UpperCase est une fonction fournie par Delphi qui renvoie la chaine donnée en paramètre convertie en majuscules)

Exercice 1 : (voir la solution)

Ecrivez le bloc type complet déclarant les deux nouveaux types suivants :

  • TChaine200 : type chaîne de 200 caractères.
  • TEntier : type équivalent au type integer.

VII-B. Type ordinaux

Les types ordinaux sont non pas un nouveau type mais une particularité pour un type. Certains types permettent des valeurs classées suivant un ordre. Ces types sont alors dits ordinaux. C'est le cas, parmi les types que vous connaissez, de tous les types entiers ('integer', 'byte', ...), des booléens, des caractères (mais pas des chaînes de caractères) et des énumérés. Il sera dorénavant mentionné lors de la présentation de nouveaux types s'ils sont ordinaux ou pas.

Les types ordinaux ont tous une sorte de correspondance avec les nombres entiers. Chaque donnée (constante ou variable) d'un type ordinal possède ce qu'on appelle une valeur ordinale, qui est donnée par la fonction 'Ord'. Cette fonction fait partie d'un ensemble de fonctions assez particulières qui acceptent des paramètres divers. En l'occurence, 'Ord' accepte n'importe quelle constante ou variable de type ordinal.

La fonction 'Ord' sera utilisée dans les paragraphes suivants, de même que nous reviendrons sur les types ordinaux.

VII-C. Type intervalle

Le type intervalle permet de définir un nouveau type ordinal personnalisé autorisant un intervalle de valeurs ordinales, en donnant les valeurs extrèmales (son minimum et son maximum). Ceci permettra de nombreuses choses notamment pour un autre type que nous allons bientôt voir.

Ce type s'écrit comme suit :

ValeurMinimale..ValeurMaximale

ValeurMinimale et ValeurMaximale sont du même type ordinal. Il est à noter que la valeur ordinale de ValeurMinimale doit être inférieure ou égale à celle de ValeurMaximale, ou sinon le compilateur signalera une erreur. La déclaration d'un type, ou d'une variable de ce type, se font de la manière suivante :

 
Sélectionnez

type
  nom_de_type = ValeurMinimale..ValeurMaximale;
var
  nom_de_variable: ValeurMinimale..ValeurMaximale;

Exemples :

 
Sélectionnez

type
  TLettreMaj = 'A'..'Z';
var
  NbPositif: 0..1000000;
  LettreMin: 'a'..'z';
  LecteurDisque: TLettreMaj;
  Chiffre: 0..9;

Déclarer une constante de type intervalle avec une valeur hors de cet intervalle est interdit. De même, pour des variables de type intervalle, assigner directement une valeur hors de l'intervalle est interdit par le compilateur :

 
Sélectionnez

NbPositif := -1;

provoquera une erreur, mais par contre :

 
Sélectionnez

Chiffre := 9;
Chiffre := Chiffre + 1;

sera accepté et pourtant "Chiffre" n'est plus entre les deux valeurs extremales. Ce sera à vous de faire attention.

VII-D. Compléments sur les types énumérés

Il est possible de déclarer de nouveaux types énumérés, personnalisés. La syntaxe en est la suivante :

Nom_du_type = (IdValeur0, IdValeur1, ..., IdValeurn);

Nom_du_type, IdValeur0, IdValeur1, ..., IdValeurn sont des identificateurs non utilisés. Nom_du_type désignera alors le type et IdValeur0, IdValeur1, ..., IdValeurn seront les n+1 valeurs possibles d'une constante ou variable de ce type. Chacun de ces identificateur a une valeur ordinale (qu'on obtient, je rappelle, à l'aide de la fonction Ord) déterminée par la déclaration du type : le premier identificateur prend la valeur ordinale 0, le suivant 1, et ainsi de suite. IdValeur0 a donc pour valeur 0, IdValeur1 a pour valeur 1, IdValeurn a pour valeur n. Par convention, on essayera de nommer IdValeurx par deux ou trois lettres minuscules qui sont une abréviation du nom de type, puis par une expression significative.

Note aux programmeurs en C, C++ ou autres :

Delphi n'offre pas les mêmes possibilités que le C ou le C++ en ce qui concerne les énumérations : il n'est en effet pas possible de donner des valeurs personnalisées aux identificateurs constituant la déclaration du type. Il sera cependant possible d'outrepasser cette limitation en déclarant une constante du type énuméré et en utilisant la syntaxe (qui effectue un transtypage) :

Nom_De_Type_Enumere(valeur_ordinale)

On n'a que rarement besoin d'un tel assemblage, contactez-moi donc si vous avez besoin de précisions.

Sous Delphi, retournez au code source de la procédure TForm1.Button1Click. Remplacez son contenu par l'exemple ci-dessous :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TTypeSupport = (tsDisq35, tsDisqueDur, tsCDRom, tsDVDRom, tsZIP);
var
  Supp1: TTypeSupport;
  I: Byte;
begin
  Supp1 := tsCDRom;
  I := Ord(Supp1);
  ShowMessage(IntToStr(I));
end;

Cet exemple déclare un nouveau type énuméré local à la procédure (mais il pourrait aussi bien être déclaré ailleurs) nommé 'TTypeSupport'. Une variable 'Supp1' est déclarée de ce type. Les valeurs possibles pour cette variable sont donc tsDisq35, tsDisqueDur, tsCDRom, tsDVDRom et tsZIP. Ces identificateurs ont une valeur ordinale. Pour vous la faire voir, une valeur a été donnée à la variable (vous pouvez changer cette valeur en piochant dans les valeurs possibles mais si vous tentez de mettre autre chose qu'une de ces valeurs, le compilateur signalera une erreur). La valeur de la variable est alors transmise à la fonction Ord qui renvoie un nombre entier positif. On a choisi de stocker ce résultat dans une variable I de type 'byte' mais on aurait pu choisir 'integer' (il y a moins de 256 valeurs possibles pour le type 'TTypeSupport', donc le type 'byte' convient bien avec sa limite de 255). Après l'exécution de l'instruction, on affiche la valeur de I à l'aide de ShowMessage et de IntToStr (IntToStr fonctionne comme FloatToStr, mais uniquement avec les nombres entiers, il sera préférable de l'utiliser avec ces derniers, plutôt que d'utiliser partout FloatToStr). En lançant l'application, vous verrez donc s'afficher la valeur ordinale de 'tsCDRom', soit 2.

C'est l'occasion idéale pour vous parler de 2 fonctions et de 2 procédures assez particulières : high, low, inc, dec. Elles acceptent chacune un unique paramètre qui doit être ordinal.

  • 'High' (c'est une fonction), tout comme 'Low' a une particularité : son paramètre peut être soit une variable de type ordinal, ou directement le nom d'un type ordinal. 'High' renvoie toujours une valeur (et non un type) du même type que le paramètre transmis (le type de la variable si le paramètre est une variable ou du type donné en paramètre si le paramètre est un type), qui est la valeur dont la valeur ordinale est la plus importante pour ce type. Par exemple, 'High(TTypeSupport)' vaudra 'tsZIP'.
  • 'Low' (c'est une fonction) fonctionne de la même manière, mais renvoie la valeur dont la valeur ordinale est la plus faible. Ainsi, par exemple, 'Low(TTypeSupport)' vaudra 'tsDisq35'.
  • 'Inc' (c'est une procédure) accepte n'importe quelle variable de type ordinal, et la modifie en augmentant sa valeur ordinale d'une unité. Ainsi, pour un nombre entier, 'Inc' a le même effet que si l'on additionnait 1. Par contre, sur les autres types, les effets sont plus intéressants car on ne peut pas leur ajouter 1 directement, il faut passer par 'Inc'.
  • 'Dec' a l'effet contraire : il enlève 1 à la valeur ordinale de la variable qu'on lui transmet en paramètre.

Exemple :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TTypeSupport = (tsDisq35, tsDisqueDur, tsCDRom, tsDVDRom, tsZIP);
var
  Supp1: TTypeSupport;
begin
  Supp1 := Low(TTypeSupport);
  ShowMessage(IntToStr(Ord(Supp1)));
  Inc(Supp1);
  ShowMessage(IntToStr(Ord(Supp1)));
  Dec(Supp1);
  ShowMessage(IntToStr(Ord(Supp1)));
end;

Dans l'exemple ci-dessus, la valeur de la variable 'Supp1' est fixée à la valeur de valeur ordinale la plus faible du type 'TTypeSupport', qui est donc 'tsDisq35'. Un appel à 'ShowMessage' permet ensuite d'afficher la valeur ordinale de Supp1. Vous remarquez que nous avons grimpé d'un échelon en n'utilisant plus de variable I, mais directement la valeur qui lui était affectée.

La variable 'Supp1' est ensuite transmise à 'Inc', qui augmente sa valeur ordinale de 1. Supp1 vaut donc 'tsDisqueDur'. Pour nous en assurer, on affiche à nouveau la valeur ordinale de 'Supp1'. Le même procédé est enfin appliqué pour tester la procédure 'Dec'.

Si vous voulez voir la chose en application, entrez le code source ci-dessus dans Delphi (on se sert encore et toujours de la procédure 'TForm1.Button1Click', et lancez l'application.

Exercice 2 : (voir la solution)

Ecrivez un type énuméré pour les 7 jours de la semaine. Déclarez une variable de ce type, initialisez-la à une valeur de votre choix et affichez sa valeur ordinale (utilisez la procédure habituelle pour cela).

Si vous le pouvez, essayez de ne pas utiliser de variable intermédiaire comme dans l'exemple ci-dessus.

VII-E. Type ensemble

Le type ensemble permet de créer des ensembles à cardinal fini (le cardinal d'un ensemble est le nombre d'éléments dans cet ensemble). Les ensembles en Pascal ne peuvent contenir que des constantes de type ordinal, dont la valeur ordinale est comprise entre 0 et 255. Toutes les constantes entières entre 0 et 255 conviennent donc, ainsi que les caractères et la plupart des types énumérés. On ne peut cependant pas mélanger les types à l'intérieur d'un ensemble, le type des éléments étant décidé dans la déclaration du type.

Le type ensemble s'écrit :

set of type_ordinal;

type_ordinal désigne au choix un nom de type ordinal ou spécifie directement un type ordinal. Voici des exemples :

 
Sélectionnez

type
  TResultats = set of Byte;
  TNotes = set of 0..20;
  TSupports = set of TTypeSupport;

Dans l'exemple ci-dessus, 'TResultats' pourra contenir des entiers de type 'byte', 'TNotes' pourra contenir des entiers entre 0 et 20, et 'TSupports' pourra contenir des éléments de type 'TTypeSupport'.

Il est alors possible de déclarer des variables de ces types. Pour utiliser ces variables, vous devez savoir comment on écrit un ensemble en Pascal : il se note entre crochets et ses éventuels sous-éléments sont notés entre virgules. J'ai bien dit éventuel car l'ensemble vide est autorisé. Par sous-élément, on entend soit un élément simple, soit un intervalle (rappelez-vous des types intervalles). les exemples ci-dessous sont des ensembles.

 
Sélectionnez

[] { ensemble vide }

[1, 2, 3] { ensemble d'entiers contenant 1, 2 et 3 }

[1, 1, 2, 3, 2] { ensemble d'entiers contenant 1, 2 et 3 }

[1..3] { ensemble d'entiers contenant 1, 2 et 3 }

['a'..'z', 'A'..'Z', '_', '0'..'9'] { ensemble de caractères contenant les lettres majuscules, minuscules, le blanc souligné et les chiffres }

[tsDisq35..tsCDRom, tsZIP] { ensemble d'élements de type TTypeSupport contenant tsDisq35, tsDisqueDur, tsCDRom et tsZIP }

Il est en outre possible de manipuler ces ensembles comme les ensembles manipulés en mathématiques : union, intersection sont possible, de même que le retrait d'un élément (si jamais vous ne maîtrisez pas ces notions, adressez-vous au professeur de mathématiques le plus proche, ou à moi).

 
Sélectionnez

[1..3, 5] + [4]    { union de deux ensembles : le résultat est [1..5] }
[1..5] - [3, 5]    { retrait d'un sous-ensemble : le résultat est [1, 2, 4] }
[1..4, 6] * [3..8] { intersection de deux ensembles : le résultat est [3, 4, 6] }

Il est également possible de comparer deux ensembles. Pour cela, on utilise des opérateurs de comparaison, comme ceux que vous connaissez déjà. Le résultat de l'opérateur est alors un booléen (vous apprendrez assez rapidement quoi faire de ce genre de booléens, qui pour le moment, il est vrai, sont assez inutilisables, mis à part dans des affectations). ci-dessous, A et B désignent deux ensembles de même type, x est un élément acceptable dans ces ensembles.

 
Sélectionnez

A <= B { est inclus : renvoie vrai si A est inclus dans B }
A >= B { inclut : renvoie vrai si B est inclus dans A }
A = B  { égalité : renvoie vrai si les deux ensembles contiennent les mêmes éléments }
A <> B { différent : renvoie vrai si les deux ensembles sont différents }
x in A { élément de : renvoie vrai si l'élément x est dans A }

Il est également possible de déclarer des constantes de type ensemble. Voici un exemple :

 
Sélectionnez

const
  SuppAmovibles = [tsDisq35, tsCDRom, tsDVDRom, tsZIP];

Pour terminer ce point, voici un certainement très utile exemple général : Pour terminer ce point, voici un certainement très utile exemple général :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TTypeSupport = (tsDisq35, tsDisqueDur, tsCDRom, tsDVDRom, tsZIP);
  TSupports = set of TTypeSupport;
var
  SuppDisponibles: TSupports;
const
  SuppAmovibles = [tsDisq35, tsCDRom, tsDVDRom, tsZIP];
begin
  { initialisation }
  SuppDisponibles := SuppAmovibles;
  { on enlève tsDVDRom et on ajoute tsDisqueDur }
  SuppDisponibles := SuppDisponibles - [tsDVDRom] + [tsDisqueDur];
end;

Cet exemple déclare les types 'TTypeSupport' et 'TSupports', une variable de type ensemble et une constante de type ensemble. La première instruction permet de donner à la variable une valeur de départ, on appelle cela une initialisation. Nous aurons l'occasion d'en reparler. La deuxième instruction permet de retirer un élément et d'en ajouter un autre. Notez que cela a été fait en une seule instruction, car les opérateurs sont exécutés de gauche à droite, sauf si des parenthèses sont présentes.

VII-F. Tableaux

Les tableaux sont une possibilité puissante du langage Pascal. Imaginons par exemple que vous vouliez stocker 100 nombres entiers de type 'word'. Vous pourriez déclarer (très mauvaise méthode) 100 variables de type 'word'. Une solution préfèrable sera d'utiliser un tableau. Le principe d'un tableau est de regrouper un certain nombre d'élements du même type, en ne déclarant qu'une variable ou qu'une constante. Les éléments individuels sont ensuite accessibles via un ou plusieurs indices de position dans le tableau.

Les tableaux en Pascal peuvent avoir une ou plusieurs dimensions. Nous consacrerons un paragraphe à l'étude de chaque cas. Un troisième paragraphe présentera des notions avancées permettant de dépasser le cadre des tableaux standards vus dans les deux premiers paragraphes. Un dernier paragraphe présentera les tableaux de taille dynamique, utilisables à partir de Delphi 4.

VII-F-1. Tableaux à une seule dimension

Un tableau à une seule dimension se déclare de la façon suivante :

array[intervalle] of type;

array définit un tableau. les crochets entourent les données de dimension du tableau. Pour un tableau à une seule dimension, un seul paramètre est donné, de type intervalle (il est possible d'utiliser d'autres choses que les intervalles, ceci sera vu dans le § VII-F-3). Vient ensuite le mot réservé of qui est suivi du type des éléments du tableau. Cet élément est un type. Tous les types vus jusqu'à présent sont acceptés, y compris d'autres tableaux. Il faudra cependant, pour certains types comme les ensembles, déclarer d'abord un type ensemble, puis utiliser ce nom de type pour déclarer le tableau. En principe, tous les types sont autorisés, du moment que la taille du tableau reste en dessous d'une limite dépendant de la version de Delphi.

Il est possible de déclarer des types, des variables et des constantes de type tableau (c.f. exemple ci-dessous). Pour accèder à un tableau, il suffit de donner le nom de la variable tableau de la constante tableau. Pour accèder à un élément, on donne le nom du tableau suivi de l'indice entre crochets. Voyez l'exemple ci-dessous pour mieux comprendre :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  { notez que tsAucun a été ajouté ci-dessous }
  TTypeSupport = (tsAucun, tsDisq35, tsDisqueDur, tsCDRom, tsDVDRom, tsZIP);
  TSupports = set of TTypeSupport;
var
  Nombres: array[1..100] of word;
  TypeLecteur: array[1..26] of TTypeSupport;
const
  Reponses: array[0..2] of string = ('non', 'oui', 'peut-être');
begin
  Nombres[1] := 13;
  Nombres[2] := Nombres[1] + 34;
  TypeLecteur[1] := tsDisq35;
  TypeLecteur[3] := tsDisqueDur;
  TypeLecteur[4] := TypeLecteur[1];
end;

Vous remarquerez dans l'exemple ci-dessus que 'Nombres' est un tableau d'entiers positifs. On peut donc affecter à chaque élément (on dira également chaque case, ou cellule) un entier positif. La deuxième variable est un tableau d'éléments de type TTypeSupport. Un tel tableau pourrait être utilisé pour stocker les types de lecteur de A à Z sur un ordinateur. Chaque case du tableau contient un élément de type TTypeSupport. On affecte des valeurs à 3 de ces cases.

La constante devrait retenir votre attention : pour donner la valeur d'une constante de type tableau, on donne une liste de tous les éléments, dans l'ordre, séparés par des virgules, le tout entre parenthèses. Ici, les éléments sont des chaînes de caractères et le tableau en contient 3 numérotées de 0 à 2.

Exercice 3 : (voir la solution)

Dans la procédure habituelle, déclarez un type tableau de dix chaînes de caractères, indicées de 1 à 10. Déclarez une variable de ce type et initialisez une de ses cases à la chaîne : « Bonjour ». Affichez ensuite le contenu de cette case à l'aide de ShowMessage (Pour cette fois, vous avez le droit d'utiliser une variable temporaire, essayez cependant de vous en passer).

VII-F-2. Tableaux à plusieurs dimensions

Vous aurez parfois besoin de tableaux à plusieurs dimensions (les tableaux à 2 dimensions sont similaires aux tableaux à double entrée que vous connaissez certainement déjà par exemple dans Microsoft Word, mais Pascal permet d'avoir plus de 2 dimensions). Je ne vous donnerai pas de preuve de leur utilité, vous en connaissez certainement déjà. Un tableau multidimensionnel se déclare ainsi :

array[intervalle1, intervalle2, ...] of type;

intervalle1 est la première dimension du tableau, intervalle2 la deuxième, ... et type est encore le type des éléments stockés dans le tableau.

Les types, variables et constantes se déclarent en utilisant cette syntaxe. Pour donner des valeurs aux éléments d'une variable tableau, on utilise la syntaxe suivante :

nom_du_tableau[indice1, indice2, ...] := valeur;

nom_du_tableau est le nom d'une variable de type tableau, indice1 est l'indice dans la première dimension, indice2 dans la deuxième, ... et valeur la valeur (dont le type est donné dans la déclaration du tableau) à stocker dans cette case.

Pour fixer la valeur d'une constante de type tableau multidimensionnel, c'est un peu plus compliqué. Considérons un tableau de dimension 3 : on devra faire comme si c'était en fait un tableau à 1 dimension dont chaque case contient un tableau à 1 dimension, qui lui-même est de dimension 1. Voyez l'exemple ci-dessous qui récapitule tout ce que nous venons de dire (Attention, la première instruction, à savoir l'affectation d'un tableau à une variable tableau, est possible à partir de Delphi 4 seulement, si vous n'avez que Delphi 2 ou 3, il vous faudra attendre que nous ayons vu les boucles 'for' pour passer outre ce genre de difficulté).

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TTab3Dim = array[1..3, 1..2, 0..1] of integer;
const
  TabExemple: TTab3Dim = { Examinez bien les parenthèses : 3 blocs qui chacun en }
    (((1, 2), (3, 4)),   { contiennent 2 qui contiennent eux-mêmes chacun deux }
     ((5, 6), (7, 8)),   { valeurs de type integer }
     ((9, 10), (11, 12)));
var
  Tab1: TTab3Dim;
begin
  Tab1 := TabExemple;
  Tab1[2, 1, 0] := 20; { ancienne valeur : 5 }
end;

Dans l'exemple ci-dessus, un type de tableau à 3 dimensions a été créé, ce type étant ensuite utilisé pour déclarer une constante et une variable. Attardez-vous sur la constante 'TabExemple' : sa valeur est donnée entre parenthèses. Chaque élément de la première dimension est considéré comme un tableau, et est donc encore noté entre parenthèses. Enfin, chaque élément de la deuxième dimension est considéré comme un tableau de 2 éléments, dont vous connaissez déjà la déclaration. Il est assez facile de se tromper dans les déclarations de constantes de type tableau, mais rassurez-vous, le compilateur saura vous signaler toute parenthèse mal placée lors de la compilation du projet.

La première instruction permet d'initialiser la variable : on lui donne une valeur. En effet, contrairement à certains langages que vous connaissez peut-être, la mémoire utilisée par les variables n'est pas nettoyée avant utilisation, c'est-à-dire que les variables, avant que vous leur donniez une première valeur, peuvent valoir tout et n'importe quoi (en général, c'est surtout n'importe quoi !). Une initialisation permet de donner une première valeur à une variable. Il ne faudra jamais utiliser la valeur d'une variable sans qu'elle ait été initialisée auparavant.

On affecte directement la valeur d'une constante de type TTab3Dim à une variable de même type, il n'y a donc pas de problème.

La deuxième instruction modifie la valeur d'une des cases du tableau, repèrez bien les indices et reportez-vous à la déclaration de la constante pour voir à quelle position ces indices font référence. Il vous faudra tenir compte des intervalles dans lesquels sont pris les indices (les indices de dimension 3 sont pris dans l'intervalle 0..1).

VII-F-3. Notions avancées sur les tableaux

Nous nous sommes limités jusqu'à présent pour les indices des tableaux à des intervalles. Ce n'est que l'une des possibilités offertes : la plus simple. Il est en fait possible d'utiliser tout type ordinal en tant qu'indice. Le cas le plus intéressant à examiner est celui des types énumérés : il sera possible de déclarer un tableau tel que celui-ci :

array[tsDisqueDur..tsDVDRom] of Char;

Mais il sera encore plus intéressant d'utiliser des noms de types énumérés en guise d'indices. Ainsi, la déclaration suivante permet d'utiliser un tableau dont chaque case est une valeur de type TTypeSupport :

type TTabSupport = array[TTypeSupport] of string;

Ce nouveau type pourra se révèler très utile, pour vous en rendre compte, regardez le code source suivant :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TTypeSupport = (tsAucun, tsDisq35, tsDisqueDur, tsCDRom, tsDVDRom, tsZIP);
  TTabSupport = array[TTypeSupport] of string;
const
  NomDesSupports: TTabSupport =
    ('', 'Disquette 3,5"', 'Disque dur', 'CD-Rom', 'DVD-Rom', 'Disquette ZIP');
var
  Sup1: TTypeSupport;
  msg: string;
begin
  Sup1 := tsCDRom;
  msg := NomDesSupports[Sup1];
  ShowMessage('support : ' + msg);
end;

Le premier type est désormais habituel. Le deuxième déclare un tableau dont les cases sont indexées par des valeurs de type TTypeSupport et dont les cases contiennent des chaînes de caractères. Vient ensuite une déclaration de constante tableau. Celle-ci s'écrit en précisant d'abord le type de la constante précédé de deux points (:), puis en donnant la valeur de la constante après un signe =. La valeur s'écrit entre parenthèses : une liste de valeurs est donnée, et affectée aux cases dans l'ordre. Les valeurs sont séparées par des virgules. Chaque chaîne correspond à un élément de type TTypeSupport.

Les deux variables serviront dans les instructions.

La première de ces instructions initialise Sup1. La deuxième initialise msg, en utilisant la constante de type tableau. Comme les indices sont de type TTypeSupport, on transmet une valeur de type TTypeSupport en indice, en l'occurence c'est la valeur de 'Sup1' qui est utilisée. Chaque case du tableau étant une chaîne de caractères, NomDesSupports[Sup1] est une chaîne, qui est affectée à une variable de type chaîne. La dernière instruction est déjà connue de vous puisqu'elle se contente d'afficher un message contenant la valeur de 'msg'.

Lorsque vous exécutez l'application, un clic sur le bouton affiche alors la boite de dialogue ci-dessous :

Image non disponible

Essayez de remplacer la valeur de départ de 'Sup1' par une autre valeur de type 'TTypeSupport' dans la première instruction. Ceci aura un effet sur la valeur de 'msg', et modifiera donc le message affiché.

Exercice 4 : (voir la solution)

  1. Reprenez le code source de l' exercice 2. Déclarez une constante de type tableau avec comme indices le type TJourSemaine, et comme contenu des cases, des chaînes de caractères. Le contenu de chaque case sera une chaîne donnant le nom du jour représenté par la valeur de type TJourSemaine indicant cette case ('Lundi' pour jsLundi par exemple).
  2. Dans le code source, au lieu d'afficher la valeur ordinale de la variable de type TJourSemaine, affichez à la place le nom du jour correspondant à la valeur de cette variable en utilisant la constante déclarée à la question 1 (indication : utilisez la valeur de la variable comme indice de la constante tableau et affichez la case ainsi obtenue).

VII-F-4. Tableaux de taille dynamique

Attention : Les tableaux de taille dynamique sont utilisables à partir de Delphi 4. Si vous avez une version antérieure, rien ne vous empèche de lire ce paragraphe, mais les manipulations seront impossibles. Sachez également que les tableaux de taille dynamique ne sont pas exactement ce qu'ils semblent être, car ils sont liés à la notion de pointeurs vue plus loin dans le guide. Soyez prudents dans l'utilisation de ces tableaux tant que vous n'aurez pas lu la section consacrée aux pointeurs.

Le langage Pascal Objet est en constante évolution dans les versions successives de Delphi. Depuis la version 5, il est possible de créer des tableaux de taille dynamique, c'est-à-dire dont la taille n'est pas fixée au départ une fois pour toutes. Ces nouveaux tableaux sont toujours indexés par des entiers, et à partir de 0. Les tableaux de taille dynamique multidimensionnels, bien qu'un peu plus délicats à manipuler, sont toutefois possibles.

Un tableau dynamique à une dimension est de la forme :

array of type_de_base

type_de_base, comme pour les tableaux non dynamiques, est le type des éléments stockés dans le tableau. Vous remarquez que la partie qui donnait auparavant les dimensions, les bornes et les limites du tableau (la partie entre crochets) est justement absente pour les tableaux dynamiques.

Ces derniers tableaux sont plus complexes à manipuler que les tableaux standards : déclarer une variable de type tableau dynamique ne suffit pas, comme dans la plupart des autres cas de variables, pour que la variable soit utilisable. Du fait que leur taille n'est pas fixe, il faudra la gèrer en même temps que le tableau lui-même et donc fixer le nombre d'éléments avant la première utilisation du tableau au moyen d'une procédure : 'SetLength'. 'SetLength' accepte plusieurs paramètres : le premier est une variable de type tableau dynamique, le deuxième paramètre est le nombre d'éléments que le tableau doit pouvoir contenir. Par la suite, lorsque vous désirerez redimensionner un tableau dynamique, il suffira de rappeler "SetLength" avec une taille différente. Il est également possible de connaître la taille actuelle d'un tableau dynamique en utilisant la fonction 'Length'. 'Length' accepte un unique paramètre qui doit être une variable de type tableau dynamique.

L'exemple ci-dessous illustre ce que nous venons de voir : un tableau dynamique va être utilisé. Les explications seront données; comme d'habitude, après le code source :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
var
  TabTest: array of string;
begin
  { initialise TabTest : TabTest contient 3 cases, indexées de 0 à 2 }
  SetLength(TabTest, 3);
  { affiche la taille du tableau }
  ShowMessage(IntToStr(Length(TabTest)));
  { initialise une case }
  TabTest[2] := 'coucou';
  { affichage du contenu de la case }
  ShowMessage(TabTest[2]);
  { redimensionnement du tableau }
  SetLength(TabTest, 2); { TabTest[2] n'est maintenant plus utilisable }
end;

Une variable locale de type tableau dynamique est déclarée. Les éléments stockés dans le tableau sont des chaînes de caractères. Le fait de déclarer une variable tableau dynamique ne suffit pas : avant son dimensionnement, elle ne contient pas de cellules. Ce nombre de cellules est fixé à 3 dans la première instruction par l'appel à 'SetLength'. Ceci permettra d'utiliser des cellules indexées de 0 à 2 (les index partent toujours de 0).

La deuxième instruction est plus délicate (il faut suivre les parenthèses, comme en mathématiques) : le tableau est donné en paramètre à la fonction 'Length' qui renvoie une valeur entière donnant le nombre de cases du tableau. Cette valeur entière est non pas stockée dans une variable mais directement donnée en paramètre à 'IntToStr' qui en fait une chaîne de caractères. Cette dernière est enfin directement transmise en tant que paramètre à 'ShowMessage' qui se chargera de l'afficher.

La troisième instruction initialise une case du tableau : la troisième (qui est indexée par 2). La 4ème affiche la chaîne 'coucou' en se servant de la valeur de la case du tableau. Enfin, la dernière instruction modifie la taille du tableau : il ne contient plus alors que 2 chaînes. La conséquence est que la chaîne dans l'ancienne troisième case est maintenant inaccessible : si on plaçait la quatrième instruction après la cinquième, une erreur serait signalée à la compilation ou à l'exécution.

Parlons maintenant un peu des tableaux dynamiques multidimensionnels : il n'est pas possible, dans la déclaration des tableaux dynamiques, de donner directement plusieurs dimensions : il faut utiliser une astuce : les éléments stockés dans le tableau à 1 dimension seront eux-mêmes des tableaux dynamiques à 1 dimension. Voici donc la déclaration d'un tableau dynamique à 2 dimensions contenant des entiers :

array of array of integer;

Il faut ici faire extrèmement attention à ne pas se tromper : ce n'est pas un vrai tableau multidimensionnel que nous avons ici : chaque case d'un tableau à une dimension contient un autre tableau. On est plus proche de ce qui suit :

 
Sélectionnez

array[1..10] of array[1..20] of integer;

que de cela :

 
Sélectionnez

array[1..10, 1..20] of integer;

Je ne dis pas ça pour vous embêter mais pour vous préparer à une possilité intéressante de ce genre de tableau : Voici un exemple qui utilise un tableau dynamique à 2 dimensions. Le tableau est d'abord utilisé en tant que tableau classique, puis on prend partie du fait que chaque cellule est un tableau dynamique, et donc que rien n'oblige tous ces petits tableaux à avoir la même taille.

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
var
  TabTest2: array of array of Integer;
begin
  { init. de TabTest2 : remarquez les deux entiers données en paramètre }   SetLength(TabTest2, 10, 20);
  { initialisation d'une case, une seule paire de crochets est utilisée }
  TabTest2[6, 18] := 108;
  { changement des dimensions, la cellule ci-dessus devient inaccessible }
  SetLength(TabTest2, 2, 4);
  { changement de la taille d'une cellule }
  SetLength(TabTest2[1], 10);
  { TabTest2[0, 8] n'existe pas, mais TabTest2[1, 8] existe }
  TabTest2[1, 8] := 29;
end;

Dans l'exemple ci-dessus, une variable de type tableau dynamique est déclarée. Ce tableau comporte 2 dimensions. La première instruction fixe la taille du tableau. Après le paramètre tableau, 'SetLength' accepte en fait autant de paramètres que le tableau comporte de dimensions. Le premier paramètre donne la taille dans la première dimension (que nous assimilerons ici à des lignes) : 10 lignes. La deuxième valeur donne le nombre de colonnes par ligne, à savoir 20. Le résultat est donc un tableau de 10 cellules par 20. La deuxième instruction donne un exemple d'utilisation d'un tel tableau : les deux dimensions sont données dans un seul crochet, ce qui peut paraître bizarre du fait que TabTest2 est plus un tableau de tableaux qu'un tableau à deux dimensions : c'est une commodité offerte par le langage. Il faudrait normalement écrire :

 
Sélectionnez

TabTest2[6][18] := 108;

écriture qui sera bien entendu acceptée par Delphi 5. La troisième instruction redimensionne le tableau en plus petit. C'est la même instruction que la première, avec des tailles différentes. La quatrième instruction vous montre la puissance de ce genre de tableaux : chaque cellule de la première dimension étant elle-même en fait un tableau dynamique à une dimension, il est possible de donner une taille différente à chacune de nos « lignes ». On obtient un tableau qui n'a plus rien de rectangulaire, mais qui pourra à l'occasion se montrer très utile. La dernière instruction se contente de donner une valeur à l'une des cases. Le texte en commentaire rappelle que cette case existe dans la deuxième ligne mais pas dans la première.

Si vous ne voyez pas vraiment l'intérêt de ces tableaux dynamiques, sachez que tout ceci est un petit avant-goût de la puissance des pointeurs, que nous verrons un peu plus tard. Si vous n'avez pas accès aux tableaux dynamiques à cause de votre version de Delphi, sachez que ces pointeurs vous permettrons de réaliser la même chose, en plus compliqué toutefois.

VII-G. Enregistrements

VII-G-1. Vue d'ensemble sur les enregistrements

Les enregistrements sont des types puissants de Pascal Objet : ils sont entièrement personnalisables. Le principe est de rassembler en un seul bloc des données diverses qu'il aurait autrement fallu stocker dans des endroits séparés. Les enregistrements permettront en quelque sorte de définir des moules à partir desquels seront obtenues des variables et des constantes.

Imaginons par exemple que vous ayez besoin dans une application de manipuler des informations relatives à un unique élément. Nous prendrons par exemple un logiciel : vous manipulerez entre autres son nom, sa version, sa langue, son occupation en espace disque. Pour manipuler l'un de ces éléments, vous pourriez déclarer 4 variables destinées chacune à stocker une information sur ce logiciel, mais cela deviendrait rapidement long et fastidieux. Il serait bien plus avantageux de n'avoir à déclarer qu'une seule variable capable de stocker toutes ces informations. Les enregistrements répondent à cette demande. Ils permettent, via la création d'un nouveau type, de créer des variables « à tiroirs ». Un type enregistrement se déclare comme suit :

 
Sélectionnez

type
  nom_de_type = record
    declarations_de_membres
  end;

nom_de_type désigne le nom par lequel le nouveau type enregistrement sera accessible. le mot Pascal réservé record débute un bloc de déclaration de membres, qui est terminé par le mot réservé end suivi comme d'habitude d'un point-virgule.

Ce bloc de déclarations de membres permet de décrire ce que contiendra une variable ou constante de ce type. Ce bloc est presque identique à un bloc de déclaration de variables, mis à part le mot réservé var qui est alors omis, et bien entendu le fait qu'on ne déclare absolument pas des variables mais qu'on décrit plutôt la structure du type : ce qu'il contiendra et sous quelle forme.

La déclaration de type ci-dessous déclare un type enregistrement pouvant contenir les informations d'un logiciel :

 
Sélectionnez

type
  TLogiciel = record
    Nom,             { nom du logiciel }
    Version,         { version du logiciel }
    Langue: string;  { langue du logiciel }
    Taille: integer; { taille exprimée en kilo-octets }
  end;

La présentation suggérée ici est à mon sens la meilleure car très lisible et revélatrice de la structure du bloc. Mis à part cette considération purement esthétique, un nouveau type nommé 'TLogiciel' (Rappel : par convention, les noms de types personnalisés, et en particulier les types enregistrement, commencent par un T majuscule, suivi d'un descriptif. C'est, au même titre que la présentation du code source, la convention adoptée par les créateurs de Delphi).

Concrètement, ce nouveau type permettra d'utiliser quatre membres : 'Nom', 'Version', 'Langue' et 'Taille' de types donnés dans la déclaration du type. On pourra ensuite déclarer des variables ou des constantes de ce type (note : il est possible mais vraiment peu recommandable de déclarer directement une variable de type enregistrement sans avoir créé ce type dans un bloc de déclaration de type. Il y a de bonnes raisons à cela : si vous voulez les connaître et voulez quand même utiliser cette possibilité, allez voir dans l'aide en ligne de Delphi.

En ce qui concerne les variables de type enregistrement, la variable en tant que telle ne nous intéresse pas vraiment, c'est plutôt son contenu qui nous intéresse. Pour accéder à un membre, il faut le qualifier, c'est-à-dire utiliser le nom d'une constante ou d'une variable qui contient un tel membre, la faire suivre d'un point et enfin du nom du membre. Ce membre se comporte alors respectivement comme une constante ou une variable du type déclaré pour ce membre dans la déclaration du type. Voici un exemple pour éclairer tout cela :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TLogiciel = record
    Nom,
    Version,
    Langue: string;
    Taille: integer;
  end;
var
  Log1, Log2: TLogiciel;
begin
  Log1.Nom := 'Delphi';
  Log2 := Log1;
end;

L'exemple ci-dessus déclare le type TLogiciel, puis deux variables de ce type. Mis à part la nouvelle forme de déclaration des types enregistrement, le principe reste le même qu'avec les autres types, à savoir qu'on définit au besoin un nouveau type, puis qu'on l'utilise dans la définition de variables ou de constantes (Nous verrons les constantes après cet exemple). Les deux instructions de la procédure sont des affectations. Dans la première, on affecte une chaîne au membre 'Nom' de la variable 'Log1' (cette variable est de type TLogiciel donc contient bien un membre nommé 'Nom'). 'Log1' étant une variable, 'Log1.Nom' est alors utilisable comme une variable, de type 'string' en l'occurence. On peut donc lui affecter une valeur chaîne.

La deuxième instruction est plus simple : c'est une affectation toute simple. La valeur d'une variable est affectée à une autre. Cette instruction affecte les valeurs de chacun des membres. On aurait pu faire la même chose en écrivant :

 
Sélectionnez

Log2.Nom := Log1.Nom;
Log2.Version := Log1.Version;
Log2.Langue := Log1.Langue;
Log2.Taille := Log1.Taille;

ce qui a pour inconvénient majeur d'être plus long et de poser un problème si on modifie le type 'TLogiciel'. Imaginez en effet qu'on ajoute un membre nommé 'Auteur'; il faudrait, dans l'exemple ci-dessus, ajouter une affectation, alors que l'affectation de Tab1 à Tab2 ne changera pas.

L'exemple de type enregistrement donné est assez simple. Rien n'interdit d'utiliser des membres de types plus complexes. L'exemple ci-dessous est basé sur TLogiciel, mais introduit des changements intéressants.

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
type
  TLangueLogiciel = (llInconnue, llFrancais, llAnglais, llAutre);
  TLogiciel = record
    Nom,
    Version,
    Auteur: string;
    Langue: TLangueLogiciel;
    LangueNom: string;
    Taille: integer;
  end;
var
  Log1: TLogiciel;
begin
  Log1.Langue := llAnglais;
end;

Dans cet exemple, un nouveau type énuméré a été créé pour spécifier la langue d'un logiciel. On prévoit le cas où la langue ne serait pas parmi les cas prévus en donnant la valeur 'llAutre'. Dans ce cas, un membre 'LangueNom' de 'TLogiciel' sera utilisé pour stocker une chaîne décrivant la langue. Tout le reste n'est que très conventionnel.

En ce qui concerne les constantes de type enregistrement, l'exemple suivant devrait vous faire comprendre comment on procède pour les déclarer :

 
Sélectionnez

const
  LogParDef: TLogiciel =
    (Nom: '';
     Version: '';
     Auteur: '';
     Langue: llInconnue;
     LangueNom: '';
     Taille: 0);

On donne la valeur de chaque membre au moyen de : membre : valeur. La liste des valeurs est délimitée par des point-virgules et est mise entre parenthèses.

Le type enregistrement n'est évidemment pas un type ordinal, mais par contre, il peut être utilisé dans des tableaux (y compris dynamiques) et dans d'autres types enregistrements. Par exemple :

 
Sélectionnez

type
  TLangueLogiciel = (llInconnue, llFrancais, llAnglais, llAutre);
  TLangue = record
    Code: TLangueLogiciel;
    Nom: string;
  end;
  TLogiciel = record
    Nom,
    Version,
    Auteur: string;
    Langue: TLangue;
    Taille: integer;
  end;
  TTabLogiciel = array of TLogiciel;

Cet exemple montre déjà des types bien élaborés. Les deux premiers types sont respectivement énumérés et enregistrement. Le troisième ('TLogiciel') utilise comme type d'un de ses membres un type enregistrement. Si nous avions une variable 'Log1' de type 'TLogiciel', il faudrait écrire 'Log1.Langue.Code' pour accéder au code de la langue du logiciel. Le dernier type est encore un peu plus difficile : c'est un tableau dynamique, dont les éléments sont de type 'TLogiciel'. Imaginons que nous ayons une variable de ce type nommée 'Logiciels' : pour accèder au code de la langue du troisième logiciel de ce tableau, il faudrait écrire 'Logiciels[2].Langue.Code'. car 'Logiciels[2]' est de type 'TLogiciel' et 'Logiciels[2].Langue' est donc de type 'TLangue'.

Ce dernier type vous parait peut-être bien délicat, mais imaginez qu'avec ce type, on peut gèrer sans trop de peine une liste de logiciels, avec leurs particularités respectives, et tout cela avec une seule variable.

VII-G-2. Manipulation avancée des enregistrements

Il peut rapidement devenir fastidieux d'écrire le préfixe « variable. » devant les membres d'un enregistrement. Pour cela il existe une technique : l'utilisation d'un bloc with. Ce dernier permet, à l'intérieur d'un bloc spécifique, de manipuler les membres d'un enregistrement comme s'ils étaient directement accessibles (sans avoir à écrire « variable. »). Ce bloc est considéré comme une instruction unique. Voici sa syntaxe :

 
Sélectionnez

with enregistrement do
  instruction;

Le mot Pascal réservé with commence l'instruction. Vient ensuite une constante, un paramètre ou une variable de type enregistrement, puis le mot réservé do suivi d'une unique instruction. Cette instruction peut utiliser les membres de la donnée entre 'with' et 'do' sans passer par « variable. ». Mais il est possible de remplacer instruction par un bloc d'instructions. Un bloc d'instructions se comporte comme une instruction unique, mais est composé de plusieurs instructions. On utilise pour le créer les mots Pascal réservés begin et end entre lesquels on écrit les instructions, comme on le ferait pour une procédure ou une fonction. Voici un exemple :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
var
  Log1: TLogiciel;
begin
  with Log1 do
    begin
      Nom := 'Delphi';
      Version := '5';
      Auteur := 'Borland/Inprise';
      Langue.Code := llAnglais;
      Langue.Nom := '';
      Taille := 275000;
    end;
end;

Dans l'exemple ci-dessus, TLogiciel est tel qu'il a été créé dans les exemples précédents. La procédure ne contient qu'une seule instruction : un bloc with. Ce bloc permet d'accèder aux membres de la variable Log1 sans qualifier ses membres. Le bloc est utilisé pour initialiser la variable. Il faut bien comprendre ici que chaque instruction à l'intérieur du bloc with devrait être adaptée pour fonctionner en dehors de ce bloc, mais que ce sont bel et bien des instructions réunies en une seule grâce aux deux mots réservés begin et end.

Il est dés lors possible, du fait que les instructions à l'intérieur d'un bloc with sont de vraies instructions, d'écrire un bloc with à l'intérieur d'un autre. Il faudra donner comme paramètre une donnée qui n'a rien à voir avec 'Log1' ou un membre de 'Log1' (with n'accepte que des enregistrements). Voici par exemple ce qu'il est possible de faire dans notre cas :

 
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
var
  Log1: TLogiciel;
begin
  with Log1 do
    begin
      Nom := 'Delphi';
      Version := '5';
      Auteur := 'Borland/Inprise';
      with Langue do
        begin
          Code := llAnglais;
          Nom := '';
        end;
      Taille := 275000;
    end;
end;

Je n'en dirai pas plus, sauf qu'il faudra éviter d'utiliser ce type de bloc à tort et à travers : il faudra avoir au minimum 2 à 3 manipulations sur les membres d'un enregistrement pour passer par un bloc with.

VII-H. Types et paramètres de procédures et fonctions

Tout ce verbiage sur les types nous a (presque) fait oublier que les types sont utilisés à un autre endroit important : dans les déclarations de fonctions et procédures, pour donner les types des paramètres et le type de résultat pour une fonction.

Et ici se pose un petit problème : une partie seulement des types que nous venons de voir sont acceptés pour les paramètres et pour le résultat d'une fonction sous Delphi 2 (Dans Delphi 5, tous les types sont autorisés). Je n'ai pas encore pu faire de tests sous Delphi 2, 3 et 4, donc il convient de se limiter aux types ordinaux et aux chaînes de caractères (ce qui inclue les types énumérés et les booléens).

VII-I. Conclusion et retour sur terre

Au terme de ce chapitre consacré aux types de données, vous admettrez certainement que je vous ai ennuyé. C'est sûrement très vrai et je m'en excuse.

Ces types de données, pourtant, vous allez souvent les utiliser dans les programmes que vous écrirez. Le chapitre qui suit est davantage concentré sur les instructions en Pascal que sur les déclarations de toutes sortes. Vous apprendrez surtout qu'un programme est rarement exécuté de manière linéaire, mais que l'exécution peut « sauter » des instructions ou en répeter certaines.


précédentsommairesuivant

  

Copyright © 2008 Frédéric Beaulieu. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.