adszone.org
venerdì 20 settembre 2024
"Il modo corretto di pensare il software"
home
consulenza  vb.net  contatti 
  Visual Basic .NET
  Programmazione ad oggetti - II
Domande

Classi

In VB.NET definiamo una classe usando le parole chiave: Class... End Class. Esempio: definiamo la classe 'Batteria' con la quale vogliamo rappresentare una pila del mondo reale.

Class Batteria

    Private m_Voltaggio As Double
    Private m_Livello As Integer 
	
    Public Sub New()
        m_Voltaggio = 0
        m_Livello = 0
    End Sub

    Public Function Alimenta(ByVal Corrente As Double) As Boolean
        If m_Livello = 0 Then
            return False
        Else  
            m_Livello = m_Livello - 1
            return True
        End If
    End Function

End Class

Col colore verde indichiamo la dichiarazione delle variabili che caratterizzano la struttura interna di ogni oggetto che creato con questa classe. Tali variabili vengono dette campi o membri e convenzionalmente diamo loro un nome che comincia con 'm_'. In blu abbiamo scritto il costruttore. In VB.NET il costruttore e' una 'Sub' (procedura) e si dichiara con il nome New. Ogni volta che incontriamo una sub New all'interno di una classe, sappiamo che quello e' un costruttore. Come vederemo, possono esserci piu' costruttori. In rosso indichiamo i metodi.

Nella classe Batteria, m_Livello memorizza il livello di carica della batteria, analogamente m_Voltaggio indica la tensione nominale della pila (ad es. 1.5 Volts, 5 Volts, etc). Ora, non ci proponiamo di fare una simulazione fisica dei processi che intervengono all'interno di una batteria reale, quindi i calcoli e le relazioni fra le grandezze in gioco saranno essenzialmente 'pro forma'. Quando istanziamo la batteria, osserviamo che il costruttore la crea 'scarica' (tutti e due i campi sono uguali a zero). Il metodo Alimenta e' una funzione che permette di far consumare alla batteria una certa quantita' di energia, in base alla corrente richiesta nel parametro. Qui ignoriamo il parametro e ci limitaimo a fornire sempre la stessa quantita' di energia, cioe' decrementiamo il livello di carica sempre di uno. Alimenta restituira' 'Falso' se non e' in grado piu' in grado di erogare energia (pila scarica). Se restituisce 'Vero', allora l'utilizzatore dell'oggetto sara' autorizzato a compiere il proprio compito (accendere un oggetto lampadina ad esempio), altrimenti smettera' di funzionare.

Istanze ed Handle

Quando vogliamo utilizzare una batteria dobbiamo innanzitutto creare l'oggetto. Lo facciamo nello stesso modo in cui dichiariamo una variabile, ma ci sono diverse osservazioni da fare:

    Dim hBatteria As Batteria

    hBatteria = New Batteria()


oppure

    Dim hBatteria As Batteria = New Batteria()

oppure

    Dim hBatteria As New Batteria()

In realta' quando istanziamo un oggetto, la variabile che utilizziamo non e' l'oggetto stesso, ma un riferimento ad esso. Questa variabile speciale e' detta handle. La possiamo considerare come una chiave d'accesso all'oggetto. Nel primo caso dichiariamo il riferimento all'oggetto (notate l'assenza delle parentesi tonde dopo il nome della classe) e solo successivamente istanziamo l'oggetto, con la parola chiave New. Trovando la parola chiave New ci viene in mente che stiamo invocando proprio il costruttore. Ricapitolando, quando abbiamo un riferimento ad un oggetto, non necessariamente abbiamo anche l'oggetto. Per ottenere l'oggetto dobbiamo usare New. La seconda forma di istanziamento e' una forma contratta che raccoglie i due passi del caso precedente in un' unica istruzione, dichiarazione piu' inizializzazione. In questo modo nasce contemporaneamente l'handle e l'oggetto a cui l'handle si riferisce. La terza forma e' simile alla seconda.

Proprieta'

Il concetto di proprieta' e' un'estensione alla programmazione ad oggetti introdotto dal Visual Basic. Le proprieta' sono costrutti simili a procedure e funzioni. Essi danno volutamente l'impressione che stiamo accedendo direttamente ad un campo interno dell'oggetto, ma in realta' non e' cosi'.

