Il tipo Record di Delphi

Approfondiamo il tipo Record in Delphi, ormai alternativa leggera e flessibile alle classi. Prima parte.

L’evoluzione dl tipo record di Delphi rende facile usarlo per una serie di attività un tempo appannaggio delle classi, ma alcune caratteristiche obbligano a progettarne bene la struttura.

Il tipo Record di Delphi – ma sarebbe più giusto dire del Pascal – è una delle caratteristiche più interessanti del linguaggio di Wirth; nel corso degli anni, però, è stato progressivamente soppiantato dalle classi, che potevano assicurare maggior controllo sui dati contenuti. A partire da Delphi 2005, sono state introdotte nuove capacità che hanno avvicinato molto i record alle classi e ridato a questa struttura un valore da considerare; pertanto, nonostante le profonde differenze concettuali fra record e classe, all’atto pratico si ripropone spesso la scelta fra l’uno o l’altra. Senza scendere in dettagli tecnici più del necessario, presenteremo una piccola guida pratica per aiutare a comprendere i pro e i contro dell’uso dei record.

La VCL usa molto spesso il tipo record: da TRTTIContext a TTimeSpan, solo per fare due esempi a caso, sono numerosi i record che incapsulano specifiche operazioni e le rendono disponibili in maniera semplice e funzionale.

Il tipo record

Il record è un tipo di dati strutturato che può contenere informazioni di tipo diverso; è simile alle strutture di alcuni linguaggi, come C#, ma c’è chi lo paragonano anche a un array con indice stringa invece che numerico. Negli esempi di questa pagina il record di esempio conterrà i dati minimi di una partita di calcio: nomi delle squadre e punteggio.

TMatch = record
  Home: string;
  Away: string;
  HomeScore: integer;
  AwayScore: integer;
end;

Non contiene né metodi, né livelli di visibilità, ma solo dati sempre visibili:

var
  myMatch: TMatch;
begin
  myMatch.Home := 'U.S. Pastore';
  myMatch.HomeScore := 1;
...

A differenza delle classi, i record non si devono né creare né distruggere. Il record, infatti, è un value type, mentre le classi sono reference type e questo, come vedremo, ha molte conseguenze importanti.

Record vs. classe

In questa tabella riportiamo le principali differenze fra record e classi presenti nella versione 10.3 (l’ultima disponibile come Community Edition mentre scriviamo queste note):

ClassiRecord
Sono puntatori?No
EreditarietàNo
Può implementare un’interfacciaNo
Helpers
Visibilità
Metodi
Campi
Proprietà
Costanti
Tipi
Overloading degli operatoriNo
VariantNoSì/No

La versione 10.4 introduce i custom managed record, dei quali ci occuperemo in altra sede, che offrono procedure automatiche di inizializzazione e finalizzazione dei record, e l’overloading dell’operatore Assign; ciò comporta anche modifiche nell’uso dei record come parametri che meritano un esame separato.

Vediamo alcuni effetti generali delle differenze esposte nella tabella:

  • Usando i record, non è necessario usare strutture di controllo degli errori come try... finally... end per assicurarsi che il rilascio delle risorse allocate avvenga correttamente, perché i record sono allocati nello stack. Ciò si traduce non solo in codice più semplice, ma anche in un compilato più compatto ed efficiente.
  • L’assenza di ereditarietà rende i record meno flessibili delle classi. In parte, si può ovviare a ciò attraverso l’uso delle parti variabili (variant) che, però, non sono completamente disponibili nella forma moderna dei record. In altre parole, non si può inserire le proprietà di un record in una parte variabile e una dichiarazione del genere non è corretta:
  TWrongRecord = record
  private
    FA: Integer;
    FB: string;
  public
    case IsInteger: integer of
      1: (
        property A: integer read FA write FA;
      )
      2: (
        property B: string read FB write FB;
      )
    end;
  end;

Mentre è corretto un record avanzato che usa la parte variabile solo per esporre dei campi:

 TRightRecord = record
 private
   a : Integer;
   function GetName : string;
 public
   b : string;
   procedure SetName (aValue : integer);
   property Name: string read GetP write SetP;
 public
 case x : integer of
   1 : (S : string);
   2 : (I : integer);
 end;
  • Analogamente, non si può implementare un’interfaccia attraverso un record.
  • In Delphi, solo i record supportano l’overloading degli operatori. In varie situazioni, ciò consente di dare al codice una semplicità (tanto di scrittura quanto di manutenzione) inarrivabile con le classi. FreePascal, invece, permette l’overloading degli operatori anche con le classi.

I record in azione

E’ utile approfondire queste differenze con qualche esempio pratico. Per chi non ha molta dimestichezza con i record, iniziamo da un esempio di record semplice valido per qualsiasi versione di Delphi:

program pRecords;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TMatch = record
    Home: string;
    Away: string;
    HomeScore: integer;
    AwayScore: integer;
  end;
var
  recordA, recordB: TMatch;
