Was ist ein verwalteter Datensatz in Delphi?

Datensätze in Delphi können Felder eines beliebigen Datentyps enthalten. Wenn ein Datensatz einfache (nicht verwaltete) Felder hat, wie numerische oder andere Aufzählungswerte, gibt es für den Compiler nicht viel zu tun. Das Erstellen und Entsorgen des Datensatzes besteht darin, Speicher zuzuweisen oder den Speicherplatz zu löschen. (Beachten Sie, dass Delphi standardmäßig keine Null-Initialisierung von Datensätzen vornimmt).

Wenn ein Datensatz ein Feld eines Typs enthält, der vom Compiler verwaltet wird (wie ein String oder eine Schnittstelle), muss der Compiler zusätzlichen Code einfügen, um die Initialisierung oder Finalisierung zu verwalten. Eine Zeichenkette wird beispielsweise als Referenz gezählt. Wenn der Datensatz den Gültigkeitsbereich verlässt, muss die Referenzzählung für die Zeichenkette innerhalb des Datensatzes verringert werden, was dazu führen kann, dass der Speicher für die Zeichenkette freigegeben wird. Wenn Sie einen solchen verwalteten Datensatz in einem Abschnitt des Codes verwenden, fügt der Compiler daher automatisch einen try-finally-Block um diesen Code herum ein und sorgt dafür, dass die Daten auch im Falle einer Ausnahme gelöscht werden. Dies ist schon seit langem der Fall. Mit anderen Worten: Verwaltete Datensätze sind Teil der Delphi-Sprache.

Datensätze mit Initialisierungs- und Finalisierungsoperatoren

In 10.4 unterstützt der Delphi-Datensatztyp nun eine benutzerdefinierte Initialisierung und Finalisierung, die über die Standardoperationen hinausgeht, die der Compiler für verwaltete Datensätze durchführt. Sie können einen Datensatz mit benutzerdefiniertem Initialisierungs- und Finalisierungscode deklarieren, unabhängig vom Datentyp seiner Felder, und Sie können einen solchen benutzerdefinierten Initialisierungs- und Finalisierungscode schreiben. Dazu fügen Sie dem Datensatztyp spezielle neue Operatoren hinzu (Sie können den einen ohne den anderen haben, wenn Sie möchten).
Nachfolgend finden Sie ein einfaches Codeschnipsel:

Typ
  TMyRecord = Datensatz
    Wert: Integer;
    class operator Initialize (out Dest: TMyRecord);
    class operator Finalize(var Dest: TMyRecord);
  Ende;

Sie müssen natürlich den Code für die beiden Klassenmethoden schreiben, z.B. deren Ausführung protokollieren oder den Datensatzwert initialisieren - hier protokollieren wir auch einen Verweis auf einen Speicherplatz, um zu sehen, welcher Datensatz jede einzelne Operation ausführt:

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

class operator TMyRecord.Finalize(var Dest: TMyRecord);
begin
  Log('zerstört' + IntToHex (Integer(Pointer(@Dest)))));
end;

Der große Unterschied zwischen diesem Konstruktionsmechanismus und dem, was vorher für Datensätze verfügbar war, ist der automatische Aufruf. Wenn Sie etwas wie den nachstehenden Code schreiben, können Sie sowohl den Initialisierer als auch den Finalisierer aufrufen und haben am Ende einen Try-Final-Block, der vom Compiler für Ihre verwaltete Datensatzinstanz erzeugt wird.

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

Mit diesem Code erhalten Sie ein Protokoll wie:

erstellt 0019F2A8
10
zerstört 0019F2A8

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

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

wodurch Sie die gleiche Sequenz im Protokoll erhalten.

Der Operator Zuweisen

Die :=-Zuweisung kopiert einfach alle Daten der Datensatzfelder. Dies ist zwar eine vernünftige Standardeinstellung, aber wenn Sie benutzerdefinierte Datenfelder und eine benutzerdefinierte Initialisierung haben, möchten Sie dieses Verhalten vielleicht ändern. Aus diesem Grund können Sie für benutzerdefinierte verwaltete Datensätze auch einen Zuweisungsoperator definieren. Der neue Operator wird mit der :=-Syntax aufgerufen, aber als Assign definiert:

  class operator Assign (var Dest: TMyRecord; const [ref] Src: TMyRecord);

