Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione I metodi anonimi in Delphi

I metodi anonimi in Delphi

E-mail Stampa PDF

I metodi anonimi

I metodi anonimi sono stati introdotti con delphi 2010, si tratta di metodi definiti inline e non hanno un nome, per questo motivo sono chiamati "anonimi".
Facciamo un esempio semplice:

procedure
begin
writeln('ciao');
end

Come si può vedere diversamente dalle normali procedure non è presente il nome. Ma allora come possono richiamati nel corso del programma? Semplice sono chiamati tramite un riferimento, quindi dovremo associare il metodo anonimo ad almeno un riferimento:

//Dichiarazione riferimento;
var

MioMetodoAnonimo : TProc
//Assegnazione metodo anonimo al riferimento;
MioMetodoAnonimo:=procedure
begin
writeln('ciao');
end;
//Fuso della procedura tramite il riferimento
MioMetodoAnonimo;

Una piccola cosa che farei notare è come il punto e virgola alla fine del metodo anonimo non faccia parte della dichiarazione del metodo anonimo, infatti nel primo esempio non compariva,  mentre quello che vediamo nell'esempio precedente è dovuto all'istruzione di assegnazione ( MioMetodoAnonimo:= ... ;) .
La seconda cosa che dovremo spiegare meglio è il tipo di dato TProc che è stato usato per definire la variabile di riferimento. Nella unit SysUtils.pas è definito così:

type
TProc = reference to procedure;   

Questo tipo di dichiarazione è una novità per delphi e in pratica i metodi anonimi al posto d'essere dichiarati come "procedure" o "procedure of object" sono dichiarati come "reference to procedure", letteralmente riferimenti a procedure.

Naturalmente, il caso di una procedura senza alcun parametro è il caso più semplice, ma è possibile dichiarare procedure e funzioni con più parametri. Molti "prototipi" sono definiti in SysUtils.pas:

// Generic Anonymous method declarations
type

  TProc = reference to procedure;
  TProc<T> = reference to procedure (Arg1: T);
  TProc<T1,T2> = reference to procedure (Arg1: T1; Arg2: T2);
  TProc<T1,T2,T3> = reference to procedure (Arg1: T1; Arg2: T2; Arg3: T3);
  TProc<T1,T2,T3,T4> = reference to procedure (Arg1: T1; Arg2: T2; Arg3: T3; Arg4: T4);

TFunc<TResult> = reference to function: TResult;
  TFunc<T,TResult> = reference to function (Arg1: T): TResult;
  TFunc<T1,T2,TResult> = reference to function (Arg1: T1; Arg2: T2): TResult;
  TFunc<T1,T2,T3,TResult> = reference to function (Arg1: T1; Arg2: T2; Arg3: T3): TResult;
  TFunc<T1,T2,T3,T4,TResult> = reference to function (Arg1: T1; Arg2: T2; Arg3: T3; Arg4: T4): TResult;
TPredicate<T> = reference to function (Arg1: T): Boolean;

Quindi se una variabile viene dichiarata di tipo TFunc<T1,T2,TResult> il compilatore si aspetterà un assegnamento ad un metodo anonimo ma con due parametri ed un risultato del tipo dichiarato. Per capire meglio facciamo un esempio pratico:

//Dichiarazione riferimento;
var

ContaLettere : TFunc<string,integer>
...
//Assegnazione metodo anonimo al riferimento;
ContaLettere:=function(testo:string):integer
begin
result:=length(testo);
end; 
...
//Uso della procedura tramite il riferimento

writeln(ContaLettere('ciao')); //scrive 4 sul monitor

Il codice sarebbe del tutto equivalente se avessimo dichiarato del tipo di riferimento direttamente senza il prototipo. Infatti potrebbe capitare che i metodi abbiano più di quattro parametri, oppure si voglia descrivere meglio il tipo di riferimento specificando esplicitamente il nome dei parametri.

