Wat is een Managed Record in Delphi?

Records in Delphi kunnen velden van elk datatype hebben. Als een record gewone (niet-beheerde) velden heeft, zoals numerieke of andere opgesomde waarden, hoeft de compiler niet veel te doen. Het maken en verwijderen van het record bestaat uit het toewijzen van geheugen of het verwijderen van de geheugenlocatie. (Merk op dat Delphi standaard geen nul-initialisatie van records uitvoert).

Als een record een veld heeft van een type dat door de compiler wordt beheerd (zoals een string of een interface), moet de compiler extra code injecteren om de initialisatie of finalisatie te beheren. Een string, bijvoorbeeld, wordt gerefereerd geteld, dus als de record uit scope gaat moet de string in de record zijn referentietelling verlagen, wat kan leiden tot het de-alloceren van het geheugen voor de string. Daarom voegt de compiler, wanneer je zo'n beheerd record gebruikt in een deel van de code, automatisch een try-finally blok toe rond die code en zorgt ervoor dat de gegevens worden gewist, zelfs in het geval van een uitzondering. Dit is al heel lang het geval. Met andere woorden, beheerde records zijn onderdeel van de Delphi taal.

Records met initialiseer- en afsluitoperatoren

Nu in 10.4 ondersteunt het Delphi-recordtype aangepaste initialisatie en finalisatie, naast de standaardbewerkingen die de compiler uitvoert voor beheerde records. Je kunt een record declareren met aangepaste initialisatie- en afsluitcode, ongeacht het datatype van de velden, en je kunt dergelijke aangepaste initialisatie- en afsluitcode schrijven. Dit wordt bereikt door specifieke, nieuwe operatoren aan het recordtype toe te voegen (je kunt de ene hebben zonder de andere als je dat wilt).
Hieronder staat een eenvoudig codefragment:

type
  TMyRecord = record
    Waarde: Integer;
    klasse operator Initialize (uit Dest: TMyRecord);
    klasse operator Finalize(var Dest: TMyRecord);
  einde;

Je moet natuurlijk de code voor de twee klassemethoden schrijven, bijvoorbeeld hun uitvoering loggen of de recordwaarde initialiseren - hier loggen we ook een verwijzing naar de geheugenlocatie, om te zien welke record elke afzonderlijke bewerking uitvoert:

klasse operator TMyRecord.Initialize (out Dest: TMyRecord);
begin
  Dest.Waarde := 10;
  Log('gemaakt' + IntToHex (Integer(Pointer(@Dest)))));
einde;

klasse operator TMyRecord.Finalize(var Dest: TMyRecord);
begin
  Log('vernietigd' + IntToHex (Integer(Pointer(@Dest)))));
einde;

Het grote verschil tussen dit constructiemechanisme en wat voorheen beschikbaar was voor records is de automatische aanroep. Als je iets schrijft zoals de code hieronder, kun je zowel de initializer als de finalizer aanroepen en eindig je met een try-finally blok gegenereerd door de compiler voor je managed record instantie.

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

Met deze code krijg je een log zoals:

aangemaakt 0019F2A8
10
vernietigd 0019F2A8

Een ander scenario is het gebruik van inline variabelen, zoals in:

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

waarmee je dezelfde volgorde in het log krijgt.

De toewijzingsoperator

De := toewijzing kopieert alle gegevens van de recordvelden. Hoewel dit een redelijke standaard is, wil je dit gedrag misschien veranderen als je aangepaste gegevensvelden en aangepaste initialisatie hebt. Daarom kun je voor aangepaste beheerde records ook een toewijzingsoperator definiëren. De nieuwe operator wordt aangeroepen met de := syntaxis, maar gedefinieerd als Toewijzen:

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

De definitie van de operator moet zeer nauwkeurige regels volgen, inclusief het hebben van de eerste parameter als een referentieparameter, en de tweede als een const die door verwijzing wordt doorgegeven. Als je dit niet doet, geeft de compiler foutmeldingen zoals de volgende:

