Enregistrements managés personnalisés

De RAD Studio
Aller à : navigation, rechercher

Présentation

Dans Delphi, les enregistrements contiennent des champs de tous types de données. Lorsqu'un enregistrement comporte des champs bruts (non managés), comme des valeurs numériques ou d'autres valeurs énumérées, le compilateur a peu d'opérations à exécuter. Pour créer ou éliminer un enregistrement, il faut allouer de la mémoire ou supprimer l'emplacement mémoire.

Si un enregistrement comporte un champ d'un type managé par le compilateur (comme une chaîne ou une interface), le compilateur doit injecter du code supplémentaire pour générer l'initialisation ou la finalisation. Une chaîne, par exemple, utilise le comptage de références, de sorte que lorsque l'enregistrement sort de la portée, la chaîne contenue dans l'enregistrement décrémente son compteur de références, libérant de ce fait de la mémoire. Ainsi, lorsque vous utilisez un tel enregistrement managé dans une section de code, le compilateur ajoute automatiquement un bloc try-finally autour de ce code, et s'assure que les données sont effacées même en cas d'exception. Ce fonctionnement existe depuis longtemps. En d'autres termes, les enregistrements managés font partie du langage Delphi.

Enregistrements avec les opérateurs Initialize et Finalize

Le type d'enregistrement de Delphi prend en charge l'initialisation et la finalisation personnalisées au-delà des opérations par défaut réalisées par le compilateur pour les enregistrements managés. Vous pouvez déclarer un enregistrement avec du code d'initialisation et de finalisation personnalisé indépendamment du type de données de ses champs, et écrire ce code d'initialisation et de finalisation personnalisé. Vous devez pour cela ajouter de nouveaux opérateurs spécifiques au type d'enregistrement (vous pouvez avoir l'un sans l'autre). Voici ci-dessous un extrait de code simple :

type
  TMyRecord = record
    Value: Integer;
    class operator Initialize (out Dest: TMyRecord);
    class operator Finalize (var Dest: TMyRecord);
  end;

N'oubliez pas que vous devez écrire le code pour les deux méthodes. Par exemple, lors de la journalisation de l'exécution ou de l'initialisation de la valeur d'enregistrement, la référence à un emplacement mémoire est aussi journalisée, pour voir quel enregistrement effectue chaque opération individuelle :

class operator TMyRecord.Initialize (out Dest: TMyRecord);
begin
  Dest.Value := 10;
  Log('created' + IntToHex (IntPtr(@Dest)));
end;

class operator TMyRecord.Finalize (var Dest: TMyRecord);
begin
  Log('destroyed' + IntToHex (IntPtr(@Dest)));
end;

La principale différence entre cette construction et ce qui existait auparavant pour les enregistrements se situe au niveau de l'invocation automatique. Lorsque vous écrivez du code similaire au code ci-dessous, vous pouvez invoquer l'initialiseur et le finaliseur, puis terminer par un bloc try-finally généré par le compilateur pour votre instance d'enregistrement managé.

procedure LocalVarTest;
var
  my1: TMyRecord;
begin
  Log (my1.Value.ToString);
end;

Avec ce code, vous obtenez un journal de type :

created 0019F2A8

10

destroyed 0019F2A8

Vous pouvez aussi utiliser des variables inline, comme :

begin
  var t: TMyRecord;
  Log(t.Value.ToString);

qui vous permet d'obtenir la même séquence dans le journal.

L'opérateur Assign

L'assignation := copie toutes les données des champs d'enregistrement. Bien que ce comportement par défaut soit raisonnable, il est possible de le changer pour des champs de données personnalisés et un code d'initialisation personnalisé. Ainsi, pour des enregistrements managés personnalisés, vous pouvez définir un opérateur d'assignation. Le nouvel opérateur est invoqué par la syntaxe  := mais est défini en tant que Assign :

type
  TMyRecord = record
    Value: Integer;
    class operator Assign (var Dest: TMyRecord; 
 		const [ref] Src: TMyRecord);

La définition de l'opérateur doit suivre des règles précises, y compris le fait d'utiliser le premier paramètre comme un paramètre de référence et le second comme un paramètre const transmis par référence. Si vous ne procédez pas ainsi, le compilateur enverra des messages d'erreur comme ci-dessous :

[dcc32 Error] E2617 First parameter of Assign operator must be a var parameter of the container type

[dcc32 Hint] H2618 Second parameter of Assign operator must be a const[Ref] or var parameter of the container type

Cet exemple de code invoquant l'opérateur Assign :

var
  my1, my2: TMyRecord;
