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

Enregistrements étendus

Après avoir défini et utilisé les enregistrements simples, avec ou sans variantes, puis étudié les modes de compactage mis en œuvre essentiellement dans un but d'échanger des données avec d'autres applications, il est temps de s'intéresser aux enregistrements étendus dont ce tutoriel se propose de montrer l'efficacité et la puissance.

3 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.

Les enregistrements étendus accroissent les possibilités offertes par les enregistrements simples en autorisant l'inclusion de méthodes et de propriétés dans la structure elle-même. Ils se situent en quelque sorte entre les enregistrements tels que le Pascal les a introduits dès sa création et les classes apparues avec le Pascal Objet. Il leur est ainsi permis de s'intégrer au mieux dans un environnement de Programmation Orientée Objet.

II. Travailler avec des enregistrements étendus

Comme tous les éléments et structures en Pascal, il est nécessaire de déclarer les enregistrements étendus. Leur déclaration implique ensuite une implémentation à la manière des méthodes des classes traditionnelles.

II-A. Déclarer un enregistrement étendu

Les enregistrements étendus ne sont actifs par défaut qu'en mode Delphi. En mode Object Pascal, il faut indiquer explicitement qu'ils vont être utilisés.

L'activation des fonctionnalités relatives aux enregistrements étendus se fait par conséquent de deux manières :

 
Sélectionnez
// première possibilité

{$mode delphi}

// seconde possibilité

{$mode objfpc}
{$modeswitch advancedrecords}

La déclaration d'un enregistrement étendu ressemble à celle d'une classe, à ceci près que la première est limitée du point de vue des options possibles. En effet, seuls sont autorisés :

  • les constructeurs ;
  • les visibilités strict private, private et public ;
  • les méthodes statiques, y compris les méthodes statiques dites de classe ;
  • les propriétés non publiées.

Comme pour les classes, la visibilité est par défaut public .

Voici une définition d'enregistrement étendu telle qu'elle pourrait figurer dans une application :

 
Sélectionnez
type

 TAddress = record
  private
    fNumber: Integer;
    fStreet: string;
    fZipCode: Integer;
    fTown: string;
    procedure SetTown(AValue: string);
    procedure SetZipCode(AValue: Integer);
  public
    class function MaxZipCode: Integer; static;
    function IsValidZipCode(N: Integer): Boolean;
    property Number: Integer read fNumber write fNumber;
    property Street: string read fStreet write fStreet;
    property ZipCode: Integer read fZipCode write SetZipCode;
    property Town: string read fTown write SetTown;
  end;

Ce type d'enregistrement comprend les différentes options de la Programmation Orientée Objet applicables aux classes et qu'acceptent les enregistrements étendus.

Pour rappel, les méthodes de classe statiques se comportent comme des variables globales qui auraient été rattachées à une structure afin de mieux contrôler leur domaine d'utilisation. Ces méthodes particulières peuvent être appelées depuis la structure elle-même, sans instanciation.

II-B. Implémenter les méthodes d'un enregistrement étendu

L'implémentation des méthodes définies par un type enregistrement étendu ne diffère en rien de celle des méthodes d'une classe. Elle se fait après la clause implementation de l'unité où est déclaré le type.

Voici à quoi pourrait ressembler l'implémentation du type enregistrement précédent :

 
Sélectionnez
{ TAddress }

function TAddress.IsValidZipCode(N: Integer): Boolean;
// *** code postal correct si entre 10 000 et 100 000 ***
begin
  Result := (N <= MaxZipCode) and (N > 9999);
end;

procedure TAddress.SetTown(AValue: string);
// *** nom de la ville ***
begin
  if fTown = AValue then
    Exit;
  // le nom de la ville est toujours en majuscules
  fTown := UpperCase(AValue);
end;

procedure TAddress.SetZipCode(AValue: Integer);
// *** code postal ***
begin
  if (fZipCode = AValue) or (not IsValidZipCode(AValue)) then
    Exit;
  fZipCode := AValue;
end;

class function TAddress.MaxZipCode: Integer;
// *** méthode de classe renvoyant la valeur maximale du code postal ***
begin
  Result := 99999;
end;

On remarquera aussitôt que les différents traitements relatifs aux champs de l'enregistrement sont encapsulés dans la structure si bien que le code est plus lisible et surtout beaucoup plus facile à mettre à jour. De plus, des opérations telles que la mise en majuscules du nom de la ville ne sont plus susceptibles d'être oubliées dans telle ou telle portion du code. En fait, les mêmes arguments qui plaidaient en faveur des classes sont valables pour les enregistrements étendus dès lors que l'héritage et le polymorphisme ne sont pas requis.

[Exemple extendedrecord01]

Afin de tester l'utilisation du nouveau type défini, une petite application permettra de saisir les données nécessaires, de les afficher et de vérifier que la méthode de classe statique se comporte bien comme une variable globale rattachée à une structure.

La structure d'enregistrement utilisée est celle vue ci-avant : elle comprend donc la déclaration du type TAddress et l'implémentation des méthodes qui l'accompagnent.

Voici l'interface graphique correspondant au projet :

Image non disponible