//Dichiarazione tipo di riferimento;
type
TContaLettere = reference to function(testo:string):integer;
//Dichiarazione variabile riferimento;
var
ContaLettere:TcontaLettere;
...
//Assegnazione metodo anonimo al riferimento;
ContaLettere:=function(testo:string):integer
begin
result:=length(testo);
end; 
...
//Uso della procedura tramite il riferimento

writeln(ContaLettere('ciao')); //scrive 4 sul monitor

Confronto con altri tipi di metodi

I metodi sono blocchi di codice, che possono essere riutilizzati più volte all'interno di un programma. Solitamente a questi blocchi di codice è associato un nome con il quale essi vengono invocati. I metodi si dividono in due categorie le funzioni, le quali restituiscono un risultato e le procedure che non restituiscono nulla.
I tipi di dati definiti come procedurali sono dei puntatori a questi blocchi di codice e possiamo dividerli in tre categorie:

  • Metodi tradizionali
  • Metodi nelle classi (regular methods)
  • Metodi anonimi

I metodi tradizionali sono le procedure e le funzioni che sono alla base della programmazione in pascal, esse possono essere assegnate a una variabile (variabili procedurali), vediamo un esempio:

//dichiarazione del tipo procedurale tradizionale
type
TMioMetodo = procedure(testo:string);

//definizione del metodo

procedure Scrivi(Testo:string);
begin
 writeln(testo);
end;
//dichiarazione della variabile procedurale
var
Mia:TMioMetodo;
begin
//assegnazione della variabile
Mia:=Scrivi;
//uso della variabile procedurale
Mia('ciao');
readln;
end.

Il secondo tipo di dato invece è specificatamente usato all'interno delle classi:

//dichiarazione del tipo procedurale regolare
type
TMioMetodo = procedure(testo:string) of object;

//dichiarazione del tipo di classe

type
TMiaClasse = class
public
procedure Scrivi(testo:string);
end;

//implementazione del metodo della classe
procedure TMiaClasse.Scrivi(testo:string);
begin
writeln(testo);
end;

//dichiarazione della variabile procedurale e dell'istanza della classe
var
Mio: TMioMetodo;
C: TMiaClasse;
begin
C:=TMiaClasse.create;

//assegnazione del metodo della classe alla variabile procedurale
  Mio := C.Scrivi;

//uso della variabile procedurale e quindi del metodo della classe
Mio('ciao');
readln;
end.

Solitamente sono usati per agire al contrario ovvero definire e implementare  un metodo esternamente alla classe, vedi funzioni di callback ed eventi.

La terza tipologia di metodi sono quelli anonimi, i quali si differenziano dai precedenti perché non hanno un nome e quindi per loro natura devono sempre essere collegati ad un riferimento, mentre per gli altri le variabili procedurali erano "opzionali" dato che un nome lo avevano.

//dichiarazione del tipo di riferimento procedurale
type
TMioMetodo = reference to procedure(testo:string);

//dichiarazione della variabile procedurale di riferimento

var
Mio:TMioMetodo;
begin
//implementazione e assegnazione del metodo anonimo
Mio:=procedure (Testo:string)
begin
  writeln(testo);
end; //assegnazione della variabile

//uso della variabile procedurale

Mio('ciao');
readln;
end.

La dichiarazione delle tre categorie di tipi procedurali sono diverse, ma il concetto che le accomuna è sempre lo stesso, cioè usare una variabile come puntatore al codice del metodo.
Nell'ultimo esempio vediamo come la variabile Mio si comporta come se fosse il nome del metodo anonimo, infatti alla fine per invocarlo basta l'istruzione "Mio('ciao');".

Uso dei metodi anonimi

Un metodo anonimo non ha un nome, quindi deve essere utilizzato con un riferimento. Tale riferimento può essere una variabile di cui il metodo è stato assegnato. Ecco un esempio.

type         
  TMioLog = reference to procedure(value: string);

TMiaClasse = class
public
procedure
Prova(Log:TMioLog);
end;

procedure
TMiaClasse.Prova(Log:TMioLog);
begin
writeln('prova metodo');
//log con il metodo anonimo
Log('log eseguito');
end;

