Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Le Classi con Delphi

Le Classi con Delphi

E-mail Stampa PDF

L'object pascal è uno dei tanti linguaggi che si basa sulla programmazione ad oggetti il cui elemento fondamentale sono le classi. In questo articolo cercheremo di approfondire meglio come il compilatore delphi implementa questi oggetti.
Mi scuso in anticipo se la trattazione di questo argomento ho deciso di iniziarla in maniera un po' complesso, ma questo articolo è rivolto ai programmatori che hanno già idea di cosa sia una classe e la programmazione ad oggetti e contiene delle informazioni fondamentali che sono la base per una buona programmazione e che molti programmatori spesso ignorano.

Come prima cosa scriviamo un piccolissimo programma di tipo console:

program prova1;

{$APPTYPE CONSOLE}

uses
SysUtils,
UnitClass1 in 'UnitClass1.pas';

var
A:classA; //dichiarazione della variabile A di tipo ClassA

begin
A:=classA.Create; //Creazione della classe
A.campo1:=3;
A.procedura1;
readln;
end.

Creiamo una istanza di una classe (classA) prima dichiarandola come variabile e poi invocando il costruttore. Dopo di che impostiamo una sua variabile (campo1=3) e ne uso la procedura1.

Vediamo in cosa consiste la classA nella unit UnitClass1:

unit UnitClass1;

interface

type
classA = class
campo1:integer;
campo2:double;
procedure procedura1;
procedure procedura2;
end;

implementation

{ classA }

procedure classA.procedura1;
begin
writeln('messaggio 1');
end;

procedure classA.procedura2;
begin
writeln('messaggio 2');
end;

end.

La classe di tipo classA è composta da 2 variabili e da 2 procedure. Ma quello che potrebbe sorprenderci è dove sta la funzione create che abbiamo usato per creare la classe?  In delphi tutte le classi discendono dalla classe TObject che ha al suo interno questa funzione e quindi quando dichiariamo il tipo di classe usando la direttiva class l'ereditarietà dal tipo TObject è implicita:

classA = class 

è equivalente a :

 classA = class(TObject)

L'eredità è una delle tante caratteristiche della programmazione ad oggetti, che ci permette di riutilizzare parti di codice scritte per altri classi semplicemente ereditandone i campi (costanti e variabili) e i metodi (funzioni o procedure). Per ereditare una classe basta includerla tra le parentesi tonde a fianco alla direttiva class, non è permessa l'eredità multipla, perciò sarà possibile ereditare solo una classe.

Vediamo un po' cosa accade al codice compilato:

//A:=classA.Create;
0040914C B201             mov dl,$01
0040914E A1648B4000       mov eax,[$00408b64]       ;eax = 00408BB0 = indirizzo dove si trova il codice della classe
00409153 E840A9FFFF       call 00403A98             ;TObject.Create viene richiamato il costruttore della classe genitrice

;Dopo la creazione della classe
; eax = 00A1C278 = indirizzo dove si trovano i dati della classe
//A.campo1:=3;
00409158 C7400403000000   mov [eax+$04],$00000003   ; eax+4 indica la prima variabile interna alla classe

//A.procedura1;
0040915F E854FAFFFF       call 00408BB8             ;classA.procedura1     
//readln;
00409164 A15CAA4000       mov eax,[$0040aa5c]
00409169 E882A2FFFF       call @ReadLn
0040916E E8699CFFFF       call @_IOTest
//end.
00409173 E8D8B2FFFF       call @Halt0

La ClasseA in memoria

La classe di fatto viene divisa in due parti, una dove risiede il codice dei suoi metodi e che esiste dal momento della sua compilazione, un'altra dove invece risiedono i suoi dati, cioè le sue variabili, che invece necessitano di essere allocati con il suo costruttore.
La prima risiede nell'area di memoria con il resto del codice (sezione .text), l'altra in un altra area dove vengono allocati i dati a runtime dalla applicazione.

//ClassA - area dati 
Indirizzo   Codice hex               
00A1C278    B0 8B 40 00               // = $00408BB0  indirizzo dove si trova il codice della classeA  
00A1C27C    00 00 00 00               // <- campo 1
00A1C280    00 00 00 00 00 00 00 00   // <- campo 2

//ClassA - area codice
Indirizzo   Codice hex                          Dati o istruzioni               Commenti  
 
00408B8C   0C104000      0040100C          ; puntatore a Tobject
00408B90   B43B4000       00403BB4          ; puntatore a Tobject.SafeCallException
00408B94   C03B4000      00403BC0          ; puntatore a Tobject.AfterConstrunction
00408B98   C43B4000      00403BC4          ; puntatore a Tobject.BeforeDestruction
00408B9C   C83B4000      00403BC8          ; puntatore a Tobject.Dispatch
00408BA0   BC3B4000      00403BBC         ; puntatore a Tobject.DefaultHandler
00408BA4   603A4000      00403A60            ; puntatore a Tobject.NewInstance
00408BA8   7C3A4000      00403A7C          ; puntatore a Tobject.FreeInstance
00408BAC   B83A4000      00403AB8         ; puntatore al Destructor (TObject.Destroy)