Le fichier LFM l'accompagnant contient les composants à déposer sur la fiche principale et les valeurs des propriétés à modifier :

 
Sélectionnez
object MainForm: TMainForm
  Left = 256
  Height = 240
  Top = 130
  Width = 657
  ActiveControl = seNumber
  Caption = 'Test des enregistrements étendus 01'
  ClientHeight = 240
  ClientWidth = 657
  Position = poScreenCenter
  LCLVersion = '1.6.4.0'
  object seNumber: TSpinEdit
    Left = 104
    Height = 23
    Top = 8
    Width = 82
    MaxValue = 0
    TabOrder = 0
    Value = 99
  end
  object lblNumber: TLabel
    Left = 16
    Height = 15
    Top = 16
    Width = 50
    Caption = 'Numéro :'
    FocusControl = seNumber
    ParentColor = False
  end
  object lbledtStreet: TLabeledEdit
    Left = 16
    Height = 23
    Top = 64
    Width = 288
    EditLabel.AnchorSideLeft.Control = lbledtStreet
    EditLabel.AnchorSideRight.Control = lbledtStreet
    EditLabel.AnchorSideRight.Side = asrBottom
    EditLabel.AnchorSideBottom.Control = lbledtStreet
    EditLabel.Left = 16
    EditLabel.Height = 15
    EditLabel.Top = 46
    EditLabel.Width = 288
    EditLabel.Caption = 'Nom de la rue :'
    EditLabel.ParentColor = False
    TabOrder = 1
    Text = 'place Blaise Pascal'
  end
  object lbledtTown: TLabeledEdit
    Left = 16
    Height = 23
    Top = 112
    Width = 288
    EditLabel.AnchorSideLeft.Control = lbledtTown
    EditLabel.AnchorSideRight.Control = lbledtTown
    EditLabel.AnchorSideRight.Side = asrBottom
    EditLabel.AnchorSideBottom.Control = lbledtTown
    EditLabel.Left = 16
    EditLabel.Height = 15
    EditLabel.Top = 94
    EditLabel.Width = 288
    EditLabel.Caption = 'Ville :'
    EditLabel.ParentColor = False
    TabOrder = 2
    Text = 'Lyon'
  end
  object lbledtZipCode: TLabeledEdit
    Left = 16
    Height = 23
    Top = 168
    Width = 288
    EditLabel.AnchorSideLeft.Control = lbledtZipCode
    EditLabel.AnchorSideRight.Control = lbledtZipCode
    EditLabel.AnchorSideRight.Side = asrBottom
    EditLabel.AnchorSideBottom.Control = lbledtZipCode
    EditLabel.Left = 16
    EditLabel.Height = 15
    EditLabel.Top = 150
    EditLabel.Width = 288
    EditLabel.Caption = 'Code postal :'
    EditLabel.ParentColor = False
    TabOrder = 3
    Text = '69001'
  end
  object mmoMain: TMemo
    Left = 368
    Height = 218
    Top = 8
    Width = 280
    ScrollBars = ssAutoBoth
    TabOrder = 4
  end
  object btnGo: TButton
    Left = 16
    Height = 25
    Top = 207
    Width = 75
    Caption = 'Enregistrer'
    OnClick = btnGoClick
    TabOrder = 5
  end
  object btnDisplay: TButton
    Left = 104
    Height = 25
    Top = 207
    Width = 75
    Caption = 'Afficher'
    OnClick = btnDisplayClick
    TabOrder = 6
  end
  object btnMaxZipCode: TButton
    Left = 192
    Height = 25
    Top = 207
    Width = 163
    Caption = 'Code postal maximal'
    OnClick = btnMaxZipCodeClick
    TabOrder = 7
  end
end

Le code correspondant réagit aux clics sur les trois boutons, en fonction des données entrées dans les zones de saisie :

 
Sélectionnez
procedure TMainForm.btnGoClick(Sender: TObject);
// *** remplissage de l'enregistrement ***
begin
  Address.Number := seNumber.Value;
  Address.Street := lbledtStreet.Text;
  Address.Town := lbledtTown.Text;
  Address.ZipCode := StrToInt(lbledtZipCode.Text);
end;

procedure TMainForm.btnMaxZipCodeClick(Sender: TObject);
// *** test de la méthode de classe statique ***
begin
  mmoMain.Lines.Add(IntToStr(TAddress.MaxZipCode));
end;

procedure TMainForm.btnDisplayClick(Sender: TObject);
// *** affichage de l'enregistrement ***
begin
  with mmoMain.Lines do
  begin
    Add('');
    Add(IntToStr(Address.Number)+ ', ' + Address.Street);
    Add(IntToStr(Address.ZipCode) + ' ' + Address.Town);
  end;
end;

Comme annoncé, la méthode de classe statique MaxZipCode peut être appelée directement depuis sa structure d'accueil, à savoir TAddress.

Les programmeurs familiers avec les classes ne trouveront rien de surprenant dans cette implémentation puisque, en dehors des restrictions mentionnées, les enregistrements étendus ont des comportements similaires à ceux des classes.

II-C. Utilité des enregistrements étendus

