Polimorfismo (informatica)
Da Wikipedia, l'enciclopedia libera.
In informatica, il termine polimorfismo (dal greco "avere molte forme") viene usato in senso generico per riferirsi a espressioni che possono rappresentare valori di diversi tipi (dette espressioni polimorfiche). In un linguaggio non tipizzato, tutte le espressioni sono intrinsecamente polimorfiche. Il termine viene usato in senso più specifico nel contesto della programmazione orientata agli oggetti, e si riferisce al fatto che una espressione il cui tipo sia descritto da una classe A può assumere valori di un qualunque tipo descritto da una classe B sottoclasse di A.
Indice |
[modifica] Polimorfismo nella programmazione a oggetti
[modifica] Caso di partenza: le figure
Supponiamo di voler sviluppare un programma in grado di disegnare dei poligoni di date dimensioni a schermo. Ogni poligono va disegnato in un modo diverso, utilizzando le librerie fornite dal linguaggio utilizzato.
Poiché a run-time non sapremo esattamente quanti e quali poligoni dovremo disegnare, è necessario che il compilatore possa ricondurre quadrato, cerchio, pentagono eccetera ad uno stesso oggetto, in modo tale da riconoscerne i metodi utilizzati. Per fare ciò dichiariamo una classe base Figura, dalla quale tutte le altre erediteranno le proprietà. L'ereditarietà, come si vedrà più avanti, consiste nell'implementare membri che sono stati solo dichiarati nella classe base.
[modifica] La classe base
Esempio (linguaggio Visual Basic):
Public MustInherit Class Figura Public MustOverride Sub Disegna() Public MustOverride Function Perimetro() As Double Public MustOverride Function Area() As Double End Class
Abbiamo appena dichiarato una classe che deve essere ereditata da altre classi, e mai utilizzata come classe base. I metodi, inoltre, devono essere sottoposti ad override (lett. scavalcamento) dalle classi che ereditano. Una volta fatto questo, possiamo implementare tutte le figure che vogliamo.
[modifica] Alcune classi derivate
Nell'esempio che segue si omettono le implementazioni di alcuni membri
Public Class Quadrato Inherits Figura Private lato As Double Public Sub New(lato As Double) ... End Sub Public Property Lato() As Double ... End Property Public Overrides Sub Disegna() 'Inserire qui le istruzioni per disegnare un quadrato in accordo con le librerie grafiche ... End Sub Public Overrides Function Perimetro() As Double Return lato*4 End Function Public Overrides Function Area() As Double Return lato*lato End Function End Class Public Class Cerchio Inherits Figura Private raggio As Double Public Sub New(raggio As Double) ... End Sub Public Property Raggio() As Double ... End Property Public Overrides Sub Disegna() 'Inserire qui le istruzioni per disegnare un quadrato in accordo con le librerie grafiche ... End Sub Public Overrides Function Perimetro() As Double Return raggio*2*Math.PI End Function Public Overrides Function Area() As Double Return raggio*raggio*Math.PI End Function End Class
E così via con le altre figure. In questo modo, volendo lavorare con un array di figure non si generano conflitti di tipo, come nell'esempio che segue:
Dim Figure(5) As Figura ... ' Supponiamo che l'utente inserisca 3 quadrati, un cerchio e un esagono (si suppone la classe Esagono implementata come sopra) ' ad es. Figure(2) = New Quadrato(4) ' Questa istruzione, proprio perché Quadrato eredita da Figura, non genera errori di compilazione ... For Each Fig As Figura In Figure Fig.Disegna() Console.WriteLine(Fig.Perimetro) Next
L'esecutore, ad ogni figura che incontrerà, effettuerà una chiamata alla subroutine opportuna della classe di appartenenza. Vediamo in che modo ciò avviene.
[modifica] Polimorfismo e compilazione
Il polimorfismo si ha con una azione combinata di compilatore e linker. Al contrario di quanto accade nella maggior parte dei casi, il run-time ha un ruolo importantissimo nell'esecuzione di codice polimorfo, in quanto non è possibile sapere, a compile-time, la classe di appartenenza degli oggetti istanziati. Il compilatore ha il ruolo di preparare l'occorrente per far decidere l'esecutore quale metodo invocare.
Ai fini della programmazione polimorfa non è necessario conoscere il linguaggio assemblativo, tuttavia è necessario avere alcune nozioni di base sull'indirizzamento per capire quanto segue.
[modifica] Cosa avviene a compile-time: la TMV
Quando viene compilata la classe base, il compilatore identifica i metodi che sono stati dichiarati virtuali (parola chiave MustOverride in Visual Basic, virtual in C++ e simbolo "#" in progettazione UML), e costruisce una Tabella dei Metodi Virtuali, indicando le signature delle funzioni da sottoporre a override. Queste funzioni restano quindi "orfane", non hanno cioè un indirizzo per l'entry-point.
Quando il compilatore si occupa delle classi derivate, raggruppa i metodi sottoposti ad override in una nuova TMV, di struttura identica a quella della classe base, stavolta indicando gli indirizzi dell'entry-point.
Ai fini teorici, possiamo supporre una tabella di questo tipo:
Figura | Quadrato | Cerchio |
_Disegna:0x0000 _Perimetro:0x0000 _Area:0x0000 |
_Disegna:1x3453 _Perimetro:0xbc1a _Area:0x25bf |
_Disegna:1x52d0 _Perimetro:0x52ab _Area:0xaa25 |
Non importa in quale ordine siano mappate le funzioni, l'importante è che si trovino nello stesso ordine (allo stesso offset) in tabella. Nota: a livello assembly le TMV non hanno identificatori: sono semplici aree di memoria di lunghezza prefissata (32 o 64 bit solitamente). Gli identificatori sono stati inseriti nell'esempio ai soli fini illustrativi.
[modifica] Cosa avviene a run-time: il binding dinamico
Abbiamo visto che il compilatore lascia spazi vuoti per i metodi non mappati. Analizziamo passo-passo, come in un trace, tutto ciò che avviene a run-time. Codice di riferimento:
Dim Circle As Figura Circle = New Cerchio(3) Circle.Disegna()
Supponiamo di aver istanziato un cerchio e di volerlo disegnare. La prima istruzione non ha grande funzionalità: riserva semplicemente spazio sullo stack per la variabile Circle di una lunghezza pari a Figura. Nella seconda istruzione tale stack viene di fatto popolato con la chiamata al costruttore. A seconda del linguaggio, la TMV di Figura viene sovrascritta con quella di Cerchio e il valore 3 viene allocato nell'area riservata al raggio di tipo Double (64 bit solitamente). Nella terza istruzione l'esecutore consulta la TMV di Cerchio e preleva l'indirizzo della prima delle funzioni mappate. Questo perché ad assembly-level non vi sono identificatori di alcun tipo. Una volta prelevato l'indirizzo, il programma è pronto per il salto all'entry-point di Disegna.