in effetti è così, quando estraggo il valore, so già di che tipo si tratta :)
è come quando hai un integer: sai che è un integer!
per questo avrò una proprietà per ogni tipo, ad esempio:
property Integers[AName : String] : TMaizAssignable_Integer Read GetIntegers Write SetIntegers;
property Strings[AName : String] : TMaizAssignable_String read GetStrings write SetStrings;
in questo modo posso cercare nella hash list (che è unica per tutti i tipi di valori) e poi assegnare il tipo giusto nel Getter
è una alternativa a creare un tipo record statico, ad esempio, per ogni tabella di un database
io ho diverse tabelle che superano i 100 campi...
avrei potuto generare i tipi record ma ho bisogno di adattarmi a qualunque tipo di tabella senza conoscerne la definizione
così perdo un po' in prestazioni (ma poco) ma ci guadagno in dinamismo
Edit:
TMaizAssignable è una mia versione di TNullable
in effetti è così, quando estraggo il valore, so già di che tipo si tratta :)
è come quando hai un integer: sai che è un integer!
Bhè, si è no ..... quando tu scrivi:
WriteLn('one = ', PNullString(HashList.Find('one'))^.Value);
istruisci al compilatore a fare una cosa ben precisa ... deferenzi in maniera rigida una "risposta" il cui risultato non è noto al compilatore (il riferimento di "HashList.Find('one'))^.Value" non può esserti noto a priori in generale).
Lo sarebbe se tu avessi un qualcosa del tipo: "HashList.Find('one'))^.ValueString" o "HashList.Find('one'))^.ValueInteger" ad esempio.
Inoltre, per l'uso delle RTTI dovresti comunque implementare nel codice del TNullable un qualcosa.
In generale, vorrei ricordare che ereditarietà, interfacce e altro con i record non si possono usare.
Ciao
EDIT: giusto per essere più chiaro, il tuo chiamamolo cast PNullString(...) non è dinamico, ma statico. Il compilatore sia aspetta di trovare quella referenza. Quindi in generale và bene avere il gettere e il setter, ma non è sufficiente. Se devi leggere un tipo di valore generico, non puoi aspettarti un tipo di valore noto, a meno che non istruisci il "getter" a darti quel tipo preciso di valore (ad esempio come fanno gli HELPER .toString .toInteger .... etc) ....
istruisci al compilatore a fare una cosa ben precisa ...
infatti: so già che cosa mi torna indietro e va bene così
il codice sotto
var i: integer
begin
i := 5;
writeln('intero I =', i);
end;
voglio scriverlo così
var ms: TMemoryspace;
begin
ms:=TMemoryspace.Create;
ms.Integers['i'] := 5;
writeln('intero I =',ms.Integers['i']);
ms.Free;
end;
solo che in più ho gestiti anche i valori null
.... ho fallito con Lazarus ... :-[ :-\ :'(
negli operators overload (class operator), c'è distinzione tra i parametri ma non tra i "risultati". In questo modo non è possibile definire l'overload dello stesso operatore, a meno di non definire più metodi distinti.
Ciò che volevo fare con il TNullable, in maniera semplice ed efficace non sembrerebbe possibile, almeno per lo mie skill con Lazarus.
D'altronde non è un caso se il NULL è gestito con un variant. I "Generic" hanno necessità di essere ancora sviluppati.
Ad esempio, il seguente codice non viene compilato, perchè per FPC le due implementazioni sono duplicate:
class operator TNullable.:= (aValue: TMyType): T;
begin
........
end;
class operator TNullable.:= (aValue: TMyType): Variant;
begin
.........
end;
Parcheggiato per adesso il progetto, proverò con FPC 3.4 ....
Ciao
l'idea era buona, peccato non si possa fare
poi bisognava vedere come si comportava una variabile di questo tipo in una espressione di calcolo o concatenazione tra stringhe
però l'idea era buona sul serio e probabilmente il cast implicito al variant avrebbe funzionato a dovere
mi viene in mente un workaround non proprio politically-correct :D
invece di usare i variant si potrebbero definire delle costanti con valori predefiniti che rappresentano il null
ad esempio:
const
NULLSTRING = '{NULL}';
NULLINT = high(integer);
NULLDATE = 0;
per cui si potrebbe scrivere:
var
s: TNullableString;
begin
s := NULLSTRING;
end;
l'assegnazione di NULLSTRING dovrebbe richiamare internamente il .Clear
poi, in una espressione o concatenazione, grazie agli operator overload, si potrebbe generare una eccezione
non è il massimo come cosa da implementare, ma si tratterebbe di poter definire delle costanti che scatenano l'assegnazione del NULL
::)
comunque, ora che ci penso, si può sempre fare qualcosa del genere
var
NullString, // questa variabile è destinata a rimanere sempre a null (non va mai assegnata)
s: TNullableString;
begin
// per dare un valore
s := 'ciao mondo';
// per impostare a null
s := NullString;
end;
La funzione, teorica, del Nullable sarebbe quella di contenere due "stati" essenziali: uno è il tipo "specializzato" e l'altro lo stato di NULL o non assegnato.
Indipendentemente dalla rappresentazione interna, il problema concettuale è la sua rappresentazione esterna (assegnazione LEFT <-> RIGHT)
In questo momento ciò che può essere NULL, è rappresentato da una variant. Ed è su questo che ci si deve confrontare.
Sino a quando il compilatore non supporterà integralmente i GENERIC, e in particolare i record managed, non potranno esserci scorciatoie allo stato di fatto. Anche l'RTTI (definito sperimentale) andrebbe implementato.
Se io assegno a un NullableInteger.Value un Intero o un NULL (lasciando perdere per un attimo la conversione da altri formati come double) il "modo" di assegnazione dovrebbe essere sempre quello:
//Esempio, lasciando perdere il metodo di assegnazione (implicito o esplicito)
NullableInteger.Value := 1;
NullableInteger.Value := NULL;
ciò perchè non è noto in generale se il dato ricevuto sarà un intero o un NULL (ad esempio supponiamo che invece di una costante riceviamo l'input da un campo di un database).
Idem quando il valore viene "emesso".
L'alternativa c'è ovviamente, ma è assolutamente "grezza", inefficiente e priva di significato: basta avere due metodi di ingresso e due metodi di "uscita" e uso uno o l'altro in funzione di quello che è il valore effettivo come nell'esempio qui sotto
//è solo esemplificativo !!!!
if Valore = NULL then
NullableInteger.ValueNull := Valore
else
NullableInteger.Value := Integer(Valore);
Con FPC 3.3.1 le cose non cambiano. Devo provare FPC 3.4.
Tanto per dare l'idea, questo è quello che volevo fare (stralcio), e che ho fatto da altre parti ....
class operator Implicit(const Value: TNullable<T>): T;
class operator Implicit(const Value: TNullable<T>): Variant;
class operator Implicit(const Value: Pointer): TNullable<T>;
class operator Implicit(const Value: T): TNullable<T>;
class operator Implicit(const Value: Variant): TNullable<T>;
// set values
nullstr:=new(PNullString);
nullstr^ := 'value 1';
HashList.Add('one', nullstr);
nullint:=new(PNullInteger);
nullint^:=2;
HashList.Add('two', nullint);
nullint:=new(PNullInteger);
nullint^:= NULL;
HashList.Add('three', nullint);
RICORDO A TUTTI CHE LAZARUS si pone come linguaggio universale su tutte le piattaforme, e quindi per affrontare tali problematiche occorre una visione globale che molto spesso noi utenti non abbiamo. Gli sviluppatori invece hanno una sensibilità verso questi aspetti e quindi non propongono sicuramente una soluzione senza vagliare pro / contro a livello globale (codice esistente, piattaforme, impatto prestazionale)
Ciao ciao
L'alternativa c'è ovviamente, ma è assolutamente "grezza", inefficiente e priva di significato: basta avere due metodi di ingresso e due metodi di "uscita" e uso uno o l'altro in funzione di quello che è il valore effettivo come nell'esempio qui sotto
//è solo esemplificativo !!!!
if Valore = NULL then
NullableInteger.ValueNull := Valore
else
NullableInteger.Value := Integer(Valore);
infatti questo non avrebbe senso, ma i campi dei database hanno .AsVariant quindi questo si potrebbe già fare
var
s: TNullableString;
begin
...
s.AsVariant := Table1.FieldByName('campo1').AsVariant;
...
end;
Edit:
però torniamo sempre li, il fatto è che
Table1.FieldByName('campo1').AsInteger non varrà mai null
dovremmo invece avere
Table1.FieldByName('campo1').AsNullableInteger
Edit2:
penso che potrei provare un class Helper per TIntegerField...
appena posso provo
In Delphi quello che ho abbozzato funziona, ci sono ancora alcuni dettagli da definire (l'avevo abbozzato per portarlo su Lazarus).
Anche in Delphi il compilatore non è propriamente compatibile al 100% con i Generic, tanto è vero che i Nullable non sono supportati ufficialmente e secondo una nota di Embarcadero ci stanno lavorando.
Richiedere il porting sicuramente sarebbe da fare, fermo restando però che l'aspetto principale che è l'overloading completo (parametri dei metodi e valore di ritorno) Lazarus non c'è l'ha.
Pensa che ho già implementato l'ingresso di un dato variant e la sua conversione nel formato <T>, compreso il NULL, grazie alle RTTI.
Di fatto è un casting, ma comunque un casting a codice non a compilazione (ovviamente, essendo il dato di ingresso non specificato).
La compatibilità che chiederemmo secondo me è troppo precoce, e forse è già stata richiesta.
P.S.: ho aggiornato il tuo codice con qualcosa di più moderno, c'è pure l'enumerazione ;D
var nullint: PNullInteger;
nullstr: PNullString;
i: integer;
//HashList: TFPHashList;
HashList: specialize TDictionary<string, pointer>;
HashEl: specialize TPair<string, pointer>;
begin
WriteLn('Start');
//HashList:=TFPHashList.Create;
HashList := specialize TDictionary<string, pointer>.Create;
try
// set values
nullstr:=new(PNullString);
nullstr^ := 'value 1';
HashList.Add('one', nullstr);
nullint:=new(PNullInteger);
nullint^:=2;
HashList.Add('two', nullint);
nullint:=new(PNullInteger);
nullint^:= 5;
HashList.Add('three', nullint);
// write
//WriteLn('one = ', PNullString(HashList.Find('one'))^.Value);
//WriteLn('two = ', PNullInteger(HashList.Find('two'))^ .Value);
//WriteLn('three = ', PNullInteger(HashList.Find('three'))^ .Value);
WriteLn('one = ', PNullString(HashList.Items['one'])^.Value);
WriteLn('two = ', PNullInteger(HashList.Items['two'])^ .Value);
WriteLn('three = ', PNullInteger(HashList.Items['three'])^.Value);
// free values
//for i := 0 to HashList.Count-1 do begin
i := 0;
for HashEl in HashList do begin
try
WriteLn('Dispose ' + i.ToString);
// Dispose( PNullString(HashList[i]) );
//FreeMem(HashList[i]);
FreeMem(HashEl.Value);
except
on e: exception do begin
WriteLn('Error during sispose (' + i.ToString + '):' + e.Message);
end;
end;
inc(i);
end;
finally
// free resources
FreeAndNil(HashList);
WriteLn('Stop');
end;
end;
però torniamo sempre li, il fatto è che
Table1.FieldByName('campo1').AsInteger non varrà mai null
dovremmo invece avere
Table1.FieldByName('campo1').AsNullableInteger
Secondo me, lo cose devono restare come sono: perchè in assegnazione devi "castare" il dato .... lascialo nativo....
Table1.FieldByName('campo1').Value := NullInt; <- Può essere valorizzato tranquillamente come NULL o Integer
EDIT: è da qui che ci deve essere il supporto dei Generic agli overload completi e del compilatore.
Ciao
Esempio:
nullstr:=new(PNullString);
nullstr^ := 'value 1';
HashList.Add('one', nullstr);
nullint:=new(PNullInteger);
nullint^:=2;
HashList.Add('two', nullint);
nullint:=new(PNullInteger);
nullint^:= NULL;
HashList.Add('three', nullint);
// write
WriteLn('one = ', TNullString(HashList.Items['one']).Value);
WriteLn('two = ', PNullInteger(HashList.Items['two'])^ .Value);
WriteLn('three = ', TNullInteger(HashList.Items['three']).Value); <- Qui il risultato dà 0 perchè WriteLn lo trasforma in 0
testvariant := NullInt^;
WriteLn('three is NULL ? -> ', testvariant = NULL); <- qui viene testato invece che il valore NULL sia effettivamente "passato"
nullint^:= 18;
WriteLn('Ora invece è -> ', TNullInteger(NullInt).Value);
quindi lo hai finito con i variant in Delphi?
il risultato è buono :)
cosa esce in output con?
WriteLn('three = ', TNullInteger(HashList.Items['three']).Value = NULL);
Aggiornamento:
eh eh, non avevo pensato al compilatore .... alle volte è contorto ....
Nella forma:
TNullableInt(.....).Value = .......
Il maledetto converte l'espressione di destra nello stesso tipo di quella di sinistra .... :o :o :o
EDIT: e la conversione avviene come RECORD MANAGED dello stesso tipo <T> tramite il metodo CREATE e tutte le funnzioni annesse.
Quindi bisogna fare attenzione a quello che si fà, perchè viene usato direttamente dal compilatore....
Ora siamo a posto. La situazione comunque non è perfettamente omogenea.
Questo perchè non ci sono limiti alla fantasia e alla perversione, come insegna @nomorelogic ( :P ).
Se devo usare i Nullable, DEVO ESSERE CERTO che chi riceve i dati (ad esempio una variabile, una funzione un ...... GESTISCA CORRETTAMENTE I NULL !!!! (ovviamente ammesso che usi i NULL).
In pratica se ciò che esce dal Nullable non è gestibile dal ricevente .... verrà generato un errore runtime.
Questo è il caso di Writeln che dei NULL proprio non ne vuole sapere.
Per fare ciò sono ricorso ad uno stratagemma ... usare una chiamata differenziata così:
WriteLn('three = ', PNullInteger(HashList.Items['three'])^.GetValueOrDefault);
Il Nullable riporterà o il valore corretto, o nel caso di NULL il valore di default del tipo.
Ora si puà gestire correttamente il metodo implicito. Con i risultati attesi.
WriteLn('three is NULL ? -> ', PNullInteger(HashList.Items['three'])^ = NULL);
In pratica, la proprietà "Value" del NULLABLE non si dovrebbe mai usare (dovrei implementare le EXPLICIT, ma di fatto non ha molto senso non potendo comunque generare con il NULL un valore diverso da ..... NULL ;D )
Le Explicit avrebbero senso per consentire una conversione gestita invece che lasciare fare al compilatore.
Ma va al di fuori delle funzionalità del NULLABLE.
Unico problema ... funziona in Delphi ... fà uso di RTTI ...
Ciao
Alcune news:
buone notizie x Lazarus, e non proprio buonissime x Delphi.
La matematica per i generic come un TNullable<T> non viene supportata nativamente da Delphi, che lascia spazio alle costruzioni di interfacce per sviluppare gli algoritimi applicativi del caso. Questa è la soluzione più flessibile ma anche la più dispendiosa in termini di energie, oltre al possibile impegno per il mantenimento. Inoltre, non essendo un supporto diretto a livello di compilazione (si usano le interfacce), sofrirebbe sivuramente in termini prestazionali.
Invece FPC supporta la matematica nei generic TNullable<T> (non sò fino a che livello) e quindi risulta più semplice applicarli.
Emb stà "studiando" la situazione.
Esempi:
type
TNullInteger: TNullable<integer>;
var
NullInt: TNullInteger;
NullInt2: TNullInteger;
A: varinant;
//Assegnazioni: OK
A := unassigned;
NullInt := NULL;
NullInt2 := 10;
NullInt := NullInt2;
NullInt := unassigned;
NullInt2 := A;
A := 50;
NullInt := A + 10;
//Le operazioni (nativamente):
NullInt := NullInt +10; //DELPHI NO, LAZARUS SI
//Comparazioni
(NullInt = NullInt2);
(A = NullInt)
(NullInt = A)
(NullInt2 = 10)
(NullInt2 = NULL)