S'ils sont à première vue plus lourds que les enregistrements simples, les enregistrements étendus apportent un certain nombre de bénéfices dont il faut avoir conscience :

  • l'encapsulation des données : dans l'esprit de la Programmation Orientée Objet, elle garantit un code plus sûr et par conséquent une meilleure maintenance ;
  • la souplesse et la puissance des propriétés : plutôt que de prévoir des routines extérieures à la structure, avec le risque d'oublier de les utiliser dans certaines portions de code difficiles à identifier, l'utilisation de propriétés garantit que le traitement désiré se fera en interne, dans l'enregistrement considéré comme une boîte noire ;
  • une meilleure intégration dans l'EDI Lazarus : la complétion de code est possible avec les enregistrements étendus, ce qui permet de retrouver rapidement les procédures et fonctions applicables à un type d'enregistrement donné ;
  • un substitut efficace et plus léger des classes : il n'est parfois pas nécessaire de mettre en œuvre des classes, avec leur mécanisme d'allocation manuelle de mémoire, alors qu'un enregistrement étendu ferait aussi bien l'affaire ;
  • une meilleure compatibilité avec Delphi : ce dernier autorise lui aussi les enregistrements étendus.

II-D. Différences entre classes et enregistrements étendus

Si les enregistrements étendus ressemblent aux classes, ils s'en distinguent toutefois par des points essentiels :

  • alloués sur la pile (ou dans le segment de données s'il s'agit de variables globales) et non sur le tas, ils ne sont pas des pointeurs sur une structure en mémoire, mais cette structure elle-même, si bien qu'ils ne connaissent pas les destructeurs ;
  • ils ne connaissent pas la notion d'héritage et il est par conséquent interdit de dériver un enregistrement d'un autre en en récupérant les propriétés et les méthodes ;
  • dans le même esprit, les notions de virtualité et d'abstraction ne sont pas admises, les directives virtual et abstract n'étant par conséquent pas autorisées ;
  • il en est de même des niveaux de visibilité strict protected, protected et published, tous inutiles à cause des restrictions précédemment énoncées ;
  • le polymorphisme leur est étranger, un type enregistrement n'étant en aucun cas l'équivalent d'un autre ;
  • les méthodes de classe ne sont autorisées que dans la mesure où elles sont explicitement marquées comme static.

Les constructeurs voient leur rôle extrêmement réduit par rapport à ceux des classes puisqu'ils n'allouent pas de mémoire, mais se contentent si besoin d'initialiser des données. Le point d'accès obligatoire qu'ils constituent pour l'instanciation d'une classe n'a plus de sens pour les enregistrements si bien qu'il est possible de les ignorer. La conséquence de ce fait est que le programmeur ne doit jamais considérer qu'un constructeur d'enregistrement sera forcément appelé.

Pour résumer, les enregistrements étendus retiennent essentiellement l'encapsulation de la Programmation Orientée Objet. Il s'agit avant tout de regrouper dans une même structure des données sous forme de champs et les méthodes relatives de près ou de loin à ces champs.

II-E. Enregistrements simples et enregistrements étendus

Les règles et fonctionnalités vues lors de l'étude des enregistrements simples sont toutes applicables aux enregistrements étendus. Cela signifie en particulier qu'il est possible de définir une partie avec variantes, pourvu qu'elle close la définition du type, tout comme il est envisageable d'utiliser le compactage pour une meilleure maîtrise de l'alignement interne des champs.

S'il n'existe pas de restrictions particulières à l'emploi des enregistrements étendus, il faut tenir compte de deux éléments importants :

  • une éventuelle partie avec variantes ne peut pas contenir de méthodes ordinaires ou statiques de classe ;
  • les méthodes et les propriétés n'augmentent pas la taille d'un enregistrement, car les informations les concernant sont stockées en dehors des instances de la structure.

[Exemple extendedrecord02]

Pour illustrer ces remarques, l'application exemple reprend l'interface graphique de la précédente.

Voici une déclaration possible d'un type enregistrement étendu, compacté et comprenant une partie avec variantes :

 
Sélectionnez
type

 TAddress = packed record
  private
    fNumber: Integer;
    fStreet: string;
    fZipCode: Integer;
    fTown: string;
    procedure SetTown(AValue: string);
    procedure SetZipCode(AValue: Integer);
  public
    class function MaxZipCode: Integer; static;
    function IsValidZipCode(N: Integer): Boolean;
    property Number: Integer read fNumber write fNumber;
    property Street: string read fStreet write fStreet;
    property ZipCode: Integer read fZipCode write SetZipCode;
    property Town: string read fTown write SetTown;
    case C: Integer of
    0: (Country: string[30]; Email: string[30]);
    1: (Phone: string[30]);
  end;

Comme pour les enregistrements simples, les variantes ne peuvent pas inclure des champs qui doivent être initialisés avant utilisation : voilà pourquoi les chaînes proposées sont des chaînes courtes.

En modifiant très légèrement les méthodes relatives aux clics, on intègre facilement ces changements :

 
Sélectionnez
procedure TMainForm.btnGoClick(Sender: TObject);
// *** remplissage de l'enregistrement ***
begin
  Address.Number := seNumber.Value;
  Address.Street := lbledtStreet.Text;
  Address.Town := lbledtTown.Text;
  Address.ZipCode := StrToInt(lbledtZipCode.Text);
  Address.C := 1;
  Address.Phone := '0334.05.58.96';
end;

procedure TMainForm.btnDisplayClick(Sender: TObject);
// *** affichage de l'enregistrement ***
begin
  with mmoMain.Lines do
  begin
    Add('');
    Add(IntToStr(Address.Number)+ ', ' + Address.Street);
    Add(IntToStr(Address.ZipCode) + ' ' + Address.Town);
    Add('Téléphone : ' + Address.Phone);
  end;