begin
  my1.Value := 22;
  my2 := my1;

produit ce journal (où un numéro de séquence est inclus dans l'enregistrement) :

created 5 0019F2A0

created 6 0019F298

5 copied to 6

destroyed 6 0019F298

destroyed 5 0019F2A0

Notez que la séquence de destruction est inversée par rapport à la séquence de construction.

L'opérateur Assign est utilisé conjointement avec des opérations d'assignation comme dans l'exemple ci-dessus, mais aussi si vous utilisez une assignation pour initialiser une variable inline. Voici deux exemples distincts :

var
  my1: TMyRecord;
begin
  var t := my1;
  Log(t.Value.ToString);

  var s: TMyRecord;
  Log(s.Value.ToString);

produit ce journal :

created 6 0019F2A8

created 7 0019F2A0

6 copied to 7

10

created 8 0019F298

10

destroyed 8 0019F298

destroyed 7 0019F2A0

destroyed 6 0019F2A8

Dans le premier exemple, la création et l'assignation se déroulent comme dans les scénarios standard avec des variables non locales. Dans le second exemple, il y a simplement une initialisation standard.

Transmission d'enregistrements managés en tant que paramètres

Les enregistrements managés fonctionnent différemment des enregistrements standard lorsqu'ils sont transmis en tant que paramètres ou renvoyés par une fonction. Voici plusieurs routines affichant les différents scénarios :

procedure ParByValue (rec: TMyRecord);
procedure ParByConstValue (const rec: TMyRecord);
procedure ParByRef (var rec: TMyRecord);
procedure ParByConstRef (const [ref] rec: TMyRecord);
function ParReturned: TMyRecord;

Chaque journal effectue les opérations suivantes :

  • ParByValue crée un nouvel enregistrement et appelle l'opérateur d'assignation (s'il est disponible) pour copier les données, détruisant la copie temporaire au moment de quitter la procédure.
  • ParByConstValue n'effectue aucune copie, aucun appel.
  • ParByRef n'effectue aucune copie, aucun appel.
  • ParByConstRef n'effectue aucune copie, aucun appel.
  • ParReturned crée un nouvel enregistrement (via Initialize) et appelle l'opérateur Assign, si l'appel est similaire au suivant, et supprime l'enregistrement temporaire :
my1 := ParReturned;

Exceptions et enregistrements managés

Lorsqu'une exception est déclenchée, les enregistrements sont globalement effacés, même lorsqu'aucun bloc try finally n'est présent, contrairement aux objets. C'est une différence fondamentale, essentielle à l'utilité réelle des enregistrements managés.

procedure ExceptionTest;
begin
  var a: TMRE;
  var b: TMRE;

  raise Exception.Create('Error Message');
end;

Cette procédure comporte deux appels de constructeur et deux appels de destructeur. C'est une différence fondamentale et essentielle pour l'utilité réelle des enregistrements managés. Consultez la dernière section relative à un pointeur simple intelligent basé sur des enregistrements managés.

D'un autre côté, si une exception est déclenchée dans l'initialiseur d'un enregistrement managé, le destructeur correspondant n'est pas invoqué, contrairement à ce qui se passe avec des objets standard.

Tableaux d'enregistrements managés

Si vous définissez un tableau statique d'enregistrements managés, ceux-ci sont initialisés en appelant l'opérateur Initialize au point de déclaration :

var
  a1: array [1..5] of TMyRecord; // call here
begin
  Log ('ArrOfRec');

Ils sont tous détruits lorsqu'ils sortent de la portée. Si vous définissez un tableau dynamique d'enregistrements gérés, le code d'initialisation est appelé avec le tableau dimensionné (avec SetLength) :

var
  a2: array of TMyRecord;
begin
  Log ('ArrOfDyn');  
  SetLength(a2, 5); // call here

Enregistrements managés Delphi dans C++

Les enregistrements managés ne sont pas pris en charge dans C++. Si vous avez besoin d'écrire du code accessible dans les deux langages, comme les composants, utilisez uniquement des enregistrements normaux dans le code qui sera visible dans C++ (c'est-à-dire dans la section interface d'une unité Delphi incluant des types déclarés et des paramètres de méthodes ou des types de retour.)

Les opérateurs Initialize et Finalize sont déclarés dans le fichier HPP généré, mais aucun constructeur ni destructeur les invoquant n'est généré. De même, l'opérateur Assign est déclaré dans le fichier HPP, mais aucun constructeur de copie ni opérateur d'assignation correspondant n'est déclaré.

Attention: Il n'est pas recommandé d'utiliser le code C++ avec un enregistrement managé.