Logo

Marco Cantù
L'essentiel sur Pascal

Traduit de l'anglais par Iannis Papageorgiadis ipapag@village.uunet.be

Chapitre 4
Les types de données définis par l'utilisateur

Avec la notion de type, une des grandes idées introduites par le langage Pascal est la possibilité de définir dans un programme de nouveaux types de données. Les programmeurs peuvent définir leurs propres types de données au moyen de constructeurs de types, tels des types sous-ensemble, des tableaux, des enregistrements, des types énumérés, des pointeurs et des ensembles. Le type de données le plus important défini par l'utilisateur est la classe; il fait partie des extensions orientées objet de Pascal Objet (point qui n'est pas traité dans cet ouvrage).

Si vous pensez que les constructeurs de type sont courants dans la plupart des langages de programmation, vous avez raison, mais Pascal a été le premier langage à introduire cette idée dans une voie formelle et très précise. Rares sont les langages possédant tant de mécanismes pour définir de nouveaux types.

Types nommés et non-nommés

Ces types peuvent être nommés pour une utilisation ultérieure ou appliqués directement à une variable. Lorsque l'on donne un nom à un type, on doit prévoir une section spécifique dans le code, comme on le voit ci-après :
type
  // définition d'un intervalle
  Uppercase = 'A'..'Z';
  // définition d'un tableau
  Temperatures = array [1..24] of Integer;
  // définition d'un enregistrement
  Date = record
    Month: Byte;
    Day: Byte;
    Year: Integer;
  end;


  // définition d'un type énuméré
  Colors = (Red, Yellow, Green, Cyan, Blue, Violet);
  // définition d'un ensemble
  Letters = set of Char;
Des constructions similaires de définition de type peuvent être utilisées directement pour définir une variable sans qu'elle ait un nom de type explicite, comme on le voit dans le code suivant :
var
  DecemberTemperature: array [1..31] of Byte;
  ColorCode: array [Red..Violet] of Word;
  Palette: set of Colors;
Remarque : En général, il faut éviter d'utiliser des types non-nommés comme dans le code ci-dessus, parce qu'on ne peut pas les passer en tant que paramètres aux routines ou déclarer d'autres variables du même type. Les règles Pascal de compatibilité de type sont, en fait, basées sur les noms des types, et non sur la définition réelle des types. Deux variables de deux types identiques sont toujours non compatibles, à moins que leurs types n'aient exactement le même nom, et des types non-nommés sont nommés de façon interne par le compilateur. Habituez vous à définir un type de données chaque fois que vous avez besoin d'une variable complexe, et vous ne regretterez pas le temps que vous y aurez consacré.
Mais que signifient ces définitions de type ? Nous donnerons quelques descriptions pour ceux qui ne sont pas familiers avec les constructions de type du Pascal. Nous essayerons aussi de souligner les différences entre les mêmes constructions dans d'autres langages de programmation; les sections suivantes pourront ainsi vous intéresser même si vous êtes familiarisé avec les définitions de type présentées ci-dessus. Enfin, nous présenterons quelques exemples de Delphi et nous introduirons quelques outils permettant d'accéder dynamiquement à l'information de type.

Les types intervalle

Un type intervalle définit un intervalle de valeurs à l'intérieur de l'intervalle d'un autre type (d'où le nom de sous étendu). Vous pouvez définir un intervalle du type Integer, de 1 à 10 ou de 100 à 1000, ou définir un intervalle du type Char comme dans:
type
  Ten = 1..10;

  OverHundred = 100..1000;

  Uppercase = 'A'..'Z';
Dans la définition d'un intervalle, on ne doit  pas spécifier le nom du type de base. On doit seulement fournir deux constantes de ce type. Le type d'origine doit être un type scalaire et le type dérivé sera un autre type scalaire.

Quand vous avez défini un intervalle, vous pouvez légalement lui assigner une valeur à l'intérieur de ce type. Le code suivant est valide:

var
  UppLetter: Uppercase;
begin
  UppLetter := 'F';
Mais celui-ci ne l'est pas:
var
  UppLetter: Uppercase;
begin
  UppLetter := 'e'; // erreur de compilation
En écrivant le code ci-dessus, vous provoquez une erreur de compilation: "L'expression constante dépasse les limites de sous étendu". Si par contre vous écrivez le code suivant:
var
  UppLetter: Uppercase;
  Letter: Char;
begin
  Letter :='e';
  UppLetter := Letter;
Delphi le compilera. A l'exécution, si vous avez activé l'option Vérification des limites (dans la page Compilateur de la boîte de dialogue Options du menu Projet), vous obtiendrez un message d'erreur Erreur de vérification d'étendue.