end;

L'exécution du programme donnera alors lieu à des affichages comme celui-ci :

Image non disponible

[Exemple extendedrecord03]

De même, on peut vérifier la taille de différentes structures suivant la nature de l'enregistrement et son compactage. Pour cela, on créera une nouvelle application qui ne comprendra qu'un bouton et un TMemo.

Il s'agit simplement d'afficher la taille des quatre structures suivantes :

 
Sélectionnez
type

TAddressExtPacked = packed record
  private
    fNumber: Integer;
    fStreet: string;
    fZipCode: Integer;
    fTown: string;
    procedure SetTown(AValue: string);
    procedure SetZipCode(AValue: Integer);
  public
    class function MaxZipCode: Integer; static;
    function IsValidZipCode(N: Integer): Boolean;
    property Number: Integer read fNumber write fNumber;
    property Street: string read fStreet write fStreet;
    property ZipCode: Integer read fZipCode write SetZipCode;
    property Town: string read fTown write SetTown;
    case C: Integer of
    0: (Country: string[30]; Email: string[30]);
    1: (Phone: string[30]);
  end;
  
  TAddressExt = record
  private
    fNumber: Integer;
    fStreet: string;
    fZipCode: Integer;
    fTown: string;
    procedure SetTown(AValue: string);
    procedure SetZipCode(AValue: Integer);
  public
    class function MaxZipCode: Integer; static;
    function IsValidZipCode(N: Integer): Boolean;
    property Number: Integer read fNumber write fNumber;
    property Street: string read fStreet write fStreet;
    property ZipCode: Integer read fZipCode write SetZipCode;
    property Town: string read fTown write SetTown;
    case C: Integer of
    0: (Country: string[30]; Email: string[30]);
    1: (Phone: string[30]);
  end;

  TAddressPacked = packed record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
    case C: Integer of
    0: (Country: string[30]; Email: string[30]);
    1: (Phone: string[30]);
  end;

  TAddress = record
    Number: Integer;
    Street: string;
    ZipCode: Integer;
    Town: string;
    case C: Integer of
    0: (Country: string[30]; Email: string[30]);
    1: (Phone: string[30]);
  end;

Le programme lui-même est réduit au gestionnaire de clics sur l'unique bouton :

 
Sélectionnez
procedure TMainForm.btnGoClick(Sender: TObject);
begin
  with mmoMain.Lines do
  begin
    Add('Simple : ' + IntToStr(SizeOf(TAddress)));
    Add('Simple compacté : ' + IntToStr(SizeOf(TAddressPacked)));
    Add('Etendu : ' + IntToStr(SizeOf(TAddressExt)));
    Add('Etendu compacté : ' + IntToStr(SizeOf(TAddressExtPacked)));
  end;
end;

Les résultats obtenus sont les suivants :

Image non disponible

L'essentiel est de retenir que le caractère étendu n'a aucun effet sur la taille d'un enregistrement et que le compactage peut s'appliquer dans tous les cas.

III. Mettre en œuvre les enregistrements étendus

L'encapsulation est certes au centre des fonctionnalités apportées par les enregistrements étendus, mais leur puissance ne s'arrête pas à elle : la suite de ce tutoriel va étudier deux applications un peu plus subtiles et très utiles dans certaines circonstances, à savoir ici la définition de nouveaux opérateurs et l'utilisation d'énumérateurs.

III-A. Définition de nouveaux opérateurs

On demande de résoudre une équation du troisième degré en sachant qu'il faut utiliser des nombres complexes comme intermédiaires pour y parvenir.

Pour rappel, les nombres complexes comprennent deux parties : une partie réelle qui correspond aux nombres réels tels que chacun les utilise et une partie dite imaginaire qui contient un nombre réel à multiplier par l'imaginaire i dont le carré vaut -1.

Comme il ne s'agit pas ici d'un cours de mathématique, le lecteur peut ignorer ces considérations ou se reporter à un cours sur les nombres complexes. Le novice se rendra compte que ces nombres imaginaires ont des applications pratiques très concrètes, en particulier en électricité et en électronique.

Du point de vue du programmeur, le problème réside dans le fait qu'il n'existe pas d'outils natifs en Pascal pour traiter ces nombres particuliers.

En revanche, rien n'empêche de créer un nouveau type enregistrement définissant un nombre complexe :

 
Sélectionnez
type

  TComplex = record
   r: Real;
   i: Real;
  end;

Tout nombre complexe est ainsi susceptible d'être représenté, au moins dans les limites de précision du type Real.

On peut rattacher à cette déclaration des fonctions utiles qui renverront les résultats des opérations les plus courantes concernant les nombres complexes : l'addition, la soustraction, la multiplication, mais aussi la comparaison à travers l'égalité par exemple.

 
Sélectionnez
type

  TComplex = record
   r: Real;
   i: Real;
  end;

  function AddComplex(c1, c2: TComplex): TComplex;
  function SubComplex(c1, c2: TComplex): TComplex;
  function MulComplex(c1, c2: TComplex): TComplex;
  function AreEqualComplex(c1, c2: TComplex): Boolean;