00408BB0    06                                                                  ;Dimensione nome
00408BB1    63 6C 61 73 73 41                   ASCII "classA"                  ;Nome classe
00408BB7    90                                  NOP
00408BB8    A1 F0A94000                         MOV EAX,DWORD PTR DS:[40A9F0]   ; prova1.00408BB8(guessed void)
00408BBD    BA DC8B4000                         MOV EDX,prova1.00408BDC         ; ASCII "messaggio 1"
00408BC2    E8 95BDFFFF                         CALL 0040495C
00408BC7    E8 3CAAFFFF                         CALL 00403608                   ; [prova1.00403608
00408BCC    E8 0BA2FFFF                         CALL 00402DDC                   ; [prova1.00402DDC
00408BD1    C3                                  RETN

00408BD2    00                                  DB 00
00408BD3    00                                  DB 00
00408BD4    FFFFFFFF                            DD FFFFFFFF
00408BD8    0B000000                            DD 0000000B
00408BDC    6D 65 73 73 61 67 67 69 6F 20 31    ASCII "messaggio 1"
00408BE7    00                                  DB 00
00408BE8    A1 F0A94000                         MOV EAX,DWORD PTR DS:[40A9F0]   ; prova1.00408BE8(guessed void)
00408BED    BA 0C8C4000                         MOV EDX,prova1.00408C0C         ; ASCII "messaggio 2"
00408BF2    E8 65BDFFFF                         CALL 0040495C
00408BF7    E8 0CAAFFFF                         CALL 00403608                   ; [prova1.00403608
00408BFC    E8 DBA1FFFF                         CALL 00402DDC                   ; [prova1.00402DDC
00408C01    C3                                  RETN

00408C02    00                                  DB 00
00408C03    00                                  DB 00
00408C04    FFFFFFFF                            DD FFFFFFFF
00408C08    0B000000                            DD 0000000B
00408C0C    6D 65 73 73 61 67 67 69 6F 20 32    ASCII "messaggio 2"

Nell'area dove si trova il codice sono stati messi in evidenza con colori diversi le parti di codice relative alle due procedure. Mentre la parte iniziale è costituita dalla vmt (virtual method table) con gli indirizzi di alcune dei metodi (virtual) ereditati dall'oggetto TObject.

Vediamo di chiarire meglio qualche concetto:

La classe si divide in 2 parti:

  • Dati: Questa parte non esiste al momento della compilazione, ma è nota la sua struttura, quindi si farà riferimento ad essa tramite offset.
    Per creare questa parte dovremo allocare della memoria e tutto questo avviene tramite il costruttore della classe. Nel case precedente il tutto è avvenuto in maniera implicita grazie alla classe TObject da cui discendono tutte le classi.
  • Codice: Questa parte esiste al momento della compilazione e viene collocata insieme a tutto il resto del codice relativo all'applicazione (solitamente nella sezione .text).

Ora verrebbe da chiedersi, ma se la classe non contenesse dati ci sarebbe ugualmente il bisogno di "crearla"? Oppure potremo accedere ai suoi metodi direttamente?
La risposta è si. Potremo accedere ai suoi metodi senza creare la classe, dato che sono stati definiti staticamente e la loro posizione è nota al compilatore.
Se avessimo definito il metodo dynamic o virtual allora non avremo potuto usare i metodi senza creare la classe dato che il compilatore ha l'ordine di non usare il loro indirizzo, ma lo vedremo in seguito.
Quello che possiamo notare è che il compilatore nella chiamata al metodo procedura1 inserisce l'esatto indirizzo del codice della procedura e per questo motivo che vengono definiti metodi static.

Direttiva virtual e dynamic

Se definiamo le procedure come virtual:

classA = class
procedure procedura1; virtual;
procedure procedura2; virtual;
end;

Se proviamo a chiamare queste funzioni:

                                                    ; ebx contiene l'indirizzo della classe (00A14E40) o meglio della sua parte dati
; all'inizio della parte dati c'è l'indirizzo della parte codice
// A.procedura1;

0040915B 8BC3             mov eax,ebx ; eax = A14E40 contiene l'indirizzo della prima parte del codice della classA
0040915D 8B10             mov edx,[eax] ; edx = 408BB0 contiene il primo elemento della vtable = indirizzo procedura1
0040915F FF12             call dword ptr [edx] ; chiama procedura1 = 00408BC0
// A.procedura2;
00409161 8BC3             mov eax,ebx ; eax = indirizzo parte codice della classe
00409163 8B10             mov edx,[eax] ; edx = indirizzo primo elemento della vtable
00409165 FF5204           call dword ptr [edx+$04] ; chiama la procerura2 = secondo elamento della vtable

Il compilatore quindi, al posto di inserire gli indirizzi dell'inizio dei blocchi di codice contenenti le istruzioni delle due funzioni, fa riferimento ad una tabella chiamata virtual method table e che si trova all'inizio della area di memoria riservata al codice della classe.
Quindi i metodi che non vengono dichiarati esplicitamente virtual vengono considerati static e nelle chiamate alla funzione viene indicato direttamente il loro indirizzo, mentre per quelli virtual il compilatore inserisce l'indirizzo dell'elemento della VMT relativo alla funzione.

//ClassA - area codice
//VTable
Indirizzo   Codice hex

... ; altri elementi della vtable relativi ai metodi ereditati da TObject

00408BB0   C08B4000      ; = 00408BC0 = indirizzo procedura1
00408BB4   F08B4000      ; = 00408BF0 = indirizzo procedura2
... ;Il resto della classA è identica a prima

Per i metodi invece dichiarati come dynamic il meccanismo è ancora diverso, infatti per ottenere l'indirizzo del metodo prescelto verrà utilizzata la funzione CallDynaInst che a sua volta utilizza la funzione GetDynaMethod scritta in assembler definita come segue:

function GetDynaMethod(vmt: TClass; selector: Smallint) : Pointer;       

dove col registro EAX verrà passato l'indirizzo della VMT della classe, col registro SI l'indice del metodo dinamico; mentre il risultato, cioè il puntatore al codice del metodo viene restituito sul registro ESI e il flag ZF sarà  uguale a 0 se il metodo viene trovato.

Come si può immaginare questo tipo di metodo risulta molto più lento di un metodo virtual e di un metodo statico. Il vantaggio dei metodi dichiarati virtual o dynamic rispetto ai metodi statici sta nel fatto che possiamo utilizzare alcune caratteristiche del polimorfismo come l' override di un metodo, ma a scapito di un processo più laborioso.

Il metodo virtual invece ha lo svantaggio che usa una quantità di memoria maggiore di un metodo dynamic perché tutti i metodi dichiarati virtual che vengono ereditati dalle classi superiori sono inseriti nella VMT delle classi derivate e in caso di uno scenario un po complesso con molte classi e con molti metodi questo potrebbe rappresentare uno spreco di risorse.

I metodi dinamici sono più lenti degli altri ma la Dynamic Method Table simile alla VMT conterrà solo i metodi dichiarati nella classe.

Creazione di una classe

Come abbiamo detto all'inizio di questo articolo una classe anche se non dichiara il suo constructor usa quello della classe TObject che ha ereditato implicitamente. Ma se creiamo un altro metodo constructor con lo stesso nome di quello ereditato? Beh di sicuro andremo a rimpiazzare il vecchio codice con quello nuovo, ma chi si occuperà di allocare lo spazio necessario per i dati della nuova classe? La verità è che il compilatore solo per il fatto di aver dichiarato come constructor il nuovo metodo aggiungerà in automatico tutte le funzioni che sono alla base della creazione di una classe e non perché lo ha ereditato da qualche altra classe.

Vediamo in breve quali sono le funzioni che si occupano di creare una classe e in quale ordine vengono richiamate:

  • TObject.Create o il Constructor della classe
  • ClassCreate
  • TObject.NewInstance (questo è dichiarato come virtual e può essere sovrascritto)
  • TObject.InstanceSize
  • GetMem
  • TObject.InitInstance


Per la distruzione invece avremo

  • TObject.Destroy o il Destructor della classe
  • ClassDestroy
  • TObject.FreeIstance (questo è dichiarato come virtual e può essere sovrascritto)
  • TObject.CleanUpInstance
  • FreeMem

Distruzione di una classe

Negli esempi precedenti non abbiamo mai inserito, per semplicità, la fase di deallocazione dello spazio di memoria occupato da una classe. Questo lo faremo usando il destructor della classe che se non esplicitamente dichiarato verrà ereditato sempre dalla classe TObject.

Var
A:ClassA
Begin
A:=Create.ClassA ;Creiamo la classA
... ;Usiamo i metodi e i campi della classA
A.Destroy; ;Distuggiamo la classA
End;

Se volessimo creare un nostro personale metodo per la distruzione della classe dovremo dichiararlo come "destructor":

type
classA = class
...
destructor Destroy;
...
end;

Il metodo per la distruzione della classe solitamente viene chiamato Destroy come quello ereditato da TObject, ma solitamente viene usato il metodo Free che a differenza del primo metodo controlla che il puntatore all'oggetto non sia nullo (nil).

procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;

Attenzione a non eliminare l'istanza della classe per più di una volta, perché il compilatore lo consentirebbe ma il programma si troverebbe a liberare una area di memoria che non risulta più occupata da una istanza di oggetto e quindi darebbe errore.
Il metodo Free non risolve il problema dato che la variabile che avevamo dichiarato risulta ancora puntare ad una determinata area di memoria e quindi il puntatore non è nullo. 
Questo è uno dei più diffusi errori nella programmazione ad oggetti in delphi. Solitamente non è un problema  resettare la variabile dato che potrà essere riutilizzata all'occorrenza, ma se si rischia una doppia distruzione e quindi si pensa di usare il metodo Free, quello che si dovrebbe fare è di assegnare il valore nil alla variabile.
Molti programmatori per automatizzare il tutto aggiungono alla propria classe il metodo FreeAndNil che fa proprio questo:

A.Free;   //distrugge la classe
A:=nil; //assegna alla variabile A il valore nullo
A:=Free; //Ora non esegue più nulla

Quando è necessario distruggere una classe? Nell'esempio de paragrafo precedente, data la sua semplicità,  in effetti non c'è un effetto collaterale dato che al termine dell'applicazione il sistema operativo libererà tutta la memoria usata dall'applicazione.
Un altro caso nel quale non è necessario distruggere una classe è quando quella classe non ha bisogno di essere creata, come spiegato in precedenza. Infatti se la classe non possiede dati ma solo funzioni statiche non vi saranno dati da allocare in memoria, ne tanto meno dati da deallocare.
A parte questi casi è sempre meglio distruggere una classe per evitare un memory leak, ossia un consumo incontrollato delle risorse in termini di memoria che porta alla saturazione della memoria disponibile dal processo.
Ricordatevene quindi ogni volta che usate una classe all'interno di una funzione o una procedura o di un'altra classe.

Questo doversi occupare della creazione e della distruzione di una classe è in opposizione agli oggetti lifetime managed o garbage collected la cui creazione e distruzione è gestita in automatico dal compilatore.

Variabili o Campi nelle classi

I dati che inseriamo dentro una classe costituiranno i suoi campi. Questi possono essere di due tipi: var e class var.

Se non specificato il compilatore presume che un campo sia sempre di tipo var:

type 
TNumber = class
var
Int: Integer;
end;

Che è equivalente alla seguente dichiarazione:

type 
TNumber = class
Int: Integer;
end;

Come abbiamo visto in precedenza tutti i campi di questo tipo possono essere utilizzati sono dopo la creazione dell'oggetto, perché sono relativi alla singola istanza della classe in memoria.

var
n1,n2:TNumber;
begin
//n1.Int:=1; //-> errore l'istanza non è stata creata
n1:=TNumber.create;
n1.Int:=1;
writeln(n1.Int); //scrive 1
n2:=TNumber.create;
n2.Int:=2;
writeln(n1.Int); //scrive 1
writeln(n2.Int); //scrive 2

Al contrario invece dichiarando un campo class var esso può essere utilizzato anche se non è stata creata l'istanza della classe, dato che fa parte della classe e non delle sue istanze. Per questo motivo esso è condiviso tra tutte le istanze della stessa classe che verranno create.

type 
TNumber = class
class var
Int: Integer;
end;
var
n1,n2:TNumber;
begin
n1.Int:=1;
writeln(n1.Int); //scrive 1
n2.Int:=2;
writeln(n1.Int); //scrive 2 perchè il campo è lo stesso per entrambe le variabili
writeln(n2.Int); //scrive 2

Nelle dichiarazioni dei campi di entrambi i tipi possono essere accorpate più voci, ed il blocco finisce nei seguenti casi:

  • Un altra dichiarazione di tipo class var or var
  • Una dichiarazione di funzione o procedura
  • Una dichiarazione di proprietà
  • Una dichiarazione di constructor o destructor
  • Uno specificatore di visibilità (public, private, protected, published, strict private, e strict protected)

Esempio:

type 
TMiaClasse = class
class var
V1,V2: Integer;
D1:double;

var
C1:integer;
C2:integer;

procedure SetC1(n:integer);
end;

I metodi nelle classi

Un metodo è una porzione di codice associato ad una classe. Esso è comune a tutte le istanze di una classe e se non usa campi (var) è possibile utilizzarlo anche se l'istanza della classe non è stata creata.

Se un metodo restituisce un valore di un qualche tipo allora sarà dichiarato come funzione (function) altrimenti come procedura (procedure).

Esempio di dichiarazione:

type 
TMiaClasse = class(TObject)
...
procedure Prova1;
function Prova2:integer;
...
end;

Successivamente è necessario scrivere l'implementazione del metodo:

procedure TMiaClasse.Prova1; 
begin
...
end;

function TMiaClasse.Prova2:integer;
begin
...
end;

Se la classe è stata dichiarata nella sezione interface di un modulo (unit) allora l'implementazione deve essere fatta nella sezione implementation.

I vari metodi possono essere associati a particolari direttive che ne influenzano la compilazione e l'uso.

Direttive
reintroduce
overload
virtual
dynamic
override
Binding
register
stdcall
safecall
cdecl
pascal
Calling convention
abstract
platform
deprecated
library
Warning

Queste direttive verranno spiegate più avanti nel dettaglio.

self

L'identificativo self rappresenta il puntatore alla singola istanza della classe a cui il metodo appartiene, e la troviamo come variabile implicita in tutti i metodi (tranne quelli statici che saranno descritti in seguito). Il compilatore lo passa come parametro nascosto a tutti i metodi.

Questa variabile è molto importante dato che consente alla classe di avere un riferimento a se stessa e quindi se è il caso ad identificarsi.

I class method

I metodi dichiarati come class si dividono in due tipi: quelli ordinari e quelli statici. Entrambi possono interagire con altri metodi, variabili e proprietà dichiarate come class dato che operano su dati e metodi che fanno parte della classe e non delle sue istanze. Per questo motivo è possibile richiamarli anche prima della creazione delle istanze.

Se è vero che le variabili class sono utilizzate per conservare dati identici per tutte le classi , come ad esempio il nome della classe , è altrettanto vero che alcune operazioni che riguardano questi dati devono essere dichiarati class per lo stesso motivo.

I metodi class ordinari si differenziano da quelli statici solo per la dichiarazione static di seguito al metodo:

type 
TMia1 = class
public
class function prova1(s: string): Boolean;
class procedure prova2(var n: integer);
class
function prova3(s: string): Boolean; virtual; //Esempio di dichiarazione di metodo class - virtual
class procedure prova4(var n: integer); virtual;
...
end;

Esempio di dichiarazione metodi static:

 

type 
TMia2 = class
public
class function prova1(s: string): Boolean; static;
class procedure prova2(var n: integer);  static;
...
end;  

I metodi static non possono essere dichiarati virtual.

Vediamo un esempio pratico:

interface

type
classA = class
class var
X:integer;
var
Y:integer;
procedure ScriviDatiNormale;
class procedure ScriviDatiClass;
class procedure ScriviDatiClassStatic; static;
end;

implementation

{ classA }

procedure classA.ScriviDatiNormale;
begin
writeln(format('normale x=%d y=%d self= %x',[X,Y,integer(self)]));
end;

class procedure classA.ScriviDatiClass;
begin
writeln(format('class x=%d self= %x',[X,integer(self)]));
end;

class procedure classA.ScriviDatiClassStatic;
begin
writeln(format('class static x=%d ',[X]));
end;

end.

Esempio di utilizzo:

var
A:classA;
begin
//Chiamo il metodo normale prima di creare la classe
A.ScriviDatiNormale;         //output->"normale x=0 y=-1 self= 7FFDD000"
//posso accedere il campo di classe
A.Y:=3;
//chiamo il metodo class ordinario usando la variabile di tipo classA
A.ScriviDatiClass;           //output->"class x=0 self= 10000"
//chiamo il metodo class ordinario usando la classe classA
classA.ScriviDatiClass;      //output->"class x=0 self= 40FF14"
//chiamo il metodo class static usando la variabile di di tipo classA
A.ScriviDatiClassStatic;     //output->"class static x=0"
//chiamo il metodo class static usando la classe classA
classA.ScriviDatiClassStatic; //output->"class static x=0"

//Creo la classe
A:=classA.Create;
A.X:=1;
A.Y:=2;

//Chiamo il metodo normale
A.ScriviDatiNormale;      //output->"normale x=1 y=2 self= A14E50"
//chiamo il metodo class ordinario
A.ScriviDatiClass;        //output->"class x=1 self= 40FF14"
//chiamo il metodo class static
A.ScriviDatiClassStatic;  //output->"class static x=1"


A.Destroy;

readln;
end.

Come possiamo notare i metodi dichiarati come class possono essere usati anche prima della creazione della istanza, dato che riguardano proprio la classe e non le sue istanze. Il metodi static non consentono l'uso del puntatore self mentre quelli ordinari ne consentono l'uso, ma come è possibile dedurre dal programmino precedente, se chiamati prima della creazione della classe non è corretto chiamarli dall'istanza.
Questo puntatore è in pratica l'indirizzo dove risiede il blocco di che contiene il codice della classe (=40FF14h) a differenza del normale self che invece è l'indirizzo della istanza (parte dati) nell'esempio = A14E50h.

Direttiva Overload

Fino ad ora abbiamo fatto riferimento a metodi senza parametri. Questi devono essere indicati dentro delle parentesi tonde dopo il nome della funzione:

Type
classA = class
procedure Scrivi(Nome:string; Numero:integer);
function Somma( N1,N2:integer):integer;
end;

I parametri sono separati da un punto e virgola, ma se sono dello stresso tipo possono essere raggruppati e separati da una virgola, come mostrato qui sopra.

Una procedura non è altro che un funzione che non da nessun risultato.

Ora dopo questa piccola introduzione veniamo al dunque, se io volessi ad esempio una funzione che somma sia interi che parole come potrei fare?

La prima cosa è quella di definire due funzioni:

function SommaParole(parola1, parola 2:string):string;
function SommaNumeri(Numero1,Numero2:integer):integer;

Nella programmazione ad oggetti l'overloading di una funzione permette di avere un nome unico per richiamare delle funzioni parametricamente diverse. Nel nostro caso avremo quindi una sola funzione chiamata Somma che a seconda dei parametri verrebbe andrebbe a usare il codice con il quale avevamo implementato le vecchie due funzioni:

function Somma(parola1, parola 2:string):string; overload;
function Somma(Numero1,Numero2:integer):integer; overlaod;

E' bastato quindi solo dichiararle overload e il gioco è fatto. Senza questa dichiarazione il compilatore non permetterà di avere due metodi con lo stesso nome all'interno di una classe.

Ereditarietà tra classi

Vediamo ora di approfondire meglio il discorso sulla ereditarietà tra le classi.
Dato che le cose riguardanti le variabili penso sia stata già chiarita, ci concentreremo principalmente sulla parte dei metodi.
Definirò una classe base (classA) che ha solo due metodi e una classe derivata (classB) che non ne ha neanche uno.

unit UnitClass3;

interface

type
//classe base
classA = class
procedure procedura1;
procedure procedura2;
end;

//classe derivata
classB = class(classA)

end;

implementation

{ classA }

procedure classA.procedura1;
begin
writeln('messaggio 1');
end;

procedure classA.procedura2;
begin
writeln('messaggio 2');
end;

Nel programma che usa queste classi userò solo la classB :

program prova3;

{$APPTYPE CONSOLE}

uses
SysUtils,
UnitClass3 in 'UnitClass3.pas';

var
B:classB;
begin
B:=classB.Create;  // opzionale
B.procedura1;
readln;
end.

In questo caso il compilatore fa la cosa più semplice, cioè implementa solo il codice che serve o meglio che viene usato.
In questo modo l'area di memoria che dovrebbe contenere il codice delle procedure conterrà solo il codice per la procedura1.

Se nella applicazione con avessimo richiamato anche la seconda procedura allora sarebbe stato inserito anche il suo codice.

In pratica si tratta solamente di istruzioni che tutte queste classi condividono e il compilatore staticamente prende il loro indirizzo e lo posiziona in ogni call che le riguarda.

Questo appare chiaro quando sia all'interno della classe base che di quella derivata c'è una procedura con lo stesso nome, ma con due implementazioni diverse:

type
//classe base
classA = class
procedure procedura1;
end;

//classe derivata
classB = class(classA)  
procedure
procedura1; //stesso nome della procedura nella classA 
end;

In quel caso vi saranno due differenti blocchi di codice che verranno richiamati quando necessario dalle rispettive classi di appartenenza.

var
A:classA;
b:classB;
begin
A:=classA.Create;
B:=classB.Create;
A.procedura1; // usa la procedura nella classA
B.procedura1; // usa la procedura nella classB
classA(B).procedura1; // usa la procedura nella classA
...

Come mostrato nell'esempio precedente nell'ultima riga abbiamo richiamato la procedura della classA dalla classB trattandola come classA (type casting). Chiamare un metodo della classe base da una classe derivata sarà sempre possibile a meno che non usiamo la direttiva override.

type
//classe base
classA = class
procedure procedura1; virtual;
end;

//classe derivata
classB = class(classA)  
procedure
procedura1; override
end;

Il risultato sarà:

var
A:classA;
b:classB;
begin
A:=classA.Create;
B:=classB.Create;
A.procedura1; // usa la procedura nella classA
B.procedura1; // usa la procedura nella classB
classA(B).procedura1; // usa la procedura nella classB
...

Possiamo rendere inaccessibile un metodo di una classe base usando quindi la direttiva override nella classe derivata, solo se la nella classe base dichiariamo la procedura virtual o dynamic.

Se invece usiamo solo metodi statici come nel caso iniziale il compilatore ci avvertirà con un messaggio di avvertimento:

[DCC Warning] Unit1.pas(15): W1010 Method 'procedura1' hides virtual method of base type 'classA'

Per evitare questo tipo di messaggi è meglio dichiarare il nuovo metodo nella classe derivata come reintroduce, questo non cambia il comportamento ma rassicura il compilatore che il programmatore è consapevole di quello che sta facendo (praticamente non lo fa mai nessuno):

type
//classe base
classA = class
procedure procedura1;
end;

//classe derivata
classB = class(classA)  
procedure
procedura1; reintroduce;
end;

Se una classe viene definita come sealed allora non può essere ereditata e neanche contenere metodi abstract.

 

Come è stato già detto in precedenza non è possibile l'ereditarietà multipla in delphi, ma per ovviare a questa differenza rispetto ad altri linguaggi, come ad esempio il C++, è possibile ereditare le classi i successione oppure includerle come variabili all'interno della nuova classe oppure usare le interfacce.

Eredità all'interno dei metodi

Una caratteristica molto bella della programmazione ad oggetti in Delphi è quella di poter ereditare e quindi riutilizzare porzioni di codice della classe base all'interno della classe derivata. Questo è possibile la parola chiave inheritated seguita o meno dalla funzione di cui si vuole ereditare il codice. Se la funzione non viene specificata il compilatore cercherà nella classe base il nome del metodo della classe derivata che sta cercando di ereditare.

Esempio :

type 
classA = class
procedure P1;
end;

classB = class(classA)
procedure P2;
end;

procedure classA.P1;
begin
writeln('1');
end;

procedure classB.P2;
begin
inherited P1;
writeln('2');
inherited P1;
writeln('2');
inherited P1;
end;

Se proviamo a usare il metoto P2 della classB:

var
B:classB;
begin
B.P2;

Avremo come risultato:

1
2
1
2
1

Se il metodo fosse stato una funzione o avesse avuto qualche parametro avremo ugualmente potuto inglobare il codice:

type 
classA = class
funzione Scrivi(n:integer):string; // Dichiariamo una funzione che trasforma un numero in una stringa
end;
...
//Implementiamo la procedura P2 ereditando il codice della funzione Scrivi della classA
procedure classB.P2;
var
testo:string;
begin
testo:=inherited Scrivi(3); //testo:=tre
writeln(testo);
end;

Direttiva abstract

Un metodo dichiarato abstract non ha impementazione nella classe in cui viene dichiarato e deve essere implementato nelle classi derivate.
Questa direttiva va usata solo di seguito ad un metodo dichiarato virtual o dynamic, dato che lo scopo è quello di prevedere un metodo ma di evitare di implementarlo, lasciando che solo le classi derivate abbiano la possibilità di implementarlo facendo finta di "sovrascriverlo".
Se si prova ad accedere ad un metodo abstract verrà generato un errore (Abstract Error). Esempio:

  classA = class
procedure prova; virtual; abstract;
end;

begin
A.prova; //chiamata al metodo prova
end;

Il programam verrà compilato ugualmente anche se il compilatore invierà un messaggio di warning:

[DCC Warning] Project5.dpr(14): W1020 Constructing instance of 'classA' containing abstract method 'classA.prova'

Dato che il metodo non è stato implementato l'elemento della VMT rimanderà alla funziona che genera l'abstract error.

Indirizzo classe A






00A14E40 00408A30 -> Indirizzo funzione A.prova






00408A30 0040272c -> Indirizzo funzione AbstractError






0040272c ....









Indirizzo in memoria






Contenuto memoria





Nella pratica inserendo un metodo abstract in una classe obbliga il programmatore a non creare mai una istanza di quella classe, ma ad usarla come modello per le sue classi derivate.

Vediamo un esempio corretto di uso di un metodo abstract:

type
classA = class
strict protected
procedure prova; virtual; abstract;
end;

classB = class(classA)
procedure prova; override;
end;

implementation

{ classB }

procedure classB.prova;
begin
writeln('ClassB');
end;

I metodi dichiarati abstract non dovrebbero essere accessibili se non dalla classe derivata che li dovrà implementare, per questo motivo è meglio classificarli come strict protected.

 

Metodi di tipo Message

Dichiarare un metodo di tipo message significa che quella procedura o quella funzione si occuperà della gestione di "messaggi" del tipo specificato dopo la direttiva message. I messaggi ai quali ci si riferisce sono quelli del systema windows e solitamente questi metodi rispondono a questi segnali ma non vengono richiamati esplicitamente nel programma.

I messaggi sono fondamentalmente delle notifiche che windows invia alle varie applicazioni in corrispondenza di alcuni eventi, come ad esempio click del mouse, posizione del mouse, la pressione di un tasto, etc..
Da un punto di vista pratico questi messaggi sono solo dei record che contengono le informazioni necessarie a identificare il "destinatario" e il tipo di messaggio con le relative informazioni da trasmettere.

Un esempio di messaggio è questo:

type
TMsg = packed record
hwnd: HWND;     // l'identificativo della finestra al quale il messaggio è diretto
message: UINT;  // l'identificativo del messaggio
wParam: WPARAM; // 32 bit di informazioni (dipende dal tipo di messaggio)
lParam: LPARAM; // 32 bit di informazioni (dipende dal tipo di messaggio)
time: DWORD;    // tempo in cui il messaggio è stato creato
pt: TPoint;     // posizione del mouse al momento della creazione del messaggio
end;

Un esempio di metodo che gestisce i messaggi di tipo WM_PAINT è questo:

procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;

Ad ogni modo questo argomento verrà approfondito in un altro articolo.

Le proprietà

Le proprietà sono un particolare tipo di campo che da' la possibilità di leggere o scrivere dei dati, ma anche eseguire del codice all'interno della classe. Esternamente all'utilizzatore si presentano come normali variabili ma non lo sono:

Type
TMiaClasse = class
A,B:integer;
function GetA:integer;
procedure SetA(n:integer);
property Val : integer read GetA write SetA;
end;

function TMiaClasse.GetA:integer;
begin
if A=0 then result:=1 else result:=A;
end;

procedure
TMiaClasse.SetA(n:integer):integer;
begin

 A:=n;
B:=n*2;
 end;

Ora vediamo di fare un esempio di utilizzo:

Var
mia:TMiaClasse;
I:integer;
begin
mia:=TMiaClasse.create;
mia.Val:=3; // scrivo nel campo Val
I:=mia.Val; // leggo il campo Val

In questo modo scrivendo e leggendo una proprietà verranno richiamate i metodi SetA e GetA, ma noi avremo l'illusione di leggere e scrivere su una comune variabile.
I motivi per usare le proprietà possono essere molteplici, tra i quali l'eseguire implicitamente dei controlli sui dati in ingresso, o nascondere e proteggere un campo dati privato dall'accesso indiscriminato, infatti una caratteristica della proprietà è che può essere definita in sola lettura, non specificando la clausola write oppure in sola scrittura omettendo la clausola read.

property Val : integer read GetA; //la proprietà Val ora è in sola lettura

Nei componenti dell'IDE delphi le proprietà pubblicate cioè dichiarate come published sono quelle che appaiono nell'Object Inspector.

Le regole per definire le proprietà sono semplici:

  • Il tipo deve essere stato definito in precedenza.
  • I membri ai quali colleghiamo la proprietà devo essere dello stesso tipo dichiarato.
    Devono essere dichiarati prima della proprietà. Nel caso di metodo non può essere dichiarato dynamic, se è dicharato virtual non può essere anche overload.
  • Tra i vari specificatori deve esserci almeno un read o un write.
  • Se colleghiamo la proprietà in lettura (read) ad un metodo, questo non deve avere parametri.
  • Se colleghiamo la proprietà in scrittura (write) ad un metodo questo deve avere un solo parametro.

Vi sono altri specificatori opzionali stored, default, e nodefault. Solitamente queste sono implicite nella dichiarazione della proprietà.

  • Possiamo definire una propietà stored true o stored false , in questo modo decideremo se la proprietà verrà salvata o meno. Se omesso di default sarà true.
  • Default deve essere seguito da un valore costante dello stesso tipo dichiarato per la proprietà e sarà il suo valore predefinito.
  • Nodefault è solitamente implicito se non dichiarato un valore predefinito, ed è usato per indicare che non vi è alcun valore predefinito.

La direttiva implements aggiunge una interfaccia alla classe aggiungendola a quelle già derivate.

Specificatori di visibilità

Gli specificatori di visibilità sono :protected, private, public, published e automated. Servono a classificare i vari campi e metodi che compongono la classe, e ne controllano la visibilità rispetto ad altre classi o altre parti di codice che utilizzano la classe a cui appartengono.

TMiaclasse = class
private
...              //Campi e metodi private  
protected
...              //Campi e metodi protected
public
...              //Campi e metodi public
published
...              //Campi e metodi published
end;

Private

I metodi o i campi dichiarati come private sono visibili solo dal codice all'interno della stessa unit. E' usato per nascondere membri della classe da altre unit che importano quella che contiene la classe.

Protected

In questo caso i metodi e i campi sono utilizabili solo dalle classi nella stessa unit e dalle classi derivate che ereditano la classe a cui i membri appartengono, così che la classe base possa essere perfettamente accessibile in quanto classe base, ma nasconda quei metodi dichiarati come protected agli utilizzatori.

Public

I metodi e i campi dichiarati come public sono sempre accessibili da qualsiasi parte di codice.
Il metodi costructor e destructor devono essere inseriti sempre in questa categoria.

Published

E' equivalente a public, ma è usata per le proprietà dei componenti dell' IDE delphi che devono essere accessibili a design time. Per questi membri vengono generate le runtime type information (RTTI). RTTI consente a un'applicazione di interrogare i campi e le proprietà di un oggetto in modo dinamico e per individuare i metodi.
Le proprietà di tipo published sono ristette ad alcuni tipi di dati, solitamente quelli che hanno dimensione di un byte o una word o una double-word. Altri sono permessi ma non ben supportati.
Tutti i metodi sono pubblicabili, ma una classe non può pubblicare due o più metodi in overload (con lo stesso nome). I campi possono essere pubblicati solo se sono di una classe o tipo di interfaccia.

Automated

E' rimasto solo per compatibilità, ma ormai in disuso. I membri dichiarati come Automated hanno la stessa visibilità de membri public, solo che per essi vengono generate le informazioni di tipo Automation necessarie per gli automation servers.

Un classico esempio di questi specificatori di visibilità è quello di dichiarare una variabile come private e la proprietà che la controllano in lettura e scrittura come published. In questo modo chi importa la unit contenente la classe e crea una istanza della classe non vedrà il campo private ma solo la sua proprietà. In molti casi questo è importante perché protegge dei membri "sensibili".

Nelle ultime versioni di Delphi è stata inserita anche la direttiva strict che funziona in associazione a protected e private.

  • I membri dichiarati come strict private sono utilizzabili in lettura e scrittura solo all'interno della stessa classe e non da altre classi anche se appartengono alla stessa unit.
  • I membri dichiarati come strict protected sono come i membri protected tranne che non vengono visti dalle classi nella stessa unit. Sono quindi accessibili all'interno della classe stessa e al'interno delle classi che la ereditano, indipendentemente dalla unit in cui si trovano.

 

Operatori per le classi

Gli operatori per le classi sono is , as e of.

La sintassi dell'operatore is è : (Istanza is Classe)->boolean . Quindi fondamentalmente serve a verificare che una particolare oggetto sia una istanza di quella determinata classe ed il risultato può essere vero o falso.

if (Sender is TButton) then ...

L'operatore as invece serve per un typecast, ovvero per trattare un oggetto come istanza di una determinata classe: (Istanza as Classe).membroClasse

Un esempio Esempio:

procedure TForm1.Button1Click(Sender: TObject);
begin
(Sender as TButton).caption:='Ok';
end;

Qui l'oggetto è passato come un generico TObject, quindi un puntatore ad un oggetto. Noi sappiamo che questo fantomatico oggetto è un TButton, quindi forziamo il compilatore a trattarlo come tale per accedere ai suoi membri.

L'operatore of serve a creare una classe come riferimento di un'altra:

type
ClassB = class of ClassA

Classi Helper

Questa funzionalità è possibile usarla solo con versione recenti di Delphi (> della versione 7), e in pratica si tratta di un tipo di classi che permettono l'estensioni di altre classi, integrandone le funzionalità con nuovi metodi.

Per fare questo la sintassi è semplice , basta dichiarare la nuova classe helper della vecchia.
Vediamo un esempio abbastanza semplice e generale; nella UnitVecchia abbiamo la nostra vecchia classe che abbiamo sempre usato:

unit UnitVecchia;

interface

type
TMiaClasse = class
Contatore:integer;
procedure Incrementa;
end;

implementation

{ TMiaClasse }

procedure TMiaClasse.Incrementa;
begin
Inc(Contatore);
end;

end.

La classe ha un solo campo e può essere incrementato con l'unica procedura Incrementa.

Ora in creiamo la UnitNuova e ci mettiamo un bell'helper alla vecchia classe:

unit UnitNuova;

interface

uses
UnitVecchia; //Devo importare la UnitVecchia per poter collegare l'helper

type
TMiaClasseHelper = class helper for TMiaClasse
procedure Decrementa;
end;

implementation

{ TMiaClasseHelper }

procedure TMiaClasseHelper.Decrementa;
begin
Dec(Contatore);
end;

 end.

All'interno della classe helper abbiamo implementato una nuova procedura che agginungerà la capacità di decrementare il contatore alla vecchia classe.

Per usare il tutto creiamo un programma molto semplice:

uses
...
UnitVecchia,UnitNuova, //Le devo inserire entrambe se voglio creare una istanza della classe vecchia
// e usare le funzionalità dell'helper

...
var
X:TMiaClasse;
begin
X:=TMiaClasse.Create;
X.Incrementa; //usa la procedura della vecchia classe
X.Decrementa; //usa la procedura dell'helper
writeln(X.contatore); //scrive 0
end.

Fate molta attenzione al fatto che la classe helper può non solo aggiungere ma anche sovrascrivere le vecchie funzionalità, infatti se dichiariamo un metodo identico a quello della classe vecchia, verrà preso in considerazione solo quello dell'helper:

unit UnitNuova;

interface

uses
UnitVecchia; //Devo importare la UnitVecchia per poter collegare l'helper

type

procedure Decrementa;
procedure Incrementa;
end;

implementation

{ TMiaClasseHelper }

procedure TMiaClasseHelper.Decrementa;
begin
Dec(Contatore);
end;

procedure TMiaClasseHelper.Incrementa;
begin
Contatore:=Contatore+2;
end;

end.

Per provare cosa accade:

var
X:TMiaClasse;
begin
X:=TMiaClasse.Create;
X.Incrementa; //usa il metodo dell'helper
X.Decrementa;
 writeln(X.contatore); //questa volta scrive 1
end.
Ultimo aggiornamento ( Mercoledì 15 Febbraio 2012 09:42 )  
Loading

Login

facebook gmail twitter




Chiudi