Remarque : Nous suggérons d'activer cette option du compilateur pendant le développement de votre programme; il sera ainsi plus robuste et plus facile à déboguer puisqu'en cas d'erreur, vous aboutirez à un message d'erreur explicite et non à un comportement indéterminé. Vous pouvez éventuellement désactiver l'option pour la construction finale du programme afin de le rendre un peu plus rapide. Cependant, la différence est vraiment minime; c'est pourquoi nous suggérons de laisser activés tous ces contrôles, même dans un programme de navigation. Cela vaut aussi pour toutes les autres options de contrôle à l'exécution, tels que dépassement de capacité et contrôle de pile.

Les types énumérés

Les types énumérés constituent un autre type scalaire défini par l'utilisateur. Au lieu d'indiquer un intervalle d'un type existant, vous listez dans une énumération toutes les valeurs possibles pour le type en question. En d'autres mots, une énumération est une liste de valeurs. Voici quelques exemples :
type
  Colors = (Red, Yellow, Green, Cyan, Blue, Violet);
  Suit = (Club, Diamond, Heart, Spade);
Chaque valeur de la liste possède un rang associé, commençant à zéro. Si vous appliquez la fonction Ord à une valeur d'un type énuméré, vous obtiendrez cette valeur sur base zéro. Par exemple, Ord (Diamond) retourne 1.
 
Remarque : Les types énumérés peuvent avoir différentes représentations internes. Par défaut, Delphi utilise une représentation 8 bits, à moins qu'il y ait plus de 256 valeurs différentes, auquel cas il utilise une représentation 16 bits. Il existe également une représentation 32 bits, qui peut être utile pour la compatibilité avec les bibliothèques C ou C++. Vous pouvez parfaitement modifier le comportement par défaut en utilisant la directive de compilation $Z.
 
La VCL (bibliothèque des composants visuels) utilise les types énumérés à plusieurs endroits. Par exemple, le style de la bordure d'une fiche est défini comme suit :
type
  TFormBorderStyle = (bsNone, bsSingle, bsSizeable,
    bsDialog, bsSizeToolWin, bsToolWindow);
Quand la valeur d'une propriété est une énumération, vous pouvez habituellement choisir dans la liste de valeurs affichées dans l'Inspecteur d'Objets, comme le montre la figure 4.1.

Figure 4.1: Une propriété de type énuméré dans l'inspecteur d'objets

Le fichier d'aide de Delphi donne généralement la liste des valeurs possibles d'une énumération. Comme alternative vous pouvez utiliser le programme OrdType, dans le répertoire TOOLS du code source du livre, pour voir la liste des valeurs de chaque énumération Delphi, ensemble, intervalle et tout autre type scalaire. Vous trouverez un exemple de l'affichage de ce programme à la figure 4.2.

FIGURE 4.2 : Information détaillée à propos d'un type énuméré, comme l'affiche le programme OrdType du répertoire TOOLS du code source du livre.

Les types ensemble

Les types ensemble désignent un groupe de valeurs où la liste des valeurs disponibles est indiquée par le type scalaire sur lequel est basé l'ensemble. Ces types scalaires sont habituellement limités et très souvent représentés par une énumération ou par un intervalle. Si nous prenons l'intervalle 1..3, les valeurs possibles sur lesquelles est basé l'ensemble comprennent uniquement le 1, uniquement le 2, uniquement le 3, la paire de valeurs 1 et 2, la paire de valeurs 1 et 3, la paire de valeurs 2 et 3, les trois valeurs, ou aucune d'entre elles.

Une variable contient habituellement une des valeurs possibles de l'intervalle de son type. Une variable de type ensemble, par contre, peut ne contenir aucune, ou contenir une, deux, trois ou plusieurs valeurs de l'intervalle. Elle peut même comprendre toutes les valeurs. Voici un exemple d'un ensemble :

type
  Letters = set of Uppercase;
Maintenant nous pouvons définir une variable de ce type et lui assigner quelques valeurs du type de base. Pour indiquer quelques valeurs dans un ensemble, on écrit une liste d'éléments, séparés par une virgule, encadrés par des crochets carrés. Le code suivant montre l'assignation à une variable de plusieurs valeurs, d'une seule valeur et d'une valeur vide :
var

  Letters1, Letters2, Letters3: Letters;
begin
  Letters1 := ['A', 'B', 'C'];
  Letters2 := ['K'];
  Letters3 := [];