Die Operator-Definition muss sehr genaue Regeln befolgen. Dazu gehört, dass der erste Parameter ein Referenzparameter ist und der zweite als const per Referenz übergeben wird. Wenn Sie dies nicht tun, gibt der Compiler Fehlermeldungen wie die folgende aus:

[dcc32 Fehler] E2617 Der erste Parameter des Assign-Operators muss ein var-Parameter des Containertyps sein
[dcc32 Hinweis] H2618 Zweiter Parameter des Assign-Operators muss ein const[Ref]- oder var-Parameter des Containertyps sein

Es gibt einen Beispielfall, in dem der Operator Zuweisen aufgerufen wird:

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

erzeugt dieses Protokoll (in dem wir dem Datensatz auch eine Sequenznummer hinzufügen):

erstellt 5 0019F2A0
erstellt 6 0019F298
5 kopiert nach 6
zerstört 6 0019F298
zerstört 50019F2A0

Beachten Sie, dass die Reihenfolge der Zerstörung umgekehrt ist als die Reihenfolge des Aufbaus.

Übergabe verwalteter Datensätze als Parameter

Verwaltete Datensätze können anders funktionieren als normale Datensätze, 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;

Ohne jetzt jedes Protokoll einzeln durchzugehen, hier die Zusammenfassung der Informationen:

  • ParByValue erstellt einen neuen Datensatz und ruft den Zuweisungsoperator (falls verfügbar) auf, um die Daten zu kopieren. Die temporäre Kopie wird beim Verlassen der Prozedur zerstört.
  • ParByConstValue macht keine Kopie und überhaupt keinen Aufruf
  • ParByRef macht keine Kopie, kein Aufruf
  • ParByConstRef macht keine Kopie, kein Aufruf
  • ParReturned erstellt einen neuen Datensatz (über Initialize) und ruft bei der Rückkehr den Operator Assign auf, wenn der Aufruf wie im Folgenden aussieht, und löscht den temporären Datensatz, sobald er wie in zugewiesen wurde: my1 := ParReturned;

Ausnahmen und verwaltete Datensätze

Wenn eine Ausnahme ausgelöst wird, werden Datensätze im Allgemeinen gelöscht, auch wenn kein expliziter Try- und Finally-Block vorhanden ist, im Gegensatz zu Objekten. Dies ist ein grundlegender Unterschied und der Schlüssel zum wirklichen Nutzen von verwalteten Datensätzen.

procedure ExceptionTest;
begin
  var a: TMRE;
  var b: TMRE;
  raise Exception.Create('Fehlermeldung');
end;

Innerhalb dieser Prozedur gibt es zwei Konstruktoraufrufe und zwei Destruktoraufrufe. Auch dies ist ein grundlegender Unterschied und ein Hauptmerkmal von verwalteten Datensätzen. Siehe den späteren Abschnitt über einen einfachen Smart Pointer, der auf verwalteten Datensätzen basiert.

Arrays von verwalteten Datensätzen

Wenn Sie ein statisches Array mit verwalteten Datensätzen definieren, werden diese initialisiert, indem Sie den Operator Initialize bei der Punktdeklaration aufrufen:

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

Sie werden alle zerstört, wenn sie aus dem Geltungsbereich herauskommen. Wenn Sie ein dynamisches Array mit verwalteten Datensätzen definieren, wird der Initialisierungscode mit der Größe des Arrays (mit SetLength) aufgerufen:

var
  a2: Array von TMyRecord;
begin
  Log ('ArrOfDyn');
  SetLength(a2, 5); // hier aufrufen

Fazit

Dies ist nur eine relativ kurze Einführung in eine großartige neue Funktion, die Embarcadero der Sprache Delphi für die kommende Version 10.4 hinzufügt. Verwaltete Datensätze funktionieren zum Beispiel für generische Datensätze und in vielen anderen Szenarien, die hier nicht behandelt werden. Und obwohl dies die wichtigste neue Funktion der Sprache ist, gibt es noch weitere, wie die neue einheitliche Speicherverwaltung für alle Plattformen. Bleiben Sie dran!

Wenn Sie ein Update-Abonnement haben, erhalten Sie unter anderem Zugang zu den Beta-Builds der kommenden Versionen. Es ist noch Zeit, an unserem Beta-Programm für 10.4 teilzunehmen!

Dies ist eine Vorschau auf eine kommende Version von RAD Studio. Es kann immer wieder zu Fehlern oder Änderungen in letzter Minute kommen. Nichts hier ist endgültig, bis die Version offiziell zur Verfügung gestellt wird.