Abbiamo definito la classe Batteria in modo che, al momento della costruzione, l'oggetto pila sia scarico. Dobbiamo pur trovare un modo di caricarlo. Potremmo usare un metodo che chiameremo Carica, al quale passiamo un valore che definisce il livello di carica, oppure possiamo creare una proprieta' alla quale assegnare direttamente il valore della carica. Nel primo caso, scriveremo:

    hBatteria.Carica(100)

nel secondo caso:

    hBatteria.Livello = 100

dove 'Livello' e' il nome della proprieta'.

Se avessimo dato al campo m_Livello l'attributo Public, avremmo potuto modificare direttamente il suo valore scrivendo: 'hBatteria.m_Livello = ...". Abbiamo pero' visto che, per ragioni di incaspulamento, la struttura interna deve rimanere nascosta ed allora questa tecnica va utilizzata raramente. Quindi per ottenere qualcosa di simile, utilizziamo le proprieta'. Notate che non esiste nessuna variabile membro chiamata Livello.

Implementiamo sia Carica che Livello:

Class Batteria

    ' ...
	
    Public Sub Carica(ByVal Livello As Integer)
        If Livello > 100 Then Livello = 100
        m_Livello = Livello
    End Sub


    Public Property Livello() As Integer
    
        Get
            return m_Livello
        End Get
        
        Set 
            If Livello > 100 Then Livello = 100
            m_Livello = Value
        End Set         
        
    End Property

End Class

Il metodo Carica, in rosso, si limita a copiare il valore di Livello nel campo m_Livello. Abbiamo evidenziato la proprieta' in verde. Il suo corpo e' racchiuso tra Property... End Property. Nell'intestazione la parola As ci permette di specificare il tipo di dato che la proprieta' rappresenta; in questo caso Livello e' un intero, coerentemente con 'm_Livello'. Notate che al suo interno ci sono due sezioni. La sezione Set indica il gruppo di operazioni che verranno compiute quando alla proprieta' si assegna un valore. Se guardate bene, noterete che questa parte e' praticamente il corpo della procedura Carica. Osservate che il valore assegnato alla proprieta' e' identificato dalla parola chiave Value. La sezione costituita dal Get invece, viene eseguita quando si tenta di utilizzare il valore contenuto nella proprieta' (ad esempio x = hBatteria.Livello). In conclusione, Get indica quali passi eseguire per passare il valore della proprieta' al codice chiamante, Set indica come e dove memorizzare il valore che si assegna. Osservate che non deve esistere necessariamente un campo che faccia da appoggio al valore della proprieta'. Molte volte in Get, ad esempio, e' comune vedere il valore sintetizzato tramite formule che utilizzano altri campi: 'return m_Base * m_Altezza / 2'.

Implementiamo anche la proprieta' Voltaggio:

Class Batteria

    ' ...

    Public Property Voltaggio() As Double
    
        Get
            return m_Voltaggio
        End Get
        
        Set 
            m_Voltaggio = Value
        End Set         
        
    End Property

End Class

Eventi

Oltre a Proprieta' e Metodi, con VB.NET una classe puo' definire Eventi. In genere una programmazione ad oggetti classica, invece, simula gli eventi tramite interfacce ed ereditarieta' che vedremo in seguito. Un evento non e' altro un qualcosa che accade e tramite esso l'oggetto informa il mondo esterno. Un bambino che piange e' un evento che puo' informare i genitori che ha fame. La nostra batteria potrebbe informare l'uilizzatore che la carica e' esaurita. Chiamiamo questo evento Scarica. E' compito dell'oggetto generare l'evento quando e' il momento. Riscriviamo parzialmente la classe evidenziando la gestione dell'evento:

Class Batteria

    Public Event Scarica()
	
    ' ...

    Public Function Alimenta(ByVal Corrente As Double) As Boolean
        If m_Livello = 0 Then
            Return False
        Else
            m_Livello = m_Livello - 1
            If m_Livello = 0 Then RaiseEvent Scarica()
            Return True
        End If
    End Function

    ' ...
End Class