En Delphi, un ensemble est généralement utilisé pour indiquer des symboles non exclusifs. Par exemple, les deux lignes de code suivantes (qui font partie de la bibliothèque Delphi) déclarent une énumération d'icônes possibles pour une fenêtre et le type ensemble correspondant :
type
  TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp);
  TBorderIcons = set of TBorderIcon;
En fait, une fenêtre donnée pourrait ne comporter aucune de ces icônes, ou l'une d'elles, ou plus d'une. Lorsque on travaille dans l'inspecteur d'objets (Figure 4.3), on peut fournir les valeurs d'un ensemble en étendant la sélection (double cliquez sur le nom de la propriété ou cliquez sur le signe plus à sa gauche) et en insérant ou en éliminant chacune des valeurs.

Figure 4.3 : Une propriété de type ensemble dans l'inspecteur d'objets

 

Une autre propriété basée sur un type ensemble est le style d'une fonte (police). Ces valeurs indiquent une police en gras, en italique, en souligné ou en barré. Bien sûr la même fonte peut avoir les deux attributs italique et gras, ne pas avoir d'attribut ou les avoir tous. Pour cette raison le style est déclaré comme étant  un ensemble.

Font.Style := []; // aucun style
Font.Style := [fsBold]; // gras uniquement
Font.Style := [fsBold, fsItalic]; // deux styles
Vous pouvez également travailler de plusieurs manières différentes sur un ensemble, y compris en ajoutant deux variables du même type ensemble (ou, pour être plus précis, en calculant l'union de deux variables ensemble) :
Font.Style := OldStyle + [fsUnderline]; // deux ensembles
En outre,  vous pouvez utiliser les exemples OrdType inclus dans le répertoire TOOLS du code source pour voir la liste des valeurs possibles de plusieurs ensembles définis par la bibliothèque des composants Delphi.

Les types tableau

Les types tableau définissent des listes d'un nombre fixe d'éléments d'un type spécifique. En général, on utilise un indice entre crochets pour accéder à un élément du tableau. Les crochets sont aussi utilisés pour spécifier les valeurs possibles de l'indice lors de la déclaration du tableau. Par exemple, on définit un groupe de 24 entiers à l'aide du code suivant :
type
  DayTemperatures = array [1..24] of Integer;
Dans la définition du tableau, vous devez indiquer un type intervalle à l'intérieur des crochets, ou définir un nouveau type intervalle spécifique en utilisant deux constantes d'un type scalaire. Cet intervalle spécifie les indices valides du tableau. Puisque vous spécifiez aussi bien l'indice maximum que l'indice minimum du tableau, les indices ne doivent pas commencer à zéro, comme c'est le cas en C, C++, Java et dans d'autres langages de programmation.

Puisque les indices des tableaux sont basés sur les intervalles, Delphi peut contrôler leur étendue comme nous l'avons déjà dit. Une constante d'intervalle invalide aboutit à une erreur de compilation ; et un indice hors intervalle utilisé à l'exécution aboutit à une erreur d'exécution si l'option correspondante du compilateur est activée.

En utilisant la définition de tableau ci-dessus, vous pouvez affecter la valeur d'une variable DayTemp1 de type DayTemperatures comme ci-après:

type
  DayTemperatures = array [1..24] of Integer;

var
  DayTemp1: DayTemperatures;
  
procedure AssignTemp;
begin
  DayTemp1 [1] := 54;
  DayTemp1 [2] := 52;
  ...
  DayTemp1 [24] := 66;
  DayTemp1 [25] := 67; // erreur de compilation
Un tableau peut avoir plusieurs dimensions, comme dans les exemples suivants :
type
  MonthTemps = array [1..24, 1..31] of Integer;
  YearTemps = array [1..24, 1..31, Jan..Dec] of Integer;
Ces deux types tableau sont construits sur les mêmes types de noyau. Ainsi vous pouvez les déclarer en utilisant les types de données précédentes, comme dans le code suivant :
type
  MonthTemps = array [1..31] of DayTemperatures;
  YearTemps = array [Jan..Dec] of MonthTemps;
Cette déclaration inverse l'ordre des indices présentés ci-dessus, mais elle permet aussi l'assignation de blocs entiers entre des variables. Par exemple, l'instruction suivante copie les températures de Jan dans Feb :
var
  ThisYear: YearTemps;
begin
  ...
  ThisYear[Feb] := ThisYear[Jan];
On peut également définir un tableau à base zéro, un type tableau avec l'indice inférieur à zéro. Généralement l'utilisation de limites plus logiques est un avantage, puisque vous ne devez pas utiliser l'indice 2 pour accéder au troisième élément, etc. Windows, cependant, utilise invariablement les tableaux à base zéro (parce qu'il est construit sur le langage C), et la bibliothèque de composants Delphi a tendance à faire de même.