Mieux encore, fort des nouvelles notions introduites avec les enregistrements étendus, on peut souhaiter encapsuler ces fonctions dans l'enregistrement lui-même :

 
Sélectionnez
type

  TComplex = record
   r: Real;
   i: Real;
   function Add(c1, c2: TComplex): TComplex;
   function Sub(c1, c2: TComplex): TComplex;
   function Mul(c1, c2: TComplex): TComplex;
   function AreEqual(c1, c2: TComplex): Boolean;
  end;

Il resterait à implémenter ces méthodes, mais leur manque d'élégance transparaît immédiatement : pourquoi ne serait-il pas possible d'utiliser les signes mathématiques habituels en informatique ? C'est là qu'intervient la technique particulière de la surcharge d'opérateurs, une possibilité qu'offrent en particulier les enregistrements étendus.

Avec elle, la déclaration du type enregistrement change :

 
Sélectionnez
type

  TComplex = record
   r: Real;
   i: Real;
   class operator +(c1, c2: TComplex): TComplex;
   class operator -(c1, c2: TComplex): TComplex;
   class operator *(c1, c2: TComplex): TComplex;
   class operator =(c1, c2: TComplex): Boolean;
  end;

Les opérateurs peuvent être redéfinis en dehors d'un type enregistrement étendu. Il s'agit par conséquent d'une technique très large. Cependant, l'encapsulation améliore encore une fois la lisibilité et la sécurité du code produit.

L'implémentation d'une telle structure ne pose que des problèmes d'ordre mathématique :

 
Sélectionnez
{ TComplex }

class operator TComplex.+(c1, c2: TComplex): TComplex;
begin
  Result.i := c1.i + c2.i;
  Result.r := c1.r + c2.r;
end;

class operator TComplex.-(c1, c2: TComplex): TComplex;
begin
  Result.i := c1.i - c2.i;
  Result.r := c1.r - c2.r;
end;

class operator TComplex.*(c1, c2: TComplex): TComplex;
begin
  Result.r := (c1.r * c2.r) - (c1.i * c2.i);
  Result.i := (c1.r * c2.i) + (c1.i * c2.r);
end;

class operator TComplex.=(c1, c2: TComplex): Boolean;
begin
  Result := (c1.r = c2.r) and (c1.i = c2.i);
end;

Dorénavant, il sera possible d'écrire des opérations telles que les suivantes :

 
Sélectionnez
Var
  c1, c2, cr: TComplex;

[…]

cr:= c1 + c2;
cr:= c1 - c2;
cr:= c1 * c2;
if c1 = c2 then
 [...]

Voilà qui paraît bien plus lisible, non ?

[Exemple extendedrecord04]

À partir des déclarations et définitions précédentes, l'application exemple est des plus simples.

En voici tout d'abord l'interface graphique :

Image non disponible

Traduite pour le fichier LFM, elle donne :

 
Sélectionnez
object MainForm: TMainForm
  Left = 256
  Height = 333
  Top = 130
  Width = 629
  Caption = 'test des enregistrements étendus 04'
  ClientHeight = 333
  ClientWidth = 629
  Position = poScreenCenter
  LCLVersion = '1.6.4.0'
  object mmoMain: TMemo
    Left = 288
    Height = 288
    Top = 32
    Width = 328
    ScrollBars = ssAutoBoth
    TabOrder = 0
  end
  object btnAdd: TButton
    Left = 16
    Height = 25
    Top = 256
    Width = 75
    Caption = 'Ajouter'
    OnClick = btnAddClick
    TabOrder = 1
  end
  object btnSub: TButton
    Left = 16
    Height = 25
    Top = 285
    Width = 75
    Caption = 'Soustraire'
    OnClick = btnSubClick
    TabOrder = 2
  end
  object btnMul: TButton
    Left = 104
    Height = 25
    Top = 256
    Width = 75
    Caption = 'Multiplier'
    OnClick = btnMulClick
    TabOrder = 3
  end
  object btnEqualP: TButton
    Left = 104
    Height = 25
    Top = 288
    Width = 75
    Caption = 'Egaux ?'
    OnClick = btnEqualPClick
    TabOrder = 4
  end
  object gbNumber1: TGroupBox
    Left = 16
    Height = 105
    Top = 16
    Width = 245
    Caption = 'Nombre 1'
    ClientHeight = 85
    ClientWidth = 241
    TabOrder = 5
    object lblReal1: TLabel
      Left = 8
      Height = 15
      Top = 16
      Width = 67
      Caption = 'Partie réelle :'
      FocusControl = fseReal1
      ParentColor = False
    end
    object lblIm1: TLabel
      Left = 8
      Height = 15
      Top = 48
      Width = 95
      Caption = 'Partie imaginaire :'
      FocusControl = fseIm1
      ParentColor = False
    end
    object fseReal1: TFloatSpinEdit
      Left = 110
      Height = 23
      Top = 8
      Width = 120
      Increment = 1
      MaxValue = 0
      MinValue = 0
      OnChange = fseReal1Change
      TabOrder = 0
      Value = 0
    end
    object fseIm1: TFloatSpinEdit
      Left = 110
      Height = 23
      Top = 42
      Width = 120
      Increment = 1
      MaxValue = 0
      MinValue = 0
      OnChange = fseIm1Change
      TabOrder = 1
      Value = 0
    end
  end
  object gbNumber2: TGroupBox
    Left = 16
    Height = 105
    Top = 136
    Width = 245
    Caption = 'Nombre 2'
    ClientHeight = 85
    ClientWidth = 241
    TabOrder = 6
    object lblReal2: TLabel
      Left = 8
      Height = 15
      Top = 16
      Width = 67
      Caption = 'Partie réelle :'
      FocusControl = fseReal2
      ParentColor = False
    end
    object lblIm2: TLabel
      Left = 8
      Height = 15
      Top = 50
      Width = 95
      Caption = 'Partie imaginaire :'
      FocusControl = fseIm2
      ParentColor = False
    end
    object fseReal2: TFloatSpinEdit
      Left = 110
      Height = 23
      Top = 8
      Width = 120
      Increment = 1
      MaxValue = 0
      MinValue = 0
      OnChange = fseReal2Change
      TabOrder = 0
      Value = 0
    end
    object fseIm2: TFloatSpinEdit
      Left = 110
      Height = 23
      Top = 42
      Width = 120
      Increment = 1
      MaxValue = 0
      MinValue = 0
      OnChange = fseIm2Change
      TabOrder = 1
      Value = 0
    end
  end