In rosso sono indicate le modifiche apportate alla classe. Per poter generare un evento bisogna specificare il suo prototipo con la parola chiave Event. Notate che il prototipo e' simile a quello di una procedura. Tra le parentesi tonde avremmo potuto inserire una lista di paramtri se fosse stato il caso. Quando abbiamo bisogno di generare l'evento lo lanciamo con la parola chiave RaiseEvent. In RaiseEvent specifichiamo il nome dell'evento e l'eventuale lista di parametri come se stessimo invocando una funzione.

Cosa ne sara' dell'evento? Esso puo' essere intercettato dalla sezione di codice che utilizza l'oggetto. Ad esempio:

    Dim WithEvents hBatteria As Batteria
    hBatteria = New Batteria()

Public Sub Batteria_Scarica() Handles hBatteria.Scarica MessageBox.Show("La batteria e' scarica") End Sub

Per abilitare la ricezione degli eventi generati da un oggetto possiamo dichiarare l'oggetto aggiungendo WithEvents alla sua dichiarazione. L'evento puo' essere agganciato ad una procedura. Nell'esempio abbiamo associato la Sub Batteria_Scarica all'evento hBatteria.Scarica tramite la parole chiave Handles. E' facile capire cosa succedera' quando l'evento verra' generato: comparira' una MessageBox che avvisera' l'utente.

Segue la classe completa e l'esempio di utilizzo in un Form di Windows.

Public Class Batteria

    Public Event Scarica()

    Private m_Voltaggio As Double
    Private m_Livello As Integer

    Public Sub New()
        m_Voltaggio = 0
        m_Livello = 0
    End Sub

    Public Function Alimenta(ByVal Corrente As Double) As Boolean
        If m_Livello = 0 Then
            Return False
        Else
            m_Livello = m_Livello - 1
            If m_Livello = 0 Then RaiseEvent Scarica()
            Return True
        End If
    End Function

    Public Sub Carica(ByVal Livello As Integer)
        If Livello > 100 Then Livello = 100
        m_Livello = Livello
    End Sub

    Public Property Livello() As Integer

        Get
            Return m_Livello
        End Get

        Set(ByVal Value As Integer)
            If Livello > 100 Then Livello = 100
            m_Livello = Value
        End Set

    End Property

    Public Property Voltaggio() As Double

        Get
            Return m_Voltaggio
        End Get

        Set(ByVal Value As Double)
            m_Voltaggio = Value
        End Set

    End Property
End Class


' Sezione di codice estratta dal form

    Dim WithEvents hBatteria As Batteria

    Public Sub Batteria_Scarica() Handles hBatteria.Scarica
        MessageBox.Show("La batteria e' scarica")
    End Sub

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        hBatteria = New Batteria()
        hBatteria.Voltaggio = 5
        hBatteria.Livello = 3
        TextBox1.Text = hBatteria.Livello
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles Button1.Click
        hBatteria.Alimenta(1)
        TextBox1.Text = hBatteria.Livello
    End Sub


Ereditarieta'