Si vous devez travailler sur un tableau, vous pouvez toujours tester ses limites en utilisant les fonctions standard Low et High, qui retournent les limites inférieure et supérieure. Il est vivement recommandé d'utiliser Low et High lorsqu'on travaille sur un tableau, spécialement dans les boucles, puisque cela rend le code indépendant de l'intervalle du tableau. Par la suite, on peut modifier l'intervalle déclaré des indices du tableau, et le code qui utilise Low et High fonctionnera encore.
 

Remarque : Notons qu'il n'y a pas de pénalité en temps d'exécution lorsqu'on utilise les fonctions Low et High avec les tableaux. Elles sont résolues à la compilation dans des expressions constantes, et non par de véritables appels à des fonctions. Cette résolution à la compilation d'expressions et d'appels de fonction se produit aussi pour de nombreuses autres fonctions système simples.
 
Delphi utilise les tableaux principalement sous la forme de propriétés de type tableau. Nous avons déjà vu un exemple d'une telle propriété dans l'exemple TimeNow, pour accéder à la propriété Items d'un composant boîte liste (ListBox). Nous donnerons quelques exemples supplémentaires concernant les propriétés de type tableau dans le chapitre suivant, lorsque nous traiterons des boucles Delphi.
 
Remarque : Delphi 4 a introduit dans Pascal Objet les tableaux dynamiques, tableaux qui peuvent être redimensionnés pendant l'exécution en allouant la quantité mémoire appropriée. Il est facile d'utiliser les tableaux dynamiques, mais il ne s'agit pas, nous semble-t-il, d'un sujet à traiter dans cette discussion sur le Pascal. Vous trouverez une description des tableaux dynamiques Delphi dans le chapitre 8.

Les types enregistrement

Les types enregistrement définissent une collection d'éléments de types différents. Chaque élément ou champ possède son propre type. La définition d'un type enregistrement spécifie la liste de tous les champs en donnant à chacun un nom qu'on utilisera plus tard pour y accéder.

Voici un petit listing avec la définition d'un type enregistrement, la déclaration d'une variable de ce type et quelques instructions utilisant cette variable :

type

  Date = record

    Year: Integer;

    Month: Byte;

    Day: Byte;

  end;

  

var

  BirthDay: Date;

  

begin

  BirthDay.Year := 1997;

  BirthDay.Month := 2;

  BirthDay.Day := 14;
Les classes et les objets peuvent être considérés comme une extension du type enregistrement. Les bibliothèques Delphi ont tendance à utiliser les types classe plutôt que les types enregistrement, mais il existe de nombreux types enregistrement définis par l'API Windows.
Les types enregistrement peuvent comporter également une partie variable, c'est-à-dire que plusieurs champs peuvent partager le même espace mémoire, même s' ils sont de types différents. (Ceci correspond à une union en C). Ou bien, vous pouvez utiliser des champs variables ou groupes de champs pour accéder au même emplacement mémoire à l'intérieur d'un enregistrement, mais en considérant de telles valeurs d'un point de vue différent. La principale utilisation de ce type consistait à stocker des données similaires mais différentes et obtenir ainsi un effet semblable à l'effet de transtypage (quelque chose de moins utile depuis l'introduction du transtypage également dans Pascal). L'utilisation des types d'enregistrement variables a largement été remplacée par la technique de la programmation orientée objet et par d'autres techniques modernes, bien que Delphi les utilise dans quelques cas particuliers.

L'utilisation d'un type enregistrement variable n'est pas sans danger et ne constitue pas une pratique de programmation à recommander, spécialement pour les débutants. Les programmeurs chevronnés peuvent par contre utiliser les types enregistrement variable, et le noyau des bibliothèques Delphi les utilise. De toute façon, vous n'aurez pas besoin de vous attaquer à ceux-ci tant que vous n'êtes pas un expert Delphi.

Les pointeurs

Un type pointeur définit une variable qui contient l'adresse en mémoire d'une autre variable d'un type de données donné (ou d'un type non défini). Ainsi une variable pointeur fait référence indirectement à une valeur. La définition d'un type pointeur ne se fait pas à l'aide d'un mot réservé spécifique; on utilise plutôt un caractère spécial. Ce caractère spécial est le symbole pointeur (^) (accent circonflexe ou caret) :
type
  PointerToInt = ^Integer;