[E2617 Eerste parameter van de Assign-operator moet een var-parameter van het containertype zijn.
[dcc32 Hint] H2618 Tweede parameter van de Assign operator moet een const[Ref] of var parameter van het containertype zijn

Er is een voorbeeldcase die de Assign operator aanroept:

var
  mijn1, mijn2: TMyRecord;
begin
  my1.Waarde := 22;
  my2 := my1;

produceert dit logboek (waarin we ook een volgnummer aan het record toevoegen):

aangemaakt 5 0019F2A0
aangemaakt 6 0019F298
5 gekopieerd naar 6
vernietigd 6 0019F298
vernietigd 50019F2A0

Merk op dat de volgorde van vernietiging omgekeerd is aan de volgorde van constructie.

Beheerde records doorgeven als parameters

Beheerde records kunnen anders werken dan gewone records, ook wanneer ze worden doorgegeven als parameters of worden geretourneerd door een functie. Hier zijn verschillende routines die de verschillende scenario's laten zien:

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

Zonder elk logboek één voor één te bekijken, is dit de samenvatting van de informatie:

  • ParByValue maakt een nieuw record en roept de toewijzingsoperator (indien beschikbaar) op om de gegevens te kopiëren, waarbij de tijdelijke kopie wordt vernietigd bij het afsluiten van de procedure
  • ParByConstValue maakt geen kopie en roept helemaal niets op
  • ParByRef maakt geen kopie, geen oproep
  • ParByConstRef maakt geen kopie, geen oproep
  • ParReturned maakt een nieuw record aan (via Initialize) en roept bij terugkeer de Assign operator aan, als de aanroep is zoals de volgende, en verwijdert het tijdelijke record zodra het terug is toegewezen zoals in: my1 := ParReturned;

Uitzonderingen en beheerde records

Wanneer een exception wordt opgeworpen, worden records in het algemeen gewist, zelfs als er geen expliciet try, finally blok aanwezig is, in tegenstelling tot objecten. Dit is een fundamenteel verschil en de sleutel tot het echte nut van beheerde records.

procedure UitzonderingTest;
begin
  var a: TMRE;
  var b: TMRE;
  raise Uitzondering.Maken('Foutmelding');
einde;

Binnen deze procedure zijn er twee constructoraanroepen en twee destructoraanroepen. Nogmaals, dit is een fundamenteel verschil en een belangrijk kenmerk van beheerde records. Zie de paragraaf verderop over een eenvoudige smart pointer gebaseerd op beheerde records.

Arrays van beheerde records

Als je een statische array van beheerde records definieert, worden deze geïnitialiseerd door de operator Initialize op te roepen bij de puntdeclaratie:

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

Ze worden allemaal vernietigd wanneer ze uit scope gaan. Als je een dynamische array van beheerde records definieert, wordt de initialisatiecode aangeroepen als de array de juiste grootte heeft (met SetLength):

var
  a2: array van TMyRecord;
begin
  Log ('ArrOfDyn');
  SetLength(a2, 5); // hier aanroepen

Conclusie

Dit is slechts een relatief korte introductie van een geweldige nieuwe functie die Embarcadero toevoegt aan de Delphi taal voor de komende 10.4 release. Beheerde records werken bijvoorbeeld voor generieke records en in vele andere scenario's die hier niet worden besproken. En hoewel dit de belangrijkste nieuwe taalfunctie is, komen er nog andere aan, zoals het nieuwe unified memory management voor alle platformen. Blijf op de hoogte!

Als je een update-abonnement hebt, is een van de voordelen toegang tot bètaversies van aankomende releases. Er is nog tijd om deel te nemen aan ons bètaprogramma voor 10.4!

Dit is een preview van een aankomende release van RAD Studio. Er kunnen altijd last-minute bugs of wijzigingen zijn. Niets hier is definitief totdat de release officieel beschikbaar is gemaakt.