Newsletter Developpez.com

Inscrivez-vous gratuitement au Club pour recevoir
la newsletter hebdomadaire des développeurs et IT pro

POO à GOGO : les enregistrements avec Free Pascal et Lazarus (1/2)

Enregistrements simples et compactage

Ce tutoriel propose une étude des enregistrements en Free Pascal applicable à Lazarus. Après quelques définitions, il s'agira d'examiner les enregistrements simples et leur compactage, prélude obligé à la découverte des enregistrements étendus introduits par la Programmation Orientée Objet et de leurs différentes facettes.

16 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Les programmes de test sont présents dans le répertoire exemples accompagnant le présent document.

Contrairement aux tableaux (array) et aux ensembles (set) qui proposent des structures pour des données homogènes, un enregistrement (record) est une structure fixe qui regroupe des données de différents types sous un même nom.

Avec les enregistrements, on pourra ainsi réunir un numéro, un nom de voie, un code postal et un nom de ville dans un enregistrement qui constituera une adresse. Comme tous les autres types, le nouveau type défini sera utilisable pour déclarer des variables.

Bien que souvent employés pour des fichiers, les enregistrements ne sont pas liés intrinsèquement à eux : il est tout à fait possible de définir un enregistrement qui regrouperait, par exemple, la partie réelle et la partie imaginaire d'un nombre complexe, hors de toute notion de fichier. De la même manière, le graphisme fait souvent appel à des enregistrements représentant des points définis par leurs coordonnées dans un repère orthonormé, sans qu'il soit là encore question de fichier.

Chaque donnée qui apparaît dans un enregistrement est appelée champ. Dans l'enregistrement d'une adresse, le code postal ou le nom de la ville sont des exemples de champs. De même, la partie imaginaire est un champ d'un enregistrement représentant un nombre complexe tandis que l'abscisse est un champ d'un enregistrement représentant les coordonnées d'un point.

II. Les enregistrements traditionnels

On appelle enregistrements traditionnels ou simples les enregistrements qui ne comprennent que des champs. En ce sens, ils s'opposent aux enregistrements étendus qui seront étudiés ultérieurement.

II-A. Déclarer un enregistrement traditionnel

II-A-1. La déclaration simple

Un enregistrement étant un type a priori inconnu du compilateur, il est nécessaire de le déclarer. On utilise à cette fin le mot réservé record.

Un enregistrement correspondant à une adresse postale pourrait prendre la forme suivante :

 
Sélectionnez
type
  TAddress = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
  end;

var
  AnAddress, AnotherAddress: TAddress;

Le type déclaré est TAddress. Grâce à cette déclaration, il devient possible de déclarer des variables de ce type : AnAddress et AnotherAddress par exemple. À leur tour, ces variables permettront de travailler avec des parties des adresses grâce aux champs définis.

La déclaration d'un type enregistrement n'alloue aucune mémoire, que ce soit pour la structure ou pour les champs.

Les types des champs d'un enregistrement sont quelconques. Comme toujours en Free Pascal, ils doivent cependant être déclarés avant toute utilisation, ce qu'il est possible de faire à l'intérieur même de la structure :

 
Sélectionnez
type
  TMyDate = record
    Day: 1..31;
    Month: (Janvier, Fevrier, Mars, Avril, Mai, Juin, Juillet, Aout,
      Septembre, Octobre, Novembre, Decembre);
    Year: 1900..2100;  
  end;

On voit dans l'exemple ci-dessus que l'intervalle Day et l'énumération Month ont été déclarés à la volée au sein de l'enregistrement TMyDate.

Les types déclarés à l'intérieur d'une structure d'enregistrement ne sont accessibles que via cette structure. De plus, ils ne peuvent être définis sous la forme d'une déclaration suivie d'une implémentation.

Il est même possible de définir un enregistrement en déclarant une variable :

 
Sélectionnez
var
  MyDate: record
    Day: 1..31;
    Month: (Janvier, Fevrier, Mars, Avril, Mai, Juin, Juillet, Aout,
      Septembre, Octobre, Novembre, Decembre);
    Year: 1900..2100;  
  end;

La variable MyDate est ici correctement définie. Cependant, une telle déclaration n'est pratiquement jamais rencontrée : d'une part, la variable définie ne sera compatible avec aucune autre variable, quand bien même les deux auraient la même structure ; d'autre part, elle annule l'intérêt principal des enregistrements, à savoir la non-répétition de la saisie de structures répétitives !

II-A-2. L'imbrication d'enregistrements

Comme un enregistrement comprend n'importe quel type de champs, il est possible de définir un champ qui sera lui-même un enregistrement (en dehors de lui-même, bien sûr).

On peut imaginer un type d'enregistrement qui correspondrait à l'identité d'une personne et qui comprendrait une partie adresse postale. La déclaration d'un tel type prendrait la forme suivante :

 
Sélectionnez
type
  TAddress = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
  end;

  TUser = record
    FirstName: string;
    SurName: string;
    Address: TAddress;
  end;

var
  Users: array[1..10] of TUser;

On voit que le type TAddress est inclus dans le type TUser et qu'ils déclarent tous les deux une nouvelle variété d'enregistrement. Une variable Users regroupe dans un tableau un ensemble de dix enregistrements de type TUser.

Il est même possible de déclarer un type d'enregistrement à l'intérieur d'un autre :

 
Sélectionnez
TUser = record
    FirstName: string;
    SurName: string;
    Address: record
      Number: Integer;
      Street: string;
      ZipCode: Integer;
      Town: string;
    end;
  end;

Cette déclaration est équivalente à celles utilisées ci-avant, à ceci près que le type TAddress n'existe plus en tant que tel.

Image non disponible

S'il n'y a pas de limite au niveau d'imbrication des enregistrements, une grande complexité nuit à la lisibilité du code. Il est par conséquent rare d'avoir à déclarer un type d'enregistrement renfermant plus d'un niveau d'imbrication.

II-A-3. Les parties avec variantes

Il arrive parfois qu'une partie de l'enregistrement dépende d'une condition particulière. Par exemple, on peut vouloir distinguer les parcours des élèves d'une filière donnée par des renseignements différents suivant les options qu'ils auront choisies. Free Pascal offre cette possibilité grâce aux parties avec variantes.