end

Le code répond aux clics sur les boutons, mais aussi aux changements dans les éditeurs des nombres :

 
Sélectionnez
procedure TMainForm.btnAddClick(Sender: TObject);
// *** addition ***
var
  LRes: TComplex;
begin
  LRes := Complex1 + Complex2;
  with mmoMain.Lines do
  begin
    Add('');
    Add('Addition');
    Add('Partie réelle : ' + FloatToStr(LRes.r));
    Add('Partie imaginaire : ' + FloatToStr(LRes.i));
  end;
end;

procedure TMainForm.btnEqualPClick(Sender: TObject);
// *** égalité ***
var
  LSt: string;
begin
  if (Complex1 = Complex2) then
    LSt := 'Les nombres sont égaux.'
  else
    LSt := 'Les nombres ne sont pas égaux.';
  with mmoMain.Lines do
  begin
    Add('');
    Add('Egalité');
    Add(LSt);
  end;
end;

procedure TMainForm.btnMulClick(Sender: TObject);
// *** multiplication ***
var
  LRes: TComplex;
begin
  LRes := Complex1 * Complex2;
  with mmoMain.Lines do
  begin
    Add('');
    Add('Multiplication');
    Add('Partie réelle : ' + FloatToStr(LRes.r));
    Add('Partie imaginaire : ' + FloatToStr(LRes.i));
  end;
end;

procedure TMainForm.btnSubClick(Sender: TObject);
// *** soustraction ***
var
  LRes: TComplex;
begin
  LRes := Complex1 - Complex2;
  with mmoMain.Lines do
  begin
    Add('');
    Add('Soustraction');
    Add('Partie réelle : ' + FloatToStr(LRes.r));
    Add('Partie imaginaire : ' + FloatToStr(LRes.i));
  end;
end;

procedure TMainForm.fseIm1Change(Sender: TObject);
// *** l'imaginaire 1 a été modifié ***
begin
  Complex1.i := fseIm1.Value;
end;

procedure TMainForm.fseIm2Change(Sender: TObject);
// *** l'imaginaire 2 a été modifié ***
begin
  Complex2.i := fseIm2.Value;
end;

procedure TMainForm.fseReal1Change(Sender: TObject);
// *** le réel 1 a été modifié ***
begin
  Complex1.r := fseReal1.Value;
end;

procedure TMainForm.fseReal2Change(Sender: TObject);
// *** le réel 2 a été modifié ***
begin
  Complex2.r := fseReal2.Value;
end;

L'essentiel réside en ce que les écritures sont à présent similaires à celles utilisées avec les opérations arithmétiques classiques.

Voici une capture d'écran de l'application décrite :

Image non disponible

Un aspect intéressant de l'utilisation des opérateurs est qu'ils permettent aussi des combinaisons moins évidentes. Par exemple, on peut imaginer affecter un nombre réel à un nombre complexe puisque le premier est un cas particulier du second.

L'opérateur d'affectation prendrait alors la forme suivante :

 
Sélectionnez
type

  TComplex = record
   r: Real;
   i: Real;
   class operator +(c1, c2: TComplex): TComplex;
   class operator -(c1, c2: TComplex): TComplex;
   class operator *(c1, c2: TComplex): TComplex;
   class operator =(c1, c2: TComplex): Boolean;
   class operator := (r : real): TComplex; // nouveau !
  end;

[…]

class operator TComplex.:=(r: real): TComplex;
// *** affectation d'un réel à un complexe ***
begin
  Result.r := r;
  Result.i := 0.0;
end;

À présent, des écritures comme celle-ci sont autorisées :

 
Sélectionnez
var
  c: TComplex;
  r: Real;

[…]

  c:= r;

Le transtypage est dorénavant inutile : l'opérateur surchargé l'effectue automatiquement à partir de l'implémentation définie.

III-B. Une histoire d'énumération

Un énumérateur est une autre technique qui peut intéresser le programmeur puisqu'elle permet de parcourir un ensemble de valeurs appartenant à une structure à travers une boucle de type for...in. Or, les enregistrements étendus acceptent l'implémentation des énumérateurs.