Une fois que l'on a défini une variable pointeur, on peut lui affecter l'adresse d'une autre variable du même type en utilisant l'opérateur @ :
var
  P: ^Integer;
  X: Integer;
begin
  P := @X;
  // modifier la valeur de deux façons différentes
  X := 10;
  P^ := 20;
Lorsqu' on a un pointeur P, avec l'expression P on fait référence à l'adresse de l'emplacement mémoire auquel se réfère aussi le pointeur; par l'expression P^ on fait référence au contenu réel de cet emplacement mémoire. C'est la raison pour laquelle dans le fragment de code ci-dessus P^ correspond à X.

Au lieu de faire référence à un emplacement mémoire existant, un pointeur peut faire référence à un bloc mémoire alloué dynamiquement (dans le tas (heap)) à l'aide de la procédure New. Dans ce cas quand on n'a plus besoin du pointeur, on doit aussi se débarrasser de la mémoire qu'on avait allouée dynamiquement, en faisant appel à la procédure Dispose.

var
  P: ^Integer;
begin
  // initialization
  New (P);
  // operations
  P^ := 20;
  ShowMessage (IntToStr (P^));
  // termination
  Dispose (P);
end;
Si un pointeur n'a pas de valeur, on peut lui affecter la valeur nil. On peut alors tester si un pointeur est nil pour voir s'il fait à ce moment référence à une valeur. Ceci est souvent utilisé, parce que le fait de déréférencer un pointeur non valide provoque une violation d'accès (connue également sous le nom de faute générale de protection, GPF) :
procedure TFormGPF.BtnGpfClick(Sender: TObject);
var
  P: ^Integer;
begin
  P := nil;
  ShowMessage (IntToStr (P^));
end;
Vous pouvez voir un exemple de l'effet de ce code en exécutant l'exemple GPF (ou en examinant la figure correspondante 4.4). L'exemple contient également des fragments du code ci-dessus.

Figure 4.4 : L'erreur système provoquée par l'accès à un pointeur nil, dans l'exemple GPF.

Dans le même programme vous pouvez trouver un exemple d'accès sûr aux données. Dans ce deuxième cas, le pointeur est assigné à une variable locale et il peut être utilisé sans danger, mais nous avons quand même ajouté un contrôle de sécurité:

procedure TFormGPF.BtnSafeClick(Sender: TObject);

var

  P: ^Integer;

  X: Integer;

begin

  P := @X;

  X := 100;

  if P <> nil then

    ShowMessage (IntToStr (P^));

end;
Delphi définit aussi un type de données Pointer qui indique des pointeurs sans type (comme void* en C). Si vous utilisez un pointeur sans type, vous devez utiliser GetMem au lieu de New. La procédure GetMem est requise chaque fois que la taille de la variable mémoire à allouer n'est pas définie.

Le fait que les pointeurs sont rarement nécessaires en Delphi est un avantage considérable de cet environnement. Néanmoins, comprendre les pointeurs est important pour la programmation avancée et pour la compréhension totale du modèle objet de Delphi, qui utilise les pointeurs "dans les coulisses".
 

Remarque : Bien que, en Delphi, on n'utilise pas souvent les pointeurs, on doit fréquemment utiliser une construction fort similaire, à savoir les références. Chaque instance d'objet est en réalité un pointeur implicite ou une référence à sa donnée réelle. De toute façon ceci est parfaitement transparent pour le programmeur qui utilise les variables objet exactement comme tout autre type de données.

Les types fichier

Un autre constructeur de type spécifique Pascal est le type file. Les types file représentent des fichiers physiques sur disque ; il s'agit certes là d'une particularité du langage Pascal. On peut définir un nouveau type de données fichier de la façon suivante :
type
  IntFile = file of Integer;
On peut ensuite ouvrir un fichier physique associé à cette structure et y écrire des valeurs entières ou lire les valeurs courantes de ce fichier. (Les exemples concernant les fichiers figuraient dans les éditions plus anciennes de Mastering Delphi et nous avons prévu de les annexer également ici).

L'utilisation des fichiers en Pascal est très aisée, mais dans Delphi il existe aussi des composants capables de stocker leur contenu dans un fichier ou de le charger depuis un fichier. Il existe un support de suivi, sous la forme de flux, et également un support base de données.

Conclusion

Ce chapitre sur les types de données définis par l'utilisateur complète notre tour du système de types Pascal. Nous sommes maintenant prêts à examiner les instructions fournies par le langage pour manipuler les variables que nous avons définies.

Prochain chapitre: Les instructions

© Copyright Marco Cantù, Wintech Italia Srl 1995-99