La structure revêt alors une forme particulière, proche de celle d'un case ordinaire :

 
Sélectionnez
type
  TProfile = (proProgramming, proNetWorks);
  TNote = 0..20;

  TStudentResults = record
    Algorihms: TNote;
    English: TNote;
    French: TNote;
    Maths: TNote;
    // 
    case Profile: TProfile of
      proProgramming: (FreePascal: TNote; Java: TNote);
      proNetWorks: (NetWorks: TNote);
  end;

L'enregistrement TStudentResults comprend une partie commune aux deux profils définis par le type TProfile. Sa déclaration se termine toutefois par une partie avec variantes : si le profil est proProgramming, il existe deux champs actifs (FreePascal et Java) alors qu'un seul champ est actif si le profil est proNetWorks (NetWorks).

Cette construction appelle plusieurs remarques :

  • le type qui permet de distinguer les variantes doit être un ordinal (c'est-à-dire ni une chaîne, ni un nombre réel, ni un Int64 ou un qword) ;
  • les types qui, comme les chaînes longues et les objets, doivent être initialisés avant d'être employés (que ce soit automatiquement ou non) ne peuvent pas paraître dans la partie variante ;
  • l'identificateur de choix (Profile dans l'exemple) est appelé discriminant ou sélecteur et constitue un champ à part entière ;
  • le discriminant est facultatif : on l'omet lorsqu'il n'est pas utilisé dans le code ;
  • les enregistrements avec variantes peuvent être imbriqués.

La partie avec variantes est toujours celle qui clôt la déclaration d'un enregistrement : la faire apparaître avant les champs communs déclenchera une erreur de compilation.

Voici une portion de code similaire à celui proposé précédemment, mais sans le discriminant :

 
Sélectionnez
type
  TNote = 0..20;

  TStudentResults = record
    Algorihms: TNote;
    English: TNote;
    French: TNote;
    Maths: TNote;
    // 
    case Boolean of
      True: (FreePascal: TNote; Java: TNote);
      False: (NetWorks: TNote);
  end;

Le type TProfile a été supprimé pour utiliser un booléen : suivant les valeurs entrées, le compilateur choisira automatiquement la bonne option parmi les deux proposées.

Accessoirement, les enregistrements avec variantes permettent de transtyper à peu de frais des variables suivant la variante sollicitée. L'exemple de points utilisés pour dessiner des rectangles illustre cette dernière possibilité :

 
Sélectionnez
type
  TPoint = record
    X: LongInt;
    Y: LongInt;
  end;
  TRect = record
   case Integer of
     0: (Left,Top,Right,Bottom: Longint);
     1: (TopLeft,BottomRight: TPoint);
  end;

Dans ces définitions, TopLeft coïncidera avec les valeurs attribuées à Left et Top tandis que BottomRight correspondra à celles de Right et Bottom. Ainsi, si l'on donne une valeur aux champs Left et Top, il sera possible de récupérer directement la valeur de TopLeft.

Aucune vérification n'est faite par le compilateur lors du transtypage qui est par conséquent sous la responsabilité entière du programmeur qui devra s'assurer que les champs à mettre en relation correspondent bien.

II-B. L'accès aux champs

II-B-1. La notation avec le point

À présent que nous savons définir des enregistrements, il est évidemment indispensable de pouvoir accéder aux informations qu'ils renferment. En fait, rien de plus simple : il suffit de faire suivre le nom de la variable de l'enregistrement d'un point et d'y adjoindre le nom du champ désiré.

Par exemple, à partir de l'enregistrement TStudentResults, on obtiendra :

 
Sélectionnez
var
  Student1: TStudentResults;
[…]

Student1.English := 12;
Student1.Maths := 7;
Student1.French := 14;
Students.NetWorks := 18;

