Benutzerdefinierte verwaltete Records

Aus RAD Studio
Wechseln zu: Navigation, Suche

Übersicht

Records in Delphi können Felder mit beliebigen Typen enthalten. Wenn ein Record einfache (nicht verwaltete) Felder enthält, wie numerische oder andere Aufzählungswerte, gibt es für den Compiler nicht viel zu tun. Das Erstellen und Freigeben des Records besteht aus der Zuweisung von Speicher bzw. dem Entfernen der Speicherposition.

Wenn ein Record ein Feld mit einem vom Compiler verwalteten Typ enthält (wie ein String oder ein Interface), muss der Compiler zusätzlichen Code für die Initialisierung oder die Finalisierung einfügen. Ein String verfügt beispielsweise über eine Referenzzählung. Wenn also der Record den Gültigkeitsbereich verlässt, muss der Referenzzähler des Strings innerhalb des Records verringert werden, was dazu führen kann, dass der Speicher für den String freigegeben wird. Wenn Sie also einen solchen verwalteten Record in einem Abschnitt des Codes verwenden, setzt der Compiler automatisch einen try-finally-Block um diesen Code und sorgt dafür, dass die Daten auch im Falle einer Exception gelöscht werden. Das erfolgt schon lange Zeit so. Mit anderen Worten: Verwaltete Records waren Bestandteil der Delphi-Sprache.

Records mit Operatoren zum Initialisieren und Finalisieren

Der Delphi-Record-Typ unterstützt die benutzerdefinierte Initialisierung und Finalisierung über die Standardoperationen hinaus, die der Compiler für verwaltete Records durchführt. Sie können einen Record mit benutzerdefiniertem Initialisierungs- und Finalisierungscode undabhängig von Datentyp der Felder deklarieren und Sie können diesen Initialisierungs- und Finalisierungscode schreiben. Dies wird erreicht, indem dem Record-Typ spezielle, neue Operatoren hinzugefügt werden. Unten finden Sie ein einfaches Codefragment:

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

Beachten Sie, dass Sie den Code für beide Klassenmethoden schreiben müssen. Beispielsweise wird bei der Protokollierung ihrer Ausführung oder der Initialisierung des Record-Werts auch eine Referenz auf den Speicherplatz protokolliert, um festzustellen, welcher Record die jeweilige Operation ausführt:

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;

Der große Unterschied zwischen diesem Konstruktor und den früheren Möglichkeiten bei Records ist der automatische Aufruf. Wenn Sie ähnlichen Code wie den unten dargestellten schreiben, können Sie die Initialisierung und Finalisierung aufrufen und erhalten einen try-finally-Block, den der Compiler für die verwaltete Record-Instanz generiert.

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

Für diesen Code erhalten Sie ein Protokoll wie das folgende:

created 0019F2A8

10

destroyed 0019F2A8

Ein weiteres Szenario ist die Verwendung von Inline-Variablen, wie hier:

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

Damit erhalten Sie dieselbe Sequenz im Protokoll.

Der Operator "Assign"

Mit der Zuweisung  := werden alle Daten der Record-Felder kopiert. Dies ist zwar ein vernünftiger Standard, aber für benutzerdefinierte Datenfelder und benutzerdefinierte Initialisierungen möchten Sie dieses Verhalten möglicherweise ändern. Aus diesem Grund können Sie für benutzerdefinierte verwaltete Records auch einen Zuweisungsoperator definieren. Der neue Operator wird mit der Syntax := aufgerufen, ist aber als "Assign" definiert:

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

Bei der Operatordefinition müssen genaue Regel eingehalten werden, wie z. B., dass der erste Parameter ein Referenzparameter und der zweite ein per Referenz übergebener const-Parameter ist. Wenn Sie diese Regeln nicht einhalten, gibt der Compiler Fehlermeldungen wie die folgenden aus:

[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

Hier ist ein Beispielfall, in dem der "Assign"-Operator aufgerufen wird:

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

Damit wird dieses Protokoll erzeugt (wobei die Sequenznummer in den Record einbezogen wird):

created 5 0019F2A0

created 6 0019F298

5 copied to 6

destroyed 6 0019F298

destroyed 5 0019F2A0

Beachten Sie, dass die Freigabe in umgekehrter Reihenfolge wie die Erstellung erfolgt.

Der "Assign"-Operator wird in Verbindung mit Zuweisungsoperationen wie im obigen Beispiel verwendet und auch, wenn Sie eine Zuweisung zur Initialisierung einer Inline-Variable verwenden. Hier gibt es zwei unterschiedliche Fälle:

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

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

Damit wird Folgendes protokolliert:

created 6 0019F2A8

created 7 0019F2A0

6 copied to 7

10

created 8 0019F298

10

destroyed 8 0019F298

destroyed 7 0019F2A0

destroyed 6 0019F2A8

Im ersten Fall erfolgen Erstellung und Zuweisung wie in regulären Szenarien mit nicht lokalen Variablen. Im zweiten Fall ist nur eine reguläre Initialisierung vorhanden.

Übergeben von verwalteten Records als Parameter

Verwaltete Records können anders als normale Records arbeiten, auch wenn sie als Parameter übergeben oder von einer Funktion zurückgegeben werden. Hier finden Sie einige Routinen, die die verschiedenen Szenarien zeigen:

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

Jeder Eintrag führt die folgenden Operationen durch:

  • ParByValue erstellt einen neuen Record und ruft den Zuweisungsoperator (falls vorhanden) auf, um die Daten zu kopieren, wobei die temporäre Kopie bei Beendigung der Prozedur freigegeben wird.
  • ParByConstValue erstellt keine Kopie und führt keinen Aufruf durch.
  • ParByRef erstellt keine Kopie und führt keinen Aufruf durch.
  • ParByConstRef erstellt keine Kopie und führt keinen Aufruf durch.
  • ParReturned erstellt einen neuen Record (über Initialize) und ruft bei der Rückkehr den Assign-Operator auf, falls der Aufruf dem folgenden entspricht, und löscht den temporären Record:
my1 := ParReturned;

Exceptions und verwaltete Records

Wenn eine Exception ausgelöst wird, werden Records im Allgemeinen gelöscht, auch wenn kein expliziter try-finally-Block vorhanden ist, im Gegensatz zu Objekten. Dies ist ein grundlegender Unterschied und der Schlüssel zum wirklichen Nutzen verwalteter Records.

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

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

In dieser Prozedur erfolgen zwei Konstruktoraufrufe und zwei Destruktoraufrufe. Dies ist wiederum ein grundlegender Unterschied und ein wesentliches Merkmal verwalteter Records. In den folgenden Abschnitten werden einfache, intelligente Zeiger gezeigt, die auf verwalteten Records basieren.

Wird dagegen im Initialisierungsabschnitt eines verwalteten Records eine Exception ausgelöst, wird der passende Destruktor nicht aufgerufen, anders als bei regulären Objekten.

Arrays verwalteter Records

Wenn Sie ein statisches Array verwalteter Records definieren, wird es durch den Aufruf des Initialisierungsoperators zum Zeitpunkt der Deklaration initialisiert:

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

Wenn sie den Gültigkeitsbereich verlassen, werden alle freigegeben. Wenn Sie ein dynamisches Array verwalteter Records definieren, wird der Initialisierungscode mit der Größe des Arrays (mit SetLength) aufgerufen:

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

Verwaltete Delphi-Records in C++

Verwaltete Records werden in C++ nicht unterstützt. Wenn Sie Code schreiben müssen, auf den von beiden Sprachen zugegriffen werden kann, wie Komponenten, verwenden Sie normale Records im Code, der für C++ sichtbar ist (d. h. im Interface-Abschnitt einer Delphi-Unit einschließlich deklarierter Typen und Methodenparameter bzw. Rückgabetypen).

Die Operatoren Initialize und Finalize werden in der generierten HPP deklariert, aber es werden keine Konstruktoren oder Destruktoren generiert, die sie aufrufen. Der Zuweisungsoperator ist in der HPP deklariert, aber der entsprechende Kopierkonstruktor und der Zuweisungsoperator sind nicht deklariert.

Achtung: Die Verwendung von C++-Code mit verwalteten Records wird nicht empfohlen.