begin
    recordA.HomeScore := 1;
    recordB := recordA;
    recordA.HomeScore := 2;
    Writeln('RecordA.HomeScore = ' + IntToStr(recordA.HomeScore));
    Writeln('RecordB.HomeScore = ' + IntToStr(recordB.HomeScore));
    Writeln('RecordA: ' + IntToStr(Integer( Pointer( @recordA))));
    Writeln('RecordB: ' + IntToStr(Integer( Pointer( @recordB))));
  Readln;
end.

Il risultato è diverso da quello che avremmo ottenuto usando una classe:

RecordA.HomeScore = 2
RecordB.HomeScore = 1
RecordA: 4352220
RecordB: 4352236

L’operatore di assegnazione :=, infatti, esegue una copia del contenuto di recordA in recordB, ma le due variabili restano allocate in aree di memoria differente. Ogni modifica apportata a recordA dopo l’assegnazione, quindi, non è propagata a recordB. Se al posto dei record avremmo usato le classi, invece, dopo l’assegnamento entrambe avrebbero puntato alla stessa istanza, per cui anche RecordB.HomeScore avrebbe restituito il valore 2.

I record come proprietà

Il tipo record di Delphi è così flessibile che, talvolta, sarebbe comodo poterlo usare per le proprietà di una classe, per esempio in strutture di opzioni, al posto delle classi. In questo caso, ci sono limitazioni e alcuni comportamenti, però, potrebbero essere sorprendenti per chi non conosce a fondo il loro funzionamento. Immaginiamo di voler usare il nostro record TMatch in questo modo:

program MatchTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TMatch = record
    Home: string;
    Away: string;
    HomeScore: integer;
    AwayScore: integer;
  end;

  TMatchInfo = class
  private
    FMatch: TMatch;
  public
    property Match: TMatch read FMatch write FMatch;
  end;
var
  MatchInfo: TMatchInfo;
begin
  MatchInfo := TMatchInfo.Create;
  try
  {Linea 30}
    MatchInfo.Match.HomeScore := 2;
   Writeln('MatchInfo.Match.HomeScore = ' + IntToStr(MatchInfo.Match.HomeScore));
    Readln;
   finally
    MatchInfo.Free;
  end;
end.

Compilare questo programma genera l’errore [dcc32 Error] MatchTest.dpr(29): E2064 Left side cannot be assigned to.; se, però, scriviamo

    MatchInfo.FMatch.HomeScore := 2;

il programma funziona e restituisce in output il valore atteso. Il motivo è semplice e sottile allo stesso tempo. La dichiarazione property Match: TMatch read FMatch è semanticamente equivalente a property Match: TMatch read GetMatch; quindi, la procedura implicita GetMatch legge FMatch, ne crea una copia e la restituisce. Il nostro programma cerca di assegnare il valore 2 alla proprietà HomeScore della copia, non a FMatch. Quando invece si punta direttamente a FMatch, non essendoci la copia di mezzo, il problema scompare.

Come si risolve il problema? Ci sono varie soluzioni. Una, che in certi casi potrebbe essere molto elegante, è quella di introdurre una nuova proprietà che legge e scrive direttamente nel campo del record:

    property HomeScore: integer read FMatch.HomeScore write FMatch.HomeScore;
...
{linea 30}
    MatchInfo.HomeScore := 2;

Di solito, però, si ricorre a oggetti e strutture proprio per avere interfacce più compatte e questa soluzione potrebbe non essere adatta. E’ possibile dichiarare il record come membro pubblico della classe:

  TMatchInfo = class
  public
    Match: TMatch;
  end;

o ricorrere all’uso di puntatori.

L’uso dei record avanzati

La soluzione più comune, però, è il passaggio ai record avanzati.

  TMatch = record
  private
    FHome: string;
    FAway: string;
    FHomeScore: integer;
    FAwayScore: integer;
  public
    property Home: string read FHome write FHome;
    property Away: string read FAway write FAway;
    property HomeScore: integer read FHomeScore write FHomeScore;
    property AwayScore: integer read FAwayScore write FAwayScore;
  end;

Con questa modifica, il programma originale è compilato ed eseguito correttamente. Perché? Lo spiega David Heffernan:

Il modo in cui il compilatore implementa le proprietà differisce nel caso di accesso diretto alla proprietà e nel caso di accesso attraverso funzioni. (The way the compiler implements properties differs for direct field property getters and for function property getters.)

David Heffernan

In estrema sintesi, la presenza della proprietà con accesso diretto al relativo field evita che il compilatore generi, attraverso un metodo get implicito, una variabile temporanea; perciò lavora sullo stesso field, come se avessimo scritto MatchInfo.Match.HomeScore := 2; (sì, è la modifica vista sopra).

Il rovescio della medaglia

Il comportamento del compilatore è tutt’altro che ottimale; anzi, potrebbe essere dovuto a un errore di progettazione. A priori, infatti, chi usa il record non è tenuto a conoscere i dettagli della sua implementazione interna né a sapere il modo in cui è stato implementato, se semplice o avanzato; non può sapere, a priori, se un certo tipo di record può essere usato come proprietà o meno. Su questo punto sarebbe auspicabile una maggior precisione del compilatore.

Nel prossimo articolo sul tipo record in Delphi vederemo anche altri aspetti delicati come l’uso dei record come parametri dei metodi o come valore di ritorno delle funzioni, e le funzioni RTTI.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *