Obiettivo
Definizione di un tipo dati che possa rappresentare anche i valori null, caratteristici nei database RDBMS, nei casi di colonne non inizializzate con alcun valore.
Generalità
Lavorando con i database, spesso ci si trova a dover gestire i valori NULL che troviamo in alcune colonne di una determinata tabella.
Le interpretazioni che si potrebbero dare a questo tipo di valore potrebbero essere diverse ma, quella che più rende l’idea, è che NULL rappresenta un “valore non definito” e, ciò che ci interessa a questo punto, è che dobbiamo gestirlo nei nostri programmi.
Spesso si tende a convertire il NULL con quello che potrebbe essere il valore nullo corrispondente a seconda del tipo: ad esempio stringa vuota per i campi carattere e 0 (zero) o -1 per i campi numerici.
Questo approccio non è proprio corretto e può portare a diversi problemi: uno su tutti, se abbiamo una stringa nulla da salvare nel database non sappiamo se salvarla come NULL o, appunto, come stringa nulla.
Si potrebbe usare il tipo Variant ma, nei sorgenti del Free Pascal, si trova definito il tipo TNullable. Si tratta di un advanced record – che ha subito diverse modifiche nel tempo – proveniente dal mondo Delphi e usato per testare la compatibilità del compilatore Free Pascal con Delphi.
Tralasciando storia e alternative, è comunque un buon approccio a questo tipo di problema nonché un modo di mettere in pratica diverse tecniche in una unica soluzione:
- sintassi avanzata dei record;
- tipi generici e specializzazioni.
Il sorgente di TNullable (attualmente lo si trova solo nei sorgenti della trunk, ma dovrebbe essere nella stable a partire dalla prossima versione di Lazarus 2.2.0) lo troviamo in:
lazarus/fpcsrc/packages/rtl-objpas/src/inc/nullable.pp
Mentre il progetto di test lo troviamo in:
lazarus/fpcsrc/tests/test/units/nullable/tnull.pp
unit
Andiamo ora ad analizzare come è stato realizzato:
unit nullable; {$mode objfpc} {$modeswitch advancedrecords}
- è attiva la modalità objfpc (modalità di default per i sorgenti Lazarus);
- è stato attivato lo switch advancedrecords, che permette l’utilizzo di funzioni e procedure anche nei tipi record;
interface
Nella sezione interface, troviamo la definizione del record come generic.
Il generic è stato utilizzato per evitare di dover definire un tipo nullable per ogni tipo nativo. Brevemente – e solo per analogia – possiamo dire che il generic sta ad una classe-con-metodi-astratti come la specializzazione (di un generic) sta a quella classe che poi implementerà i metodi astratti.
Più avanti, quando vedremo le specializzazioni sarà più chiaro.
Type { TNullable } generic TNullable<T> = record ... Public procedure Clear ... property HasValue: Boolean ... property IsNull: Boolean ... property Value: T ... property ValueOrDefault: T ... class function Empty: TMyType; static; class operator Initialize ... class operator Explicit(aValue: T): TMyType; class operator Explicit(aValue: TMyType): T; class operator := (aValue: T): TMyType; class operator := (aValue: TMyType): T; end;
Nella sezione public troviamo le definizioni ci interessano per l’utilizzo di questo tipo di record.
procedure Clear;
Invocando Clear, la variabile verrà impostata come valore nullo.
property HasValue: Boolean read FHasValue write SetHasValue;
Se la variabile è nulla, HasValue restituirà False; se invece è valorizzata, HasValue restituirà True.
property IsNull: Boolean read GetIsNull;
Se la variabile è nulla, IsNull restituirà True; se invece è valorizzata restituirà False.
property Value: T read GetValue write SetValue;
La proprietà Value verrà utilizzata per leggere il valore.
property ValueOrDefault: T read GetValueOrDefault;
La proprietà ValueOrDefault è utile quando la variabile non è valorizzata ma si vuole il valore di default del tipo specializzato.
class function Empty: TMyType; static;
Funzione del tipo (anche se la dichiarazione è class function) che restituisce un valore vuoto.
class operator Initialize(var aSelf : TNullable);
E’ il costruttore, serve ad in inizializzare la variabile.
In questo caso il codice (lo trovate nella sezione implementation) è:
aSelf.FHasValue:=False;
class operator Explicit(aValue: T): TmyType;
Definizione class operator nei casi di cast esplicito verso un tipo nullable.
Esempio:
Var A : specialize TNullable<String>; B : String; begin B:='ciao'; A:=specialize Tnullable<String>(B); // <- cast esplicito
class operator Explicit(aValue: TMyType): T;
Definizione class operator nei casi di cast esplicito verso un tipo.
Esempio:
Var A : specialize TNullable<String>; B : String; begin a.Value:='ciao'; B:=String(A); // <- cast esplicito
class operator := (aValue: T): TmyType;
Definizione class operator nei casi di assegnazione di un valore ad un tipo nullable specializzato.
Esempio:
Var A : specialize TNullable<String>; B : String; begin Result:=''; A.Value:=Val1; B:=A; // <- assegnazione da nullable specializzato a tipo
Specializzazione
La specializzazione si rende necessaria per poter utilizzare un generic mappandolo ad un tipo esistente.
Ad esempio, se volessimo un tipo nullable sia per gli interi che per le stringhe, potremmo dichiarare:
Type TNullableString = specialize TNullable<String>; TNullableInteger = specialize Tnullable<Integer>;
In alternativa, possiamo dichiarare una variabile nullable specializzata, direttamente nella sezione var senza dover definire un nuovo tipo specializzato.
Ad Esempio:
Var A : specialize TNullable<String>;
Conclusioni
La definizione di TNullable descritta qui è un concentrato di tecniche che si possono utilizzare con il pascal ad oggetti.
Non è solo una definizione accademica ma è anche e soprattutto una soluzione pratica ad un problema esistente con un approccio moderno.
Spero questo articolo sia stato utile e che possa dare spunto per fare sempre meglio.