Si dice spesso che la programmazione ad oggetti trae vantaggio dal riutilizzo del codice. Questo non vuol dire copiare nei propri sorgenti una classe scritta da qualcun altro. L'ereditarieta' e' quella caratteristica che permette di riutilizzare il codice. Si ha ereditarieta' quando una nuova classe, detta derivata, e' costruita sulla struttura di una classe gia' esistente, detta classe base. La classe derivata quindi, e' una classe che ha gia' una funzionalita' di base che lo sviluppatore non deve riscrivere. Notate che qui non e' importante avere a disposizione il codice della classe base per derivare. A partire del comportamento base dell'oggetto, lo sviluppatore puo' ampliare la sua struttura (dati piu' metodi) specializzandone le funzionalita'.

Pensate al rapporto che intercorre tra l'oggetto Rettangolo e l'oggetto Quadro. Fisicamente e' facile pensare il Quadro derivato dal Rettangolo, infatti il Quadro e' anche un Rettangolo. La derivazione lo porta poi anche all'abilita' di disegnare un'immagine al suo interno. Questo esempio dimostra visivamente come il Quadro abbia ereditato la struttura del Rettangolo, cioe' la sua forma. Su quella stessa forma si e' poi costruito il resto. Descriviamo le classi che definiscono i due oggetti.

Public Class Rettangolo

    Protected m_X As Integer
    Protected m_Y As Integer
    Protected m_Base As Integer
    Protected m_Altezza As Integer

    Public Sub New(ByVal x As Integer, ByVal y As Integer, _
          ByVal base As Integer, ByVal altezza As Integer)
        m_X = x
        m_Y = y
        m_Base = base
        m_Altezza = altezza
    End Sub

    ' metodi ...
End Class

Public Class Quadro
    Inherits Rettangolo

    Private m_Immagine As Integer

    Public Sub New(ByVal x As Integer, ByVal y As Integer, _
          ByVal base As Integer, ByVal altezza As Integer)
        MyBase.New(x, y, base, altezza)
        m_Immagine = 0
    End Sub

    Public Sub InserisciImmagine(ByVal i As Integer)
        m_Immagine = i
    End Sub

    Public Sub Disegnati()
        ' disegna l'immagine numero i
    End Sub

End Class

Volendoci dedicare all'ereditarieta', diciamo subito che tralasceremo i dettagli tecnici della gestione dell'immagine; per non appesantire, supponiamo di identificare l'immagine con un indice (numero intero).

La prima cosa che abbiamo evidenziato e' la parola chiave che ci permette di realizzare l'ereditarieta' Inherits. Dal momento che stiamo ereditando, ci basta definire soltanto il campo m_Immagine, che conterra' l'indice dell'immagine da visualizzare. Avendo ereditato, alla classe Quadro appartengono anche i campi Protected della classe Rettangolo. Se avessimo definito i campi Private ne avremmo impedito l'accesso da parte della classe derivata. La parola chiave MyBase e' un riferimento alla classe base. Notate che tramite MyBase noi invochiamo direttamente il costruttore della classe base. Cio' e' necessario perche' avvenga la sua corretta inizializzazione. Infatti, come potrebbe costruirsi la classe Rettangolo senza sapere i suoi parametri? Glieli forniamo noi nel costruttore derivato. Notate invochiamo il costruttore base per primo.

A questo punto, in Quadro, abbiamo a disposizione tutti i campi ed i metodi della classe base che non siano dichiarati come 'Private'. A questi ne abbiamo aggiunti altri che specializzano l'impiego dell'oggetto.

Overriding

L'overriding si riferisce ai metodi che una classe eredita. La classe derivata puo' ridefinire metodi della classe base. Quando si ridefinisce (overriding) un metodo, il nuovo metodo deve avere lo stesso nome e la stessa lista di parametri del vecchio metodo, quello che cambia e' l'implementazione:

    ' Metodo della classe base
    Public Overridable Sub Messaggio(ByVal msg As String)
        MessageBox.Show(msg)
    End Sub
    
    ' Metodo della classe derivata
    Public Overrides Sub Messagio(ByVal msg As String)
        MessageBox.Show("(" & msg & ")")
    End Sub

Nell'esempio, la classe derivata ora dispone di un metodo 'Messaggio' che e' stato sovrascritto. Quando invocheremo tale metodo, l'oggetto visualizzera' lo stesso messaggio che avrebbe visualizzato se non avesse reimplementato il metodo, ed in piu' racchiude il messaggio tra le parentesi tonde. Per abilitare l'overriding di un metodo bisogna associargli l'attributo Overridable e nel sovrascriverlo si usa l'attributo Overrides.

Overloading

Anche l'overloading si riferisce ai metodi che una classe eredita. Nella classe derivata un metodo puo' essere definito piu' volte usando lo stesso nome, ma variando la lista di parametri:

    ' Metodo della classe base
    Public Sub Incrementa(ByVal step As Integer)
        valore = valore + step
    End Sub
    
    ' Metodi della classe derivata
    Public Overloads Sub Incrementa(ByVal step As Double)
        valore = valore + step
    End Sub

    Public Overloads Sub Incrementa(ByVal step_l As Integer, _
        ByVal step_h As Integer)
        valore = valore + (step_h * 256 + step_l)
    End Sub
    

L'attributo Overloads permette di 'sovraccaricare' un metodo con diverse implementazioni. Quando invochiamo tale metodo individuiamo una particolare implementazione in base al tipo e/o numero di parametri che gli forniamo. Poiche' ad un ben preciso nome di metodo facciamo corrispondere diverse implementazioni, qualcuno definisce l'overloading come Polimorfismo

Polimorfismo

Il polimorfismo e' una conseguenza dell'ereditarieta'. Ricordiamo che cosa e' un Handle. Esso e' una chiave di accesso a metodi, proprieta', eventi ed anche a campi Public dell'oggetto. Poiche' un oggetto della classe derivata e' anche un oggetto della classe base, noi possiamo riferirci ad esso tramite un handle della classe base. Ovviamente, in tal modo, potremo accedere solamente ai metodi della classe base.

Un primo vantaggio di questo fatto e' che, se abbiamo piu' classi che derivano tutte dalla stessa classe, possiamo trattarle tutte in maniera uniforme grazie ai metodi in comune. Con essi possiamo gestirle indipendentemete dal loro tipo. Pensiamo al problema della gestione di un insieme di oggetti. Gli elementi dell'insieme devono essere tutti dello stesso tipo. Con l'ereditarieta', se tutti gli oggetti che vogliamo trattare sono derivati dalla stessa classe base, il tipo comune e' proprio quello della classe base.

Inoltre, l'overriding permette di cambiare l'implementazione del metodo ereditato. Ora, supponiamo che tutte le classi derivate a cui appartengono gli oggetti dell'insieme abbiamo uno stesso metodo ereditato sovrascritto ciascuno in maniera differente. Invocando tale metodo per ogni oggetto dell'insieme vedermo comportamenti diversi.


' B e C derivano da A
'
' Classe A
'    |________Classe B
'    |
'    |________Classe C
'

Public Class A

    ' Resto della classe, omesso
	
    ' Metodo della classe base, A
    Public Sub Descrizione()
        MessageBox.Show("Sono la classe A")
    End Sub
    
End Class    
    
Public Class B

    ' Resto della classe, omesso
	
    ' Metodo della classe derivata B derivata da A
    Public Overrides Sub Descrizione()
        MessageBox.Show("Sono la classe B")
    End Sub
    
End Class

Public Class C

    ' Resto della classe, omesso
    	
    ' Metodo della classe derivata C derivata da A
    Public Overrides Sub Descrizione()
        MessageBox.Show("Sono la classe C")
    End Sub
    
End Class

...

' Test

Dim obj As A
Dim obj_b As New B()
Dim obj_c As New C()

obj = obj_b
obj.Descrizione() ' scrivera': Sono la classe B 

obj = obj_c
obj.Descrizione() ' scrivera': Sono la classe C 
    

Nell'esempio, abbiamo definito tre classi, A e' la classe base, B e C derivano da essa e cambiano l'implementazione del metodo 'Descrizione()' che e' ereditato. Nel test abbiamo dichiarato tre variabili oggetto e due oggetti, uno di tipo B e l'altro di tipo C. La variabile obj e' soltanto un riferimento ad un eventuale oggetto del tipo A. La prima assegnazione associa il riferimento obj all'oggetto obj_b, cio' e' possibile perche', benche' appartengano a tipi diversi, obj_b appartiene anche alla classe A. Successivamente all'assegnazione, invochiamo il metodo descrizione. Poiche' obj e' associato a obj_b e poiche' nella classe B 'Descrizione()' e' stato sovrascritto, il risultato dell'invocazione sara' il messaggio: Sono la classe B. Allo stesso modo quando associamo obj a obj_c otterremo l'altro messaggio: Sono la classe C. Al comportamento descritto si da' il nome di Polimorfismo perche', invocazioni di uno stesso metodo su oggetti diversi, ottiene risposte diverse.

Metodi statici

La differenza tra una classe ed un oggetto e' che un oggetto e' un'istanza della classe, la classe e' il modello dell'oggetto. Quindi, se vogliamo invocare un metodo dobbiamo prima creare l'oggetto. Esistono, pero', dei metodi che possono essere invocati indipendentemente dall'instanziamento. Tali metodi vengono detti statici o Shared in VB.NET. Insieme ai metodi anche i campi possono essere definiti statici. Si utilizza l'attributo Shared come mostra l'esempio. Un membro shared e' unico per tutte le istanze di una classe.

Public Class Test

    Public Shared membroCondiviso As Integer

    ' Altri campi ...
	
    Public Shared Sub Metodo_Shared()
        MessageBox.Show("shared")
    End Sub

    Public Sub Metodo_NonShared()
        MessageBox.Show("non shared")
    End Sub

	' Altri metodi ...
    
End Class    

...

'Verifica

Dim x As Integer
x = Test.membroCondiviso ' ok
Test.Metodo_Shared() ' ok
Test.Metodo_NonShared() ' Errore

dim obj As Test = New Test()
obj.Metodo_NonShared() ' ok

Interfacce

Come la maggior parte dei concetti relativi all'OOP, l'interfaccia rappresenta un modo di concepire il codice. L'interfaccia e' simile ad una classe, ma non e' istanziabile. Ogni interfaccia deve essere implementata, in quanto manca del codice vero e proprio. Implementiamo l'interfaccia con una classe che eredita da essa. L'interfaccia costituisce un modello, un insieme di metodi senza corpo che qualunque classe che la voglia implementare, dovra' definire.

Public Interface IStrumentoMusicale
    Sub Suona(ByVal Nota As Integer, ByVal Durata As Integer)
    Sub Pausa(ByVal Durata As Integer)
End Interface

Public Class Violino
    Implements IStrumentoMusicale
    Public Sub Suona(ByVal Nota As Integer, ByVal Durata As Integer) _
            Implements IStrumentoMusicale.Suona
        MessageBox.Show("Violino: suona")
    End Sub
    Public Sub Pausa(ByVal Durata As Integer) _
           Implements IStrumentoMusicale.Pausa
        MessageBox.Show("Violino: pausa")
    End Sub
End Class

Public Class Pianoforte
    Implements IStrumentoMusicale
    Public Sub Suona(ByVal Nota As Integer, ByVal Durata As Integer) _
           Implements IStrumentoMusicale.Suona
        MessageBox.Show("Pianoforte: suona")
    End Sub
    Public Sub Pausa(ByVal Durata As Integer) _
           Implements IStrumentoMusicale.Pausa
        MessageBox.Show("Pianoforte: pausa")
    End Sub
End Class

Public Class Chitarra
    Implements IStrumentoMusicale
    Public Sub Suona(ByVal Nota As Integer, ByVal Durata As Integer) _
           Implements IStrumentoMusicale.Suona
        MessageBox.Show("Chitarra: suona")
    End Sub
    Public Sub Pausa(ByVal Durata As Integer) _
           Implements IStrumentoMusicale.Pausa
        MessageBox.Show("Chitarra: pausa")
    End Sub
End Class

L'esempio presentato mostra un'interfaccia e tre implementazioni della stessa. L'interfaccia e' definita dal blocco Interface... End Interface. Notate che i metodi che essa propone sono prototipi, essi non hanno corpo, indicano i metodi che dovranno necessariamente essere implementati se si vuole ereditare dall'interfaccia. Le classi Violino, Pianoforte, Chitarra, implementano, ciascuna, tutti e due i metodi. Ovviamente ognuna li implementa in maniera diversa. Una classe indica che sta implementando un'interfaccia con la parola chiave Implements. La parola Implements e' usata anche nella definizione del metodo implementatore, con essa indichiamo quale metodo stiamo implementando.

In VB.NET, le interfacce vengono usate per simulare quello che in altri linguaggi viene detta ereditarieta' multipla. Una classe puo' infatti implementare piu' interfacce, ma non puo' ereditare da piu' di una classe.

Le interfacce sono utili, ad esempio, nella gestione degli eventi. Senza dilungarci, diciamo che potremmo creare un interfaccia EM che contenga i prototipi degli eventi del mouse, un'altra ET, che contenga quelli della tastiera , etc. Quando vogliamo essere informati di qualche evento, ci basta far implementare le interfacce appena dette da un oggetto A ed inserirlo nella lista di un oggetto B che fornisce gli eventi. Ad evento accaduto, l'oggetto B richiamera' il metodo relativo, definito in una delle suddette interfacce. Questo metodo, implementato dal nostro oggetto A, non solo viene invocato, notificando dell'evento occorso, ma e' in grado di rispondere adeguatamente con la sua implentazione.

Ultimo aggiornamento 13/01/2004