Enregistrements managés personnalisés
Sommaire
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é.