Les données n'ont pas à être entrées dans l'ordre de déclaration de l'enregistrement, même si cette contrainte peut éviter d'oublier certaines d'entre elles. Il n'est pas plus indispensable de fournir toutes les données requises par l'enregistrement si elles ne sont pas utilisées, même si, là encore, une mauvaise initialisation d'une variable locale (comme n'importe quelle autre variable locale) peut avoir des conséquences imprévisibles.

Comme vu plus haut, le transtypage est automatique, mais non contrôlé pour les champs avec variantes. On pourra donc écrire :

 
Sélectionnez
type
  TPoint = record
    X: LongInt;
    Y: LongInt;
  end;

  TRect = record
   case Integer of
     0: (Left,Top,Right,Bottom: Longint);
     1: (TopLeft,BottomRight: TPoint);
  end;

var
  MyRect: TRect;
[…]

  MyRect.Top := 15;
  MyRect.Left := 32;
  MyRect.Bottom := 125;
  MyRect.Right := 232;
 
  Memo1.Lines.Add(Format('Largeur : %d', [MyRect.BottomRight.X - MyRect.TopLeft.X]));

On a tout intérêt à bien nommer les champs : un des avantages des enregistrements est qu'ils offrent une lecture facile du code, à condition bien évidemment que l'intitulé de leurs champs soit parlant !

Lorsque l'enregistrement comprend des enregistrements imbriqués, l'accès aux sous-champs s'effectue par des points supplémentaires pour rendre compte de l'arborescence. Ainsi, on obtiendra :

 
Sélectionnez
type
  TUser = record
    FirstName: string;
    SurName: string;
    Address: record
      Number: Integer;
      Street: string;
      ZipCode: Integer;
      Town: string;
    end;
  end;

var
  User1, User2: TUser; 

[…]

User1.Address.Number:= 45
User1.Address.Town:= 'Paris';
User2.FirstName:= 'Blaise';
User2.Address.ZipCode:= 78000;

II-B-2. De l'utilisation de with

Une construction particulière avec le mot réservé with est possible afin d'éviter les saisies répétitives. Au lieu de l'écriture précédente, on peut simplifier la saisie ainsi :

 
Sélectionnez
var
  Student1: TStudentResults;
[…]

with Student1 do
begin
  English := 12;
  Maths := 7;
  French := 14;
  NetWorks := 18;
end;

L'instruction with est capable de faire le départ entre les différentes sources des données. Ainsi, des constructions comme les suivantes sont autorisées :

 
Sélectionnez
type
  TMyRec = record
    Data1: Integer;
    Data2: Integer;
    Data3: Boolean;    
  end;

  TMySecondRec = record
    Zone: Integer;
    Checked: Boolean;
  end;

[…]

var
  MyRec: TMyRecord;
  MySecondRec: TMySecondRec;

[…]

  with MyRec, MySecondRec do
  begin
    Data1 := 45;
    Data2 := Zone;
    Data3 := Checked;
  end;

//ou même...
with MyRec, MySecondRec do
  begin
    Data1 := 45;
    Data2 := Zone;
    Checked := Data3;
  end;

Le dernier exemple montre que les affectations n'ont même pas à être unilatérales. Il faudra cependant faire attention à maintenir un minimum de lisibilité, surtout pour des structures complexes : si le compilateur ne se perd pas dans l'imbroglio des champs et des affectations, il risque de ne pas en être de même pour le programmeur !

En cas d'ambiguïté, le compilateur la signalera :

 
Sélectionnez
type
  TMyRec = record
    Data1: Integer;
    Data2: Integer;
    Data3: Boolean;    
  end;

  TMySecondRec = record
    Data1: Integer;
    Zone: Integer;
    Checked: Boolean;
  end;

[…]

var
  MyRec: TMyRecord;
  MySecondRec: TMySecondRec;

[…]

  with MyRec, MySecondRec do
  begin
    Data1 := 45; / / erreur !
    Data2 := Zone;
    Data3 := Checked;
  end;

Dans ce dernier cas, le compilateur ne peut pas décider de quel Data il s'agit si bien qu'il s'arrête en déclenchant une erreur.

Dans le cas particulier de champs homonymes, il reste toujours à craindre que des erreurs surviennent et qu'elles soient difficiles à détecter. Qu'on pense à des champs baptisés X, Y, Left, Right, Top... Comment reconnaître sans erreur à quoi ils appartiennent au milieu d'un code touffu ?

De nombreux programmeurs proscrivent totalement cette écriture avec with, surtout dans des équipes où plusieurs intervenants sont susceptibles de partager certaines portions de code. Les mêmes font aussi valoir que l'éditeur de Lazarus propose automatiquement les champs possibles après le point, évitant ainsi une bonne partie de la frappe et des potentielles erreurs qui lui sont liées.

II-B-3. Les enregistrements en tant que constantes typées

Un enregistrement peut revêtir la forme d'une constante typée. Pour cela, il suffit de spécifier la valeur constante de tous les champs, dans l'ordre de leurs déclarations :

 
Sélectionnez
type
  TAddress = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
  end;

  TUser = record
    FirstName: string;
    SurName: string;
    Address: TAddress;
  end;

const
  User1: TUser = (FirstName: 'Blaise'; SurName: 'Pascal';
                    Address.Number: 12;
                    Address.Street: 'rue de Port-Royal';
                    Address.ZipCode: 78000);

Le nom de chaque champ est séparé de la valeur affectée par le signe « : ». Chaque affectation est séparée de la suivante par un point-virgule et le tout est placé entre parenthèses.

Si l'enregistrement comporte une partie avec variantes, seule la variante sélectionnée peut contenir des valeurs assignées.

II-B-4. Les problèmes d'affectation

L'affectation ne pose en général pas de problème avec les enregistrements. Il suffit en effet d'affecter une variable d'un type donné à une autre variable du même type :

 
Sélectionnez
type
  TAddress = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
  end;

var
  MyAddress, YourAddress: TAddress;

[…]

  MyAddress.Number:= 12;
  MyAddress.Street:= 'rue de la Paix';
  MyAddress.ZipCode:= 75016;
  MyAddress.Town:= 'Paris';

  YourAdress:= MyAddress;

Dans l'exemple donné, la variable YourAddress voit tous ses champs affectés selon ceux de la variable MyAddress.

Les difficultés commencent si l'on essaye d'affecter des variables d'un type donné avec un type qui ne sera pas exactement similaire. Autrement dit, deux types déclarés séparément, même s'ils contiennent les mêmes champs, ne sont pas compatibles directement pour l'affectation.

 
Sélectionnez
type
  TAddress = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
  end;

  TAddress2 = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
  end;

var
  MyAddress: TAddress;
  YourAddress: TAddress2; 

[…]

  MyAddress.Number:= 12;
  MyAddress.Street:= 'rue de la Paix';
  MyAddress.ZipCode:= 75016;
  MyAddress.Town:= 'Paris';

  YourAddress:= MyAddress; // erreur de compilation !

Pour affecter au premier enregistrement les valeurs contenues dans le second, on procédera alors champ par champ et sans utiliser with, bien évidemment. Une autre possibilité, beaucoup plus compacte, mais sans autre vérification du compilateur que celle de la taille des types en jeu, consistera à transtyper le second enregistrement pour le forcer à correspondre au premier :

 
Sélectionnez
YourAddress:= TAddress(MyAddress);

II-C. Les limites des enregistrements

Par définition, les enregistrements comprennent des champs dont le format est déterminé dès leur déclaration et sont de longueur fixe. Ils ne sont donc pas adaptés au stockage de données de longueurs variables. Généralement, on fera appel dans ce dernier cas à des technologies plus souples comme les fichiers XML ou Json.

Une autre limite est plus difficile à saisir par le débutant : les pointeurs ne sont pas utilisables dans des enregistrements si ces derniers doivent être sauvegardés. En effet, un pointeur renvoie à une adresse en mémoire qui n'est valable que lors de la session d'exécution de l'application. C'est cette adresse qui sera sauvegardée et non les données sur lesquelles il pointe ! Lors d'une exécution ultérieure, récupérer cette adresse depuis un enregistrement sauvegardé n'aurait plus aucun sens et conduirait très probablement à des violations d'accès et au plantage de l'application.

De la même manière, des types comme les chaînes longues ou les objets, qui cachent un dispositif d'allocation et de libération de la mémoire, ne peuvent être utilisés que dans le cadre restreint de l'instance lors de laquelle ils auront été créés. Là encore, la sauvegarde de tels enregistrements stockerait des pointeurs sans signification lors d'une exécution ultérieure.

Dans les exemples choisis jusqu'à présent, vous aurez sans doute remarqué que seules des chaînes longues avaient été employées : c'est la situation de développement la plus courante. Puisqu'elles n'étaient pas stockées sur un support, leur utilisation ne posait pas de problèmes particuliers. Bien plus, le traitement des caractères UTF-8 était correct, ce qui n'aurait pas été le cas avec des chaînes courtes. En revanche, la sauvegarde aurait été problématique, les enregistrements ne stockant que les adresses pointant vers les chaînes de caractères elles-mêmes.

II-D. Deux exemples simples d'utilisation

Afin de rendre plus concrètes les notions abordées jusqu'à présent, il peut être utile d'écrire quelques exemples élémentaires.

II-D-1. Illustration des affectations

[Exemple record01]

Le premier exemple a pour objectif essentiel d'illustrer les affectations.

La structure d'enregistrement utilisée est très simple :

 
Sélectionnez
type 
  TMyRecord = record
    FirstName: string;
    Surname: string;
    Sexe: Boolean;
    Age: Word;
  end;

L'interface utilisateur prendra cette apparence:

Image non disponible

On remarque des zones de saisie (deux TEdit pour le prénom et le nom, une TCheckBox pour le sexe, un TSpinEdit pour l'âge), cinq boutons pour exécuter les actions voulues et un TMemo pour l'affichage des résultats. Le fichier LFM renseigne exactement sur l'interface ainsi créée :

 
CacherSélectionnez
object MainForm: TMainForm
  Left = 347
  Height = 403
  Top = 146
  Width = 606
  Caption = 'Test des enregistrements (record) 01'
  ClientHeight = 403
  ClientWidth = 606
  Position = poScreenCenter
  LCLVersion = '1.6.2.0'
  object mmoMain: TMemo
    Left = 324
    Height = 383
    Top = 10
    Width = 272
    Align = alRight
    BorderSpacing.Around = 10
    ScrollBars = ssAutoBoth
    TabOrder = 0
  end
  object gbData: TGroupBox
    Left = 10
    Height = 383
    Top = 10
    Width = 305
    Align = alLeft
    BorderSpacing.Around = 10
    Caption = 'Données'
    ClientHeight = 363
    ClientWidth = 301
    TabOrder = 1
    object lblFirstName: TLabel
      Left = 13
      Height = 15
      Top = 8
      Width = 48
      BorderSpacing.Around = 10
      Caption = 'Prénom :'
      FocusControl = edtFirstName
      ParentColor = False
    end
    object edtFirstName: TEdit
      Left = 72
      Height = 23
      Top = 0
      Width = 216
      BorderSpacing.Around = 10
      TabOrder = 0
      Text = 'Blaise'
    end
    object lblSurname: TLabel
      Left = 13
      Height = 15
      Top = 40
      Width = 33
      BorderSpacing.Around = 10
      Caption = 'Nom :'
      FocusControl = edtSurname
      ParentColor = False
    end
    object edtSurname: TEdit
      Left = 72
      Height = 23
      Top = 35
      Width = 216
      BorderSpacing.Around = 10
      TabOrder = 1
      Text = 'Pascal'
    end
    object cbSexe: TCheckBox
      Left = 13
      Height = 19
      Top = 72
      Width = 68
      BorderSpacing.Around = 10
      Caption = 'Femme ?'
      TabOrder = 2
    end
    object seAge: TSpinEdit
      Left = 72
      Height = 23
      Top = 101
      Width = 50
      BorderSpacing.Around = 10
      MaxValue = 150
      TabOrder = 3
      Value = 20
    end
    object lblAge: TLabel
      Left = 13
      Height = 15
      Top = 109
      Width = 27
      BorderSpacing.Around = 10
      Caption = 'Age :'
      FocusControl = seAge
      ParentColor = False
    end
    object btnClear: TButton
      Left = 10
      Height = 25
      Top = 150
      Width = 75
      BorderSpacing.Around = 10
      Caption = 'Nettoyer'
      OnClick = btnClearClick
      TabOrder = 4
    end
    object btnNew: TButton
      Left = 104
      Height = 25
      Top = 150
      Width = 75
      Caption = 'Nouveau'
      OnClick = btnNewClick
      TabOrder = 5
    end
    object btnNewWith: TButton
      Left = 200
      Height = 25
      Top = 150
      Width = 75
      Caption = 'Avec With'
      OnClick = btnNewWithClick
      TabOrder = 6
    end
    object btn2With: TButton
      Left = 10
      Height = 25
      Top = 192
      Width = 75
      Caption = 'Avec 2 With'
      OnClick = btn2WithClick
      TabOrder = 7
    end
    object btnDirect: TButton
      Left = 10
      Height = 25
      Top = 232
      Width = 75
      Caption = 'Direct'
      OnClick = btnDirectClick
      TabOrder = 8
    end
  end
end

Le bouton intitulé « Nettoyer » permet de remettre à zéro le contenu du TMemo :

 
Sélectionnez
procedure TMainForm.btnClearClick(Sender: TObject);
begin
  mmoMain.Lines.Clear;
end;

Le bouton intitulé « Nouveau » crée un nouvel enregistrement en remplissant les champs à partir des données entrées par l'utilisateur :

 
Sélectionnez
procedure TMainForm.btnNewClick(Sender: TObject);
begin
  // on récupère les données depuis les contrôles adéquats
  MyRecord.FirstName := edtFirstName.Text;
  MyRecord.Surname := edtSurname.Text;
  MyRecord.Age := seAge.Value;
  MyRecord.Sexe := cbSexe.Checked;
  // on ajoute les données au mémo
  mmoMain.Lines.Add(MyRecord.FirstName);
  mmoMain.Lines.Add(MyRecord.Surname);
  mmoMain.Lines.Add('Age : ' + IntToStr(MyRecord.Age));
  if MyRecord.Sexe then
    mmoMain.Lines.Add('Sexe féminin')
  else
    mmoMain.Lines.Add('Sexe masculin');
  mmoMain.Lines.Add('');
end;

Le bouton intitulé « Avec With » effectue le même travail que le précédent, mais en utilisant le raccourci with. L'écriture en est plus condensée, mais au risque de confusions possibles :

 
Sélectionnez
procedure TMainForm.btnNewWithClick(Sender: TObject);
begin
  with MyRecord do
  begin
    FirstName := edtFirstName.Text;
    Surname := edtSurname.Text;
    Age := seAge.Value;
    Sexe := cbSexe.Checked;
    mmoMain.Lines.Add(FirstName);
    mmoMain.Lines.Add(Surname);
    mmoMain.Lines.Add('Age : ' + IntToStr(Age));
    if Sexe then
      mmoMain.Lines.Add('Sexe féminin')
    else
      mmoMain.Lines.Add('Sexe masculin');
  end;
  mmoMain.Lines.Add('');
end;

Le quatrième bouton est intitulé « Avec 2 With ». Même si le compilateur effectue correctement son travail, la lisibilité décroît encore avec la combinaison de deux with dont l'un se rapporte à l'objet mmoMain de type TMemo afin d'accéder à ses méthodes :

 
Sélectionnez
procedure TMainForm.btn2WithClick(Sender: TObject);
begin
  with  MyRecord, mmoMain.Lines do
  begin
    FirstName := edtFirstName.Text;
    Surname := edtSurname.Text;
    Age := seAge.Value;
    Sexe := cbSexe.Checked;
    Add(FirstName);
    Add(Surname);
    Add('Age : ' + IntToStr(Age));
    if Sexe then
      Add('Sexe féminin')
    else
      Add('Sexe masculin');
    Add('');
  end;
end;

Le dernier bouton, intitulé « Direct », montre qu'il est possible d'affecter directement un enregistrement à un autre s'ils sont du même type. La première partie de la méthode n'utilise pas les with imbriqués, de manière à lever toute ambiguïté. La seconde est plus condensée, mais le code est exposé à des confusions possibles :

 
Sélectionnez
procedure TMainForm.btnDirectClick(Sender: TObject);
var
  MyRecord2: TMyRecord;
begin
  MyRecord.FirstName := edtFirstName.Text;
  MyRecord.Surname := edtSurname.Text;
  MyRecord.Age := seAge.Value;
  MyRecord.Sexe := cbSexe.Checked;
  mmoMain.Lines.Add(MyRecord.FirstName);
  mmoMain.Lines.Add(MyRecord.Surname);
  mmoMain.Lines.Add('Age : ' + IntToStr(MyRecord.Age));
  if MyRecord.Sexe then
    mmoMain.Lines.Add('Sexe féminin')
  else
    mmoMain.Lines.Add('Sexe masculin');
  mmoMain.Lines.Add('');
  
  MyRecord2 := MyRecord;

  with MyRecord2, mmoMain.Lines do
  begin
    Add(FirstName);
    Add(Surname);
    Add('Age : ' + IntToStr(Age));
    if Sexe then
      Add('Sexe féminin')
    else
      Add('Sexe masculin');
    Add('');
  end;
end;

L'exécution de l'application donnera ce type d'affichage :

Image non disponible

II-D-2. Illustration des enregistrements constantes typées et des parties variantes

[Exemple record02]

Le second exemple met en œuvre des enregistrements avec des parties variantes déclarées sous forme de constantes typées.

L'interface utilisateur est très simple puisqu'elle ne comporte que deux TButton et un TMemo. Voici une capture d'écran correspondant au projet :

Image non disponible

Le fichier LFM sera alors celui-ci :

 
CacherSélectionnez
object MainForm: TMainForm
  Left = 442
  Height = 463
  Top = 310
  Width = 623
  Caption = 'Test des enregistrements (record) 02'
  ClientHeight = 463
  ClientWidth = 623
  Position = poScreenCenter
  LCLVersion = '1.6.4.0'
  object mmoMain: TMemo
    Left = 288
    Height = 418
    Top = 16
    Width = 304
    ScrollBars = ssAutoBoth
    TabOrder = 0
  end
  object btnTypedConstant: TButton
    Left = 24
    Height = 25
    Top = 32
    Width = 240
    Caption = 'Constante typée (avec discriminant)'
    OnClick = btnTypedConstantClick
    TabOrder = 1
  end
  object btnTypedConstant2: TButton
    Left = 24
    Height = 25
    Top = 72
    Width = 240
    Caption = 'Constante typée (sans discriminant)'
    OnClick = btnTypedConstant2Click
    TabOrder = 2
  end
end

Le code de l'application est lui aussi simple du point de vue de l'écriture, mais il va poser quelques problèmes à l'occasion de modifications minimes.

Dans un premier temps, il faut déclarer des structures de travail :

 
Sélectionnez
type  
  TProfile = (proProgramming, proNetWorks);
  TNote = 0..20;

  TStudentResults = record
    Algorihms: TNote;
    English: TNote;
    French: TNote;
    Maths: TNote;
    case Profile: TProfile of
      proProgramming: (FreePascal: TNote; Java: TNote);
      proNetWorks: (NetWorks: TNote);
  end;

  TStudentResults2 = record
    Algorihms: TNote;
    English: TNote;
    French: TNote;
    Maths: TNote;
    // 
    case Boolean of
      True: (FreePascal: TNote; Java: TNote);
      False: (NetWorks: TNote);
  end;

Le type enregistrement TStudentResults utilise le discriminant Profile contrairement à TStudentResults2 qui s'en passe. Chacun des boutons procédera au test d'un des deux types déclarés.

Le premier bouton btnTypedConstant utilise le discriminant :

 
Sélectionnez
procedure TMainForm.btnTypedConstantClick(Sender: TObject);
// *** constante typée avec discriminant ***
const
  LStudent: TStudentResults = (Algorihms: 12;
    English: 9;
    French: 8;
    Maths: 11;
    Profile: proProgramming;
    FreePascal: 15;
    Java: 13);
begin
  mmoMain.Lines.Add('');
  case LStudent.Profile of
    proProgramming: mmoMain.Lines.Add('Etudiant (programmation)');
    proNetWorks: mmoMain.Lines.Add('Impossible !');
  end;
  mmoMain.Lines.Add('Algorithmes : ' + IntToStr(LStudent.Algorihms));
  mmoMain.Lines.Add('Anglais : ' + IntToStr(LStudent.English));
  mmoMain.Lines.Add('Français : ' + IntToStr(LStudent.French));
  mmoMain.Lines.Add('Maths : ' + IntToStr(LStudent.Maths));
  mmoMain.Lines.Add('Free Pascal : ' + IntToStr(LStudent.FreePascal));
  mmoMain.Lines.Add('Java : ' + IntToStr(LStudent.Java));
end;

Non seulement les valeurs peuvent être affectées aux champs, mais il est aussi possible de retrouver l'information correspondant au profil de l'étudiant via le discriminant.

Ce n'est pas ce qu'il advient avec la forme sans discriminant gérée par le second bouton btnTypesConstant2 :

 
Sélectionnez
procedure TMainForm.btnTypedConstant2Click(Sender: TObject);
// *** constante typée avec discriminant ***
const
  LStudent: TStudentResults2 = (Algorihms: 12;
    English: 9;
    French: 8;
    Maths: 11;
    FreePascal: 15;
    Java: 13);
begin
  mmoMain.Lines.Add('');
  mmoMain.Lines.Add('Étudiant (programmation ou réseaux ?)');
  mmoMain.Lines.Add('Algorithmes : ' + IntToStr(LStudent.Algorihms));
  mmoMain.Lines.Add('Anglais : ' + IntToStr(LStudent.English));
  mmoMain.Lines.Add('Français : ' + IntToStr(LStudent.French));
  mmoMain.Lines.Add('Maths : ' + IntToStr(LStudent.Maths));
  mmoMain.Lines.Add('Free Pascal : ' + IntToStr(LStudent.FreePascal));
  mmoMain.Lines.Add('Java : ' + IntToStr(LStudent.Java));
end;

On remarque en effet qu'il est impossible dans ce cas de déterminer le type d'étudiant dont il s'agit. Dans ce cas précis, il aurait été préférable de déclarer un discriminant explicite.

Mais le plus important n'est pas là : diverses manipulations de ce code vont provoquer des problèmes dont le programmeur a tout intérêt à tenir compte !

En premier lieu, ajouter une simple ligne à la fin des méthodes précédentes montre que les champs occultés par le code (ici, le champ NetWorks) restent accessibles avec ou sans discriminant :

 
Sélectionnez
// !!! Mauvais aiguillage, mais tâche effectuée
  mmoMain.Lines.Add('Réseaux : ' + IntToStr(LStudent.NetWorks) + ' ???');

Dans tous les cas, le champ NetWorks prend la valeur qui lui correspond dans l'enregistrement une fois l'emplacement mémoire transtypé si nécessaire (ici, ce n'est pas la peine puisque le champ FreePascal est aussi de type Integer). On obtiendra par conséquent cet affichage, sans aucun avertissement du compilateur :

Image non disponible

Il faut retenir que les champs relatifs à des parties variantes et les discriminants qui s'y rapportent restent sous l'entière responsabilité du programmeur.

D'autres comportements peuvent même paraître étranges. Il suffit de modifier ainsi les constantes typées pour s'en rendre compte :

 
Sélectionnez
const
  LStudent: TStudentResults = (Algorihms: 12;
    English: 9;
    French: 8;
    Maths: 11;
    Profile: proProgramming;
    FreePascal: 15;
    Java: 13;
    NetWorks: 4);

Tout se passe alors comme si la valeur du champ NetWorks était ignorée : l'écran d'exécution obtenu est strictement le même que précédemment ! Le pire est qu'il en est de même si l'on change la valeur du discriminant Profile pour le mettre à proNetWorks

La souplesse que procurent les enregistrements, en particulier lors de l'utilisation de constantes typées, est source de pièges potentiels dont il faut tenir compte avec la plus grande prudence.

En revanche, toujours dans le cas des constantes typées, le compilateur n'appréciera pas du tout qu'un champ quelconque soit renseigné avant un autre. Pour le vérifier, il faut par exemple commenter le champ FreePascal ou inverser les champs French et Maths : on obtient alors une erreur lors de la compilation.

III. Le compactage des enregistrements

La compréhension des mécanismes mis en œuvre lors de l'utilisation des enregistrements est importante pour qui veut les utiliser au mieux, en particulier si des programmes doivent échanger des informations ou si différentes plates-formes sont en jeu. Les problèmes essentiels résident dans la taille des enregistrements qui dépend elle-même de l'alignement des champs en leur sein.

III-A. De la taille d'un enregistrement

III-A-1. Taille d'un enregistrement

Par définition, les enregistrements ont une taille fixe. Par conséquent, quelles que soient les valeurs affectées aux champs d'un enregistrement donné, l'utilisateur a l'assurance que chaque enregistrement sera similaire à un autre du point de vue de sa taille. En revanche, rien n'assure qu'un type enregistrement aura la même taille d'une plate-forme à une autre. De même, l'ordre de déclaration des champs a son importance, la taille pouvant être modifiée par le simple déplacement de l'un d'entre eux.

Sans autre précision concernant la structure d'un enregistrement, il ne faut jamais supposer un emplacement particulier en mémoire pour un champ donné. Si le compilateur range toujours les champs dans l'ordre indiqué par le code source, il optimise l'espace occupé.

[Exemple record03]

Le premier problème est par conséquent celui de l'espace occupé en mémoire par un enregistrement. Afin de mettre en évidence cette difficulté, nous allons écrire une petite application qui ne comprendra que trois TButton accompagnés de trois TLabel placés comme suit :

Image non disponible

Grâce à la fonction SizeOf, chaque bouton calculera la taille des enregistrements tels que définis ci-après :

 
Sélectionnez
type

  TSimpleRecord = record 
    ADouble: Double;
    AnInteger: Integer;
    ABoolean: Boolean;
    AnotherBoolean: Boolean;
  end;

  TSimpleRecord2 = record 
    AnotherBoolean: Boolean;
    ADouble: Double;
    AnInteger: Integer;
    ABoolean: Boolean;
  end;

  TPackedRecord = packed record 
    ADouble: Double;
    AnInteger: Integer;
    AnotherBoolean: Boolean;
    ABoolean: Boolean;
  end;

La seule nouveauté est l'emploi pour le dernier type du mot réservé packed qui demande au compilateur de compacter l'enregistrement concerné. Avant d'expliquer son fonctionnement, nous allons voir l'ensemble en action.

Le code est vraiment très simple puisqu'il ne fait que réagir aux clics sur les boutons pour afficher dans l'étiquette appropriée la valeur de la taille de l'enregistrement visé :

 
Sélectionnez
{ TMainForm }

  TMainForm = class(TForm)
    btnSimpleRecord: TButton;
    btnPackedRecord: TButton;
    btnSimpleRecord2: TButton;
    lblSimpleRecord2: TLabel;
    lblSimpleRecord: TLabel;
    lblPackedRecord: TLabel;
    procedure btnPackedRecordClick(Sender: TObject);
    procedure btnSimpleRecord2Click(Sender: TObject);
    procedure btnSimpleRecordClick(Sender: TObject);
  private
    { private declarations }
  public
    { public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.lfm}

{ TMainForm }

procedure TMainForm.btnSimpleRecordClick(Sender: TObject);
begin
  lblSimpleRecord.Caption := IntToStr(sizeof(TSimpleRecord));
end;

procedure TMainForm.btnPackedRecordClick(Sender: TObject);
begin
  lblPackedRecord.Caption := IntToStr(sizeof(TPackedRecord));
end;

procedure TMainForm.btnSimpleRecord2Click(Sender: TObject);
begin
  lblSimpleRecord2.Caption := IntToStr(sizeof(TSimpleRecord2));
end;

end.

Les valeurs attendues pourraient être calculées manuellement. Sachant qu'un booléen occupe un octet, qu'un entier en occupe quatre et qu'un nombre flottant en double précision en occupe huit, une addition montre qu'il faut normalement quatorze octets pour composer ces enregistrements.

L'exécution de ce programme rudimentaire crée cependant d'importantes surprises.

Image non disponible

Seul l'enregistrement compacté occupe la place escomptée. En fait, le mot réservé packed permet l'alignement de tous les champs sur un début d'octet sans créer de vides entre eux : le total de l'espace occupé est par conséquent la somme des longueurs de tous les champs calculées en octets.

Le premier type (TSimpleRecord) occupe deux octets de trop : en commentant tour à tour les autres champs, on s'aperçoit que le compilateur a choisi d'aligner les champs sur huit octets, soit la taille du plus grand des champs. On obtient donc huit octets pour le Double et huit octets pour stocker l'Integer et les deux Boolean, soit seize octets.

De la même manière, la taille démesurée du second type d'enregistrement (TSimpleRecord2) s'explique si l'on commente les autres champs ou si l'on remplace le champ ADouble par un champ de type Integer, par exemple. L'alignement se fait toujours en fonction du plus grand des champs : huit octets pour le Boolean isolé, huit octets pour le Double, huit octets pour l'Integer rangé avec le dernier Boolean, soit vingt-quatre octets.

Il ressort de ces différentes manipulations qu'il est fortement conseillé, en dehors de directives de compactage, de placer les champs les plus courts à la fin de la déclaration de l'enregistrement.

[Exemple record03bis]

Il est possible de remplacer packed par bitpacked: comme son nom l'indique, bitpacked va demander l'alignement des champs non plus sur le prochain octet libre, mais sur le prochain élément binaire libre (bit).

En modifiant ainsi la déclaration de l'enregistrement, on obtiendra une taille de treize octets dans l'application exemple :

 
Sélectionnez
TPackedRecord = bitpacked record // unique modification 
    ADouble: Double;
    AnInteger: Integer;
    AnotherBoolean: Boolean;
    ABoolean: Boolean;
  end;

Après ajout d'un TButton et d'un TLabel pour le nouveau type, ainsi que la méthode OnClick associée, le compilateur parvient à grignoter un peu de place de telle façon qu'un octet ne soit plus nécessaire à l'enregistrement :

Image non disponible

Une dernière information peut être utile : celle de la place occupée par un enregistrement comprenant une partie avec variantes. Cette dernière commence par le champ sélecteur s'il a été renseigné par un identificateur, suivi de la place requise par la plus grande des variantes.

III-A-2. Vitesse d'exécution et compactage

Après ce qui vient d'être vu, on pourrait en conclure que les enregistrements ont toujours intérêt à être compactés, mais ce serait aller trop vite en besogne. En effet, comme souvent en informatique, un compromis existe entre l'espace mémoire occupé et la vitesse d'exécution : les processeurs sont plus rapides lorsqu'ils travaillent à des frontières natives. Ces dernières peuvent être des octets ou des groupements d'octets, généralement sur 32 ou 64 bits de nos jours. Les forcer à manipuler des données hors de ces frontières est possible, mais nécessite plus de travail et par conséquent plus de temps.

Comme les temps mesurés seraient trop imprécis en utilisant des instruments aussi grossiers que les timers, il paraît nécessaire de vérifier les temps d'accès aux enregistrements grâce à des outils plus perfectionnés comme des outils de profilage. On a utilisé ProLaza pour les résultats qui suivent, un outil commercial qui a montré depuis longtemps son efficacité.

[Exemple record04]

L'application testée affecte 100 000 fois les mêmes données aux champs d'enregistrements de types différents. On demande à ProLaza de déterminer le temps utilisé par chacune des procédures. Voici le moule de chacune de ces procédures, l'unique modification à apporter étant de changer le type d'enregistrement utilisé :

 
Sélectionnez
procedure TMainForm.ChangeSimpleRecord;
var
  Li: Integer;
  LRec: TSimpleRecord;
begin
  for Li := 1 to 100000 do
  begin
    LRec.ABoolean := True;
    LRec.ADouble := 12589748.012458;
    LRec.AnInteger := 987654321;
    LRec.AnotherBoolean := False;
  end;
end;

Les résultats sont surtout probants pour bitpacked qui ralentit sérieusement l'exécution du code alors que packed ne se fait pas sentir, au moins pour ce type d'enregistrement :

Image non disponible

La quatrième colonne indique le pourcentage d'occupation de la routine alors que la sixième exprime son temps d'exécution réel. Les résultats ont été obtenus en compilant le programme en 32 bits avec Lazarus 1.6.4 (donc sous Free Pascal 3.0.2) sur une machine Windows 10 Pro équipée d'un processeur i3 4130 3,4 Ghz avec 16 Go de mémoire vive.

III-B. De l'utilité du compactage

Le compactage des enregistrements apporte des bénéfices non négligeables :

  • il économise l'espace occupé par les enregistrements, surtout pour de grandes séries de données ;
  • il permet de communiquer avec d'autres langages de programmation qui n'auront pas les mêmes stratégies d'optimisation de la structure d'un enregistrement ;
  • il est indispensable à la lecture et à l'écriture de fichiers binaires qui n'accepteront pas d'être réorganisés ;
  • il autorise les transtypages en alignant correctement les champs pour qu'ils coïncident, mais sans vérifier la pertinence de ces transtypages.

En dehors d'éventuels problèmes de performances, le compactage n'a malheureusement pas toutes les vertus nécessaires pour une utilisation sans souci : les règles définies ci-après limitent parfois sérieusement les domaines de leur emploi !

III-C. Les règles du compactage

III-C-1. Compacter avec bitpacked

Bitpacked n'aligne les champs sur les éléments binaires (bits) que dans la mesure où ils relèvent d'un type ordinal. La fonction BitSizeOf renvoie alors la taille en bits de l'enregistrement ou du champ passé en paramètre.

Voici le résultat obtenu avec l'exemple 03bis :

Image non disponible

Dans tous les cas où le type des champs n'est pas un ordinal, l'alignement se fait sur un début d'octet. Il est aussi à noter que la taille d'un enregistrement est limitée par le système : 229 octets (soit 512 Mo) pour un système en 32 bits et 261 octets pour le 64 bits.

De plus, il est impossible avec ce mode de compactage de retrouver l'adresse d'un champ si sa longueur n'est pas un multiple de 8 et s'il n'est pas aligné sur une frontière d'octet, ce qui interdit de l'employer comme paramètre variable d'une procédure ou d'une fonction.

Enfin, non seulement l'implémentation dépend des plates-formes (en particulier à cause de l'endiannes des systèmes), mais elle est même susceptible d'être modifiée dans le futur : le programmeur doit considérer l'enregistrement créé comme une boîte noire.

III-C-2. Compacter avec packed

Packed n'a pas la même signification suivant le contexte. Si la directive $BITPACKING est active, packed est l'équivalent de bitpacked tel qu'étudié précédemment. Dans le cas contraire, l'alignement d'un champ se fera toujours sur la frontière du premier octet libre dans l'enregistrement.

Pour les utilisateurs d'Apple, packed est toujours l'équivalent de bitpacked dans le mode MACPAS.

Le format du compactage peut être contrôlé grâce à la directive $PACKRECORDS dont la syntaxe générale est :

 
Sélectionnez
{$PACKRECORDS n}

Les valeurs prises par n peuvent être : 0, 1, 2, 4, 8, 16, 32, C ou DEFAULT.

Si la taille d'un champ est inférieure au nombre n, son alignement dans l'enregistrement se fera sur la puissance de 2 plus grande ou égale à cette taille. Dans le cas contraire, l'alignement se fera sur des débuts d'octets tous les n octets.

La valeur C pour le paramètre n correspond à l'alignement utilisé par le compilateur GNU C : elle est à utiliser dans des unités d'importation lors de l'utilisation de routines en C.

Les valeurs DEFAULT et 0 correspondent à l'alignement tel qu'il est pratiqué par le compilateur, hors de toute directive spéciale. Il est par conséquent dépendant de la plate-forme utilisée.

Pour un type enregistrement donné, l'unique façon d'être certain de pouvoir utiliser les données quelle que soit la plate-forme en jeu est d'utiliser la directive {$PACKRECORDS 1}.

L'alignement peut encore être spécifié par les directives spéciales suivantes :

 
Sélectionnez
{$A1} // équivaut à {$PACKRECORDS 1 }
{$A2} // équivaut à {$PACKRECORDS 2 }
{$A4} // équivaut à {$PACKRECORDS 4 }
{$A8} // équivaut à {$PACKRECORDS 8 }

Elles constituent de simples raccourcis, mais peuvent paraître moins explicites que la forme développée.

Il est aussi possible d'utiliser les directives $A ou $ALIGN qui sont des équivalents dès lors qu'un nombre les accompagne, mais qui ont d'autres significations pour les systèmes Apple et qui n'acceptent pas les autres valeurs de $PACKRECORDS.

Enfin, la directive $CODEALIGN comprend des paramètres qui autorisent le contrôle de l'alignement de champs suivant leur taille :

  • RECORDMIN indique la taille minimale d'alignement ;
  • RECORDMAX indique la taille maximale d'alignement.

Leur utilisation prendra, par exemple, la forme :

 
Sélectionnez
 {$CODEALIGN RECORDMIN=2}
 {$CODEALIGN RECORDMAX=4}

Ainsi, avec RECORDMIN, on pouvait fixer dans l'exemple 03bis une limite inférieure à l'alignement des champs :

 
Sélectionnez
{$CODEALIGN RECORDMIN=12}
  TSimpleRecord2 = record // 24
    AnotherBoolean: Boolean;
    ADouble: Double;
    AnInteger: Integer;
    ABoolean: Boolean;
  end;

À partir de ce moment, la taille affichée pour l'enregistrement aurait été de 40 et non de 24 !

En cas d'emploi successif de ces différentes directives, c'est la dernière rencontrée qui l'emporte.

IV. Conclusion

À l'aide de ce tutoriel, vous devriez être capable d'utiliser au mieux les enregistrements dans vos programmes. Vous aurez appris à :

  • définir un type enregistrement simple avec ou sans variantes ;
  • affecter des valeurs à des champs et les retrouver à la demande ;
  • tenir compte des limites des enregistrements ;
  • comprendre les mécanismes régissant le compactage afin de l'utiliser à bon escient.

Cet ensemble de connaissances va vous permettre à présent d'aborder les enregistrements étendus introduits par la Programmation Orientée Objet : ce sont des outils puissants, pour la plupart rencontrés à l'occasion de la manipulation d'objets, qui s'offrent ainsi aux programmeurs.

Merci à Alcatîz pour sa relecture technique et à Claude Leloup pour la correction orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2017 Gilles Vasseur. 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.