Pour que le mécanisme fonctionne, il faut définir :

  • un type enregistrement (c'est l'énumérateur) capable de parcourir l'ensemble de données voulu ;
  • une fonction au sein de l'enregistrement abritant l'ensemble à parcourir, fonction qui renvoie une donnée du type défini ci-dessus.

L'énumérateur lui-même doit impérativement implémenter deux fonctions et une propriété :

  • la propriété en lecture seule Current qui renvoie l'élément en cours de traitement ;
  • la fonction GetCurrent qui acquiert l'information nécessaire à Current ;
  • la fonction MoveNext qui passe à l'élément suivant et qui renvoie un booléen (True si l'élément a pu être acquis, False si la fin de l'ensemble a été atteinte).

Une fois ces conditions remplies, le compilateur saura, sans aucune intervention, gérer une boucle for...in avec les enregistrements du type visé.

[Exemple extendedrecord05]

Une petite application va illustrer ce mécanisme plus simple qu'il n'y paraît : on se propose de saisir dix notes et de les afficher dans un TMemo ou d'en calculer la moyenne.

L'interface utilisateur aura cette forme :

Image non disponible

Le fichier LFM correspondant est celui-ci :

 
Sélectionnez
object MainForm: TMainForm
  Left = 256
  Height = 370
  Top = 173
  Width = 384
  ActiveControl = SpinEdit1
  Caption = 'test des enregistrements étendus 05'
  ClientHeight = 370
  ClientWidth = 384
  OnCreate = FormCreate
  Position = poScreenCenter
  LCLVersion = '1.6.4.0'
  object SpinEdit1: TSpinEdit
    Left = 8
    Height = 23
    Top = 8
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 0
    Value = 14
  end
  object SpinEdit2: TSpinEdit
    Left = 8
    Height = 23
    Top = 40
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 1
    Value = 8
  end
  object SpinEdit3: TSpinEdit
    Left = 8
    Height = 23
    Top = 72
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 2
    Value = 4
  end
  object SpinEdit4: TSpinEdit
    Left = 8
    Height = 23
    Top = 104
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 3
    Value = 12
  end
  object SpinEdit5: TSpinEdit
    Left = 8
    Height = 23
    Top = 136
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 4
    Value = 15
  end
  object SpinEdit6: TSpinEdit
    Left = 8
    Height = 23
    Top = 168
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 5
    Value = 20
  end
  object SpinEdit7: TSpinEdit
    Left = 8
    Height = 23
    Top = 200
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 6
    Value = 16
  end
  object SpinEdit8: TSpinEdit
    Left = 8
    Height = 23
    Top = 232
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 7
    Value = 10
  end
  object SpinEdit9: TSpinEdit
    Left = 8
    Height = 23
    Top = 264
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 8
    Value = 13
  end
  object SpinEdit10: TSpinEdit
    Left = 8
    Height = 23
    Top = 296
    Width = 50
    MaxValue = 20
    OnChange = SpinEdit1Change
    TabOrder = 9
    Value = 10
  end
  object btnEnumerate: TButton
    Left = 8
    Height = 25
    Top = 336
    Width = 75
    Caption = 'Enumérer'
    OnClick = btnEnumerateClick
    TabOrder = 10
  end
  object mmoMain: TMemo
    Left = 104
    Height = 314
    Top = 8
    Width = 262
    ReadOnly = True
    ScrollBars = ssAutoBoth
    TabOrder = 11
  end
  object btnAverage: TButton
    Left = 96
    Height = 25
    Top = 336
    Width = 75
    Caption = 'Moyenne'
    OnClick = btnAverageClick
    TabOrder = 12
  end
end

Les notes sont les champs d'un enregistrement TStudentResults qui possède un énumérateur portant sur elles :

 
Sélectionnez
type
  TNote = 0..20;
  TNotes = array[1..10] of TNote;

  { TNotesEnumerator }

  TNotesEnumerator = record
  private
    fIndex: Integer;
    fNotes: TNotes;
    function GetCurrent: TNote;
  public
    function MoveNext: Boolean;
    property Current: TNote read GetCurrent;
  end;

  { TStudentResults }

  TStudentResults = record
    Notes: TNotes;
    function GetEnumerator: TNotesEnumerator;
    function Average: Real;
  end;

L'implémentation des méthodes des enregistrements est très simple. TNotesEnumerator implémente les deux fonctions imposées :

 
Sélectionnez
{ TNotesEnumerator }

function TNotesEnumerator.GetCurrent: TNote;
begin
  Result := fNotes[fIndex];
end;

function TNotesEnumerator.MoveNext: Boolean;
begin
  Inc(fIndex);
  Result := fIndex < Length(fNotes);
end;

Quant à TStudentResults, il se charge d'implémenter l'unique méthode nécessaire pour l'énumération :

 
Sélectionnez
{ TStudentResults }

function TStudentResults.GetEnumerator: TNotesEnumerator;
begin
  Result.fNotes := Notes;
  Result.fIndex := -1;
end;

Il s'agit pour elle d'initialiser le tableau de notes avec celui en cause et de pointer juste avant le premier élément de l'énumération.

Dans un énumérateur, il faut initialiser le pointeur juste avant le premier élément dans la mesure où l'on a choisi d'incrémenter ce pointeur en début de méthode MoveNext.

Le calcul de la moyenne qui se promettait d'être fastidieux est à présent fortement simplifié :

 
Sélectionnez
function TStudentResults.Average: Real;
var
  LNote: TNote;
  LTot: Integer;
begin
  LTot := 0;
  for LNote in Notes do
    Inc(LTot, LNote);
  Result := LTot / Length(Notes);
end;

La méthode utilisée se contente en effet de parcourir les notes entrées et d'en faire la somme avant la division nécessaire par le nombre de notes.

L'écriture des gestionnaires OnClick des deux boutons est elle aussi simplifiée à l'extrême. Sachant que la variable d'enregistrement est déclarée dans TMainForm et s'appelle res, on a :

 
Sélectionnez
procedure TMainForm.btnEnumerateClick(Sender: TObject);
var
  LNote: TNote;
begin
  with mmoMain.Lines do
  begin
    Add('');
    for LNote in res.Notes do
      Add('Note : ' + IntToStr(LNote));
  end;
end;

procedure TMainForm.btnAverageClick(Sender: TObject);
begin
  with mmoMain.Lines do
  begin
    Add('');
    Add('Moyenne : ' + FloatToStr(Res.Average));
  end;
end;

Il ne reste qu'à initialiser les champs de res lors de la création de la fiche et à permettre leur mise à jour lorsqu'un des TSpinEdit est modifié :

 
Sélectionnez
procedure TMainForm.FormCreate(Sender: TObject);
begin
  Res.Notes[1] := SpinEdit1.Value;
  Res.Notes[2] := SpinEdit2.Value;
  Res.Notes[3] := SpinEdit3.Value;
  Res.Notes[4] := SpinEdit4.Value;
  Res.Notes[5] := SpinEdit5.Value;
  Res.Notes[6] := SpinEdit6.Value;
  Res.Notes[7] := SpinEdit7.Value;
  Res.Notes[8] := SpinEdit8.Value;
  Res.Notes[9] := SpinEdit9.Value;
  Res.Notes[10] := SpinEdit10.Value;
end;

procedure TMainForm.SpinEdit1Change(Sender: TObject);
begin
  Res.Notes[(Sender as TSpinEdit).TabOrder + 1] := (Sender as TSpinEdit).Value;
end;

Une astuce permet de ne créer qu'un seul gestionnaire OnChange pour tous les TSpinEdit. L'application les reconnaît alors par transtypage et en fonction de la valeur de leur propriété TabOrder définie automatiquement par l'EDI suivant l'ordre de tabulation de chaque contrôle.

On pourrait de même simplifier l'initialisation en employant la même astuce :

 
Sélectionnez
procedure TMainForm.FormCreate(Sender: TObject);
var
  Li: Integer;
begin
  for Li := 1 to ComponentCount do
   if (Components[Li - 1] is TSpinEdit) then
    Res.Notes[Li] := (Components[Li - 1] as TSpinEdit).Value;
end;

Il s'agit de parcourir tous les composants présents sur la fiche (ils sont numérotés à partir de 0) et de remplir le tableau de notes avec la valeur de la propriété Value des composants de type TSpinEdit.

L'exécution de cette application donnera des affichages comme celui-ci :

Image non disponible

[Exemple extendedrecord06]

Pour mettre en place un énumérateur, une autre possibilité se présente en remarquant qu'un énumérateur est une forme d'opérateur.

Il est par conséquent possible de réécrire une partie du code précédent ainsi :

 
Sélectionnez
TStudentResults = record
  Notes: TNotes;
  function Average: Real;
  class operator Enumerator(Res: TStudentResults): TNotesEnumerator; end;

Il faut faire attention de bien faire figurer pour l'opérateur de classe un paramètre du type de l'enregistrement dans lequel l'opérateur est défini.

L'opérateur sera alors implémenté comme suit :

 
Sélectionnez
class operator TStudentResults.Enumerator(Res: TStudentResults):
  TNotesEnumerator;
begin
  Result.fNotes := Res.Notes;
  Result.fIndex := -1;
end;

Le résultat obtenu n'est en rien modifié. En dehors des portions de code ci-dessus, l'application exemple est identique à la précédente.

[Exemple extendedrecord07]

Enfin, afin de rendre le code plus lisible, il est possible d'utiliser des identificateurs pour la méthode MoveNext et la propriété Current autres que ceux par défaut. Afin que le compilateur s'y retrouve, il suffit de compléter les déclarations avec le modificateur enumerator :

 
Sélectionnez
TNotesEnumerator = record
  private
    fIndex: Integer;
    fNotes: TNotes;
    function GetCurrentNote: TNote;
  public
    function NextNote: Boolean; enumerator MoveNext;
    property CurrentNote: TNote read GetCurrentNote; enumerator Current;
  end;

Une fois la méthode NextNote et le getter de la propriété CurrentNote implémentés, les correspondances se feront automatiquement si bien que les résultats obtenus seront similaires à ceux obtenus par les procédures précédentes.

IV. Conclusion

Avec ce tutoriel, vous aurez appris à :

  • déclarer un enregistrement étendu ;
  • implémenter ses méthodes ;
  • évaluer les avantages de son utilisation ;
  • définir et utiliser des opérateurs ;
  • définir et manipuler des énumérateurs.

Il reste au moins un des aspects des enregistrements à étudier, mais qui mérite à lui seul un autre tutoriel : il s'agit des assistants qui seront abordés prochainement !

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.