var
MioLog: TMioLog;
MiaClasse: TMiaClasse;
begin
//Definizione metodo anonimo
MioLog:=procedure
begin
writeln(testo);
end;

MiaClasse:=TMiaClasse.create;

//invocazione del metodo della classe
//usando come argomento un riferimento al metodo anonimo

MiaClasse.Prova(MioLog);

readln;
end.

In questo modo è possibile definire esternamente funzioni da usare all'interno di una classe, e passarle poi come parametro. Naturalmente questo non è niente di innovativo dato che le funzioni di callback esistevano anche prima, ma la vera potenza dei metodi anonimi è che, non avendo la necessità di essere associati ad un nome, possono essere inseriti come parametri loro stessi. Riprendendo l'esempio precedente, eliminiamo la variabile procedurale usata come riferimento e usiamo il metodo direttamente come parametro, naturalmente rispettando la "forma" del metodo:

var
MiaClasse: TMiaClasse;
begin
MiaClasse:=TMiaClasse.create;
MiaClasse.Prova(procedure(testo:string)
begin
writeln(testo);
end);
readln;
end.

Per mostrare la versatilità dei metodi anonimi mostriamo come possono essere assegnati a dei metodi di una classe:

type
//dichiarazione del tipo procedurale
TMioLog = reference to procedure(value: string);

//dichiarazione della classe con un membro di tipo procedurale
TMiaClasse = class
public
LogProc:TMioLog; //in questo modo si dichiara un metodo come se fosse un membro
 constructor create(Log:TMioLog);
procedure Prova;
end;

//nella creazione il parametro procedurale è assegnato al membro della classe
constructor TMiaClasse.create(Log:TMioLog);
begin
LogProc:=Log;
end;

//una funzione usa al suo interno il metodo-membro LogProc
procedure TMiaClasse.Prova;
begin
writeln('prova metodo');
LogProc('log eseguito');
end;

var
MiaClasse: TMiaClasse;
begin
MiaClasse:=TMiaClasse.create(procedure(testo:string)
begin
writeln(testo);
end);
MiaClasse.Prova;
readln;
end.

Anche in questo caso si potevano usare i metodi normali, ma come si può notare i metodi anonimi sono molto versatili. Quello che è stato fatto è di dichiarare un membro di  tipo procedurale all'interno di una classe, a questo punto è possibile trattarlo, in quanto puntatore, come una qualsiasi dato e quindi è lecito assegnargli l'indirizzo di un blocco di codice, in questo caso anonimo. Questa tecnica ci permette di superare alcuni vincoli imposti dal compilatore riguardanti l'assegnazione diretta di un metodo anonimo ad un metodo di una classe, come spiegato in seguito.

Facciamo anche un esempio di come un metodo di una classe possa essere assegnato direttamente ad un riferimento per un metodo anonimo:

type
TMioTipo = reference to procedure(x: Integer);
TMiaClasse = class
procedure Prova(x: Integer);
end;

var
m: TMioTipo;
i: TMiaClasse;
begin
// ...
m := i.Method; //assegnazione di un metodo della classe ad un riferimento
end;

Ho fatto vedere questo esempio perché il caso contrario, cioè assegnare un riferimento di un metodo direttamente ad un puntatore ad un metodo di una classe (regular method), non è consentito. Il motivo è che i riferimenti sono type managed, mentre i puntatori a metodi regolari non lo sono, quindi per sicurezza questa operazione non viene permessa.
Questo è anche il motivo per cui non è possibile assegnare un metodo anonimo ad un evento, che è un tipo di metodo regolare.
Come abbiamo visto in precedenza per poter usare un metodo anonimo per un metodo di classe o un evento occorre dichiararlo come riferimento a un metodo anonimo.
Giusto per completezza facciamo un altro esempio che riguarda un evento. In Delphi gli eventi sono un particolare tipo di proprietà che viene dichiarata di tipo procedurale, per poter assegnare un metodo anonimo ad un evento sarà necessario dichiararla come riferimento ad un metodo:

type
TProc = reference to procedure;
TMioComponente = class(TComponent)
private
// Il membro procedurale è dichiarato come riferimento a una procedura
 FMioEvento: TProc;
public
// La proprietà è dichiarata come riferimento a una procedura
property MioEvento: TProc read FMioEvento write FMioEvento;
// funzione di test per l'evento
procedure Prova;
end;

procedure TMioComponente.Prova;
begin
if Assigned(FMioEvento) then FMioEvento;
end;

var
c: TMioComponente;
begin
c := TMioComponente.Create(Self);
//assegnazione del metodo anonimo all'evento
c.MioEvent := procedure
begin
ShowMessage('OK evento!'); // mostra che l'evento funziona
end;

//test sull'evento della funzione
c.Prova;
readln;
end.

 

Le chiusure in Delphi

Grazie ai metodi anonimi è possibile creare delle chiusure anche in Delphi. Non che se ne sentisse il bisogno...ma vediamo un semplice esempio:

type
TMioContatore = reference to function:integer;

//definizione di una funzione che implementa il metodo anonimo contatore
function CreaContatore(x:integer):TMioContatore;
begin
result:=function():integer
begin
x:=x+1;
result:=x;
end;
end;

var
//dichiarazioen della varibile procedurale che farà da riferimento per il metodo anonimo
c: TMioContatore;
begin
//assegnazione del metodo anonimo al riferimento
c:=CreaContatore(0);

//uso del riferimento e quindi del metodo anonimo
writeln(c); //scrive 1
writeln(c); //scrive 2

 readln;
end.

Prima di proseguire è meglio chiarire alcuni dettagli che potrebbero creare un po' di confusione: le parentesi tonde devono esplicitamente essere usate anche se non è presente alcun parametro.

result:=function():integer

La stessa cosa vale anche per il punto e virgola mancante dopo il tipo di dato restituito.

Riprendendo il discorso, il modo di usare un metodo anonimo è la stessa delle altre volte, tranne che per un dettaglio: tramite il metodo anonimo viene conservato il valore della variabile x anche al di fuori del suo normale raggio d'azione. In questo caso si dice che la funzione interna è chiusa oltre il campo di azione che la contiene, o più brevemente è una chiusura.
Vediamo di spiegare il codice passo passo, in modo da rendere più comprensibile, quello che per alcuni potrebbe sembrare complicato.
come prima cosa incontriamo la dichiarazione del tipo di riferimento, che deve essere rispettato dalla variabile procedurale e dal valore restituito dalla funzione di inizializzazione.

type
TMioContatore = reference to function:integer;

In seguito abbiamo la funzione a cui abbiamo affidato il compito di inizializzare il contatore, e che in realtà ha il compito ben più importante di implementare al suo interno il metodo anonimo da fornire come risultato.

  function CreaContatore(x:integer):TMioContatore;
begin
result:=function():integer
begin
x:=x+1;
result:=x;
end;
end;

Quello che bisogna capire è che al momento in cui viene invocata questa funzione non viene assolutamente eseguito il codice interno al metodo anonimo. Le uniche operazioni eseguite sono:

  • Memorizzare il valore attuale del parametro passato associato al puntatore restituito
  • Restituire un puntatore al metodo anonimo

In pratica la parte in rosso è un blocco di codice che corrisponderà ad un oggetto, il quale verrà creato e restituito come risultato della funzione.
Questo oggetto è dotato di un reference counter, che al momento della sua creazione viene inizializzato ad uno. Ogni volta che il metodo viene assegnato ad una variabile procedurale questo contatore viene incrementato.
In definitiva possiamo separare le varie parti che entrano in gioco quando si usa un metodo anonimo:

  1. La variabile procedurale, che non è altro che un puntatore all'oggetto.
  2. L'oggetto creato a runtime che contiene il contatore dei riferimenti ad esso, i valori dei parametri usati per la sua creazione e un puntatore alla sua parte funzionale. Si può dire che questa sia la parte variabile del metodo anonimo, dato che i dati al suo interno possono cambiare durante il programma e rappresenta l'istanza del metodo anonimo, infatti ogni volta che il metodo viene creato ve ne sarà una diversa.
  3. La parte funzionale dell'oggetto con la quale potremo raggruppare i metodi con i quali è gestito oggetto e il codice vero e proprio del metodo anonimo.Questa parte non cambia e sarà la stessa per tutte le istanze del metodo anonimo.

