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):
Classi | Record | |
Sono puntatori? | Sì | No |
Ereditarietà | Sì | No |
Può implementare un’interfaccia | Sì | No |
Helpers | Sì | Sì |
Visibilità | Sì | Sì |
Metodi | Sì | Sì |
Campi | Sì | Sì |
Proprietà | Sì | Sì |
Costanti | Sì | Sì |
Tipi | Sì | Sì |
Overloading degli operatori | No | Sì |
Variant | No | Sì/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.