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

Enregistrements étendus

Gilles Vasseur

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.

Commentez

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

- Auteur : Gilles Vasseur

- Parution : 8 avril 2017

- Public visé :

- Licence : 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.

Sommaire

- I. Introduction

- II. Travailler avec des enregistrements étendus

- II-A. Déclarer un enregistrement étendu

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

- II-C. Utilité des enregistrements étendus

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

- II-E. Enregistrements simples et enregistrements étendus

- III. Mettre en œuvre les enregistrements étendus

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

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

- IV. Conclusion

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 :

// 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 :

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 :

{ 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 :

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

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 :

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 :

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 :

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 :

[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 :

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 :

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 :

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 :

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.

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 :

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 :

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 :

{ 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 :

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 :

Traduite pour le fichier LFM, elle donne :

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 :

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 :

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 :

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 :

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 :

Le fichier LFM correspondant est celui-ci :

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 :

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 :

{ 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 :

{ 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é :

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 :

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é :

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 :

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 :

[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 :

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 :

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 :

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.