Come infatti si può immaginare questo tipo di oggetto non è un semplice blocco di codice, al suo interno sono compresi il corpo vero e proprio del metodo anonimo, ma anche i metodi ereditati dalla classe TInterfaceObject; per esempio nel caso precedente i metodi dell'oggetto sono:

  • CreaContatore$ActRec.$0$Body
  • TInterfacedObject.QueryInterface
  • TInterfacedObject._AddRef
  • TInterfacedObject._Release

naturalmente tutte queste cose sono prodotte dal compilatore e la gestione dei metodi anonimi è nascosta al programmatore.

Proseguendo nella spiegazione dell'esempio precedente, troviamo la dichiarazione della variabile di riferimento al metodo, la quale deve essere dello stesso tipo del metodo anonimo restituito dalla funzione di inizializzazione:

var
c: TMioContatore;

A questo punto è possibile usare la funzione di inizializzazione per creare il metodo anonimo e assegnareil puntatore a questo oggetto alla variabile di riferimento:

  c:=CreaContatore(0);

In questo caso il contatore di riferimento all'interno dell'oggetto sarà pari a due.
Successivamente è possibile usare usare la variabile di riferimento per invocare il metodo anonimo:

  writeln(c);   //scrive 1
writeln(c); //scrive 2

questo a prima vista potrebbe trarre in inganno, perché si sta cercando di stampare a video la variabile di riferimento, ma in realtà si tratta di funzioni annidate; per capire meglio:

var
i: integer;
begin
c:=CreaContatore(0);
i:=c;
writeln(i);

Quello che viene stampato è l'intero che viene restituito dal metodo anonimo.

Il fatto che ogni volta che viene invocato il metodo anonimo, tramite la variabile di riferimento c, il risultato è sempre diverso, dipende dalla variabile x, la quale viene incrementata dopo ogni chiamata.
La variabile x, passata come parametro, persiste in memoria perché conservata nell'oggetto che rappresenta il metodo anonimo. In generale possiamo dire che quando all'interno del metodo anonimo viene utilizzata una variabile esterna al metodo questa viene "catturata" dall'oggetto.
Catturare una variabile significa estenderne il tempo di vita oltre i limiti del suo campo di azione naturale. Infatti la vita di una variabile locale dovrebbe terminare al termine della funzione che la ha utilizzata. Ad esempio, la vita della variabile x sarebbe dovuta terminare dopo il primo uso della funzione CreaContatore, ma, essendo stata catturata, è usata fino a che l'oggetto del metodo anonimo non viene rilasciato. 
Facciamo un esempio di variabile locale catturata dal metodo anonimo:

  function CreaContatore():TMioContatore;
var
y:integer;
begin
y:=0;
result:=function():integer
begin
y:=y+1;
result:=y;
end;
end;

In questo caso il contatore si auto-inizializza a zero al momento della creazione, per il resto funziona come prima.
Per dimostrare quanto detto fino ad ora usiamo due riferimenti per usare il metodo anonimo e le se istanze:

var
c,h: TMioContatore;
begin
CreaContatore;
//crea una istanza del metodo anonimo e la assegna a c
c:=CreaContatore();
//assegna a h la stessa istanza puntata da c
h:=c;
writeln(c); //stampa 1
writeln(c); //stampa 2
writeln(h); //stampa 3

//crea una nuova istanza e la assegna a c
c:=CreaContatore();
writeln(c); //stampa 1
writeln(h); //stampa 4

readln;
end.

Per concludere, sinceramente per motivi di compatibilità, sconsiglierei di usare i metodi anonimi, ma se proprio fosse necessario farlo spero abbiate capito come usarli.


Ultimo aggiornamento ( Sabato 31 Luglio 2010 10:19 )  
Loading

Login




Chiudi