A33. Distruttori
Gli oggetti COM (Component Object Model) utilizzati dal vecchio VB6 possedevano una caratteristica peculiare che permetteva di
determinare quando non vi fosse più bisogno di loro e la memoria associata potesse essere rilasciata: erano dotati di un reference counter,
ossia di un "contatore di riferimenti". Ogni volta che una variabile veniva impostata su un oggetto COM, il contatore veniva aumentato
di 1, mentre quando quella variabile veniva distrutta o se ne cambiava il valore, il contatore scendeva di un'unità. Quando tale valore
raggiungeva lo zero, gli oggetti venivano distrutti. Erano presenti alcuni problemi di corruzione della memoria, però: ad esempio se due
oggetti si puntavano vicendevolmente ma non erano utilizzati dall'applicazione, essi non venivano distrutti (riferimento circolare).
Il meccanismo di gestione della memoria con il .Net Framework è molto diverso, e ora vediamo come opera.
Anche se ci siamo divertiti con Finalize, questo metodo deve essere definito solo se strettamente necessario, per alcune ragioni. La prima è che il GC impiega non uno, ma due cicli per finalizzare un oggetto in cui è stata definita Finalize dal programmatore. Il motivo consiste nella possibilità che venga usata la cosiddetta resurrezione dell'oggetto: in questa tecnica, ad una variabile globale viene assegnato il riferimento alla classe stessa usando Me; dato che in questo modo c'è ancora un riferimento valido all'oggetto, questo non deve venire distrutto. Tuttavia, per rilevare questo fenomeno, il GC impiega due cicli e si rischia di occupare memoria inutile. Inoltre, sempre per questa causa, si impiega più tempo macchina che potrebbe essere speso in altro modo.
Il meccanismo di gestione della memoria con il .Net Framework è molto diverso, e ora vediamo come opera.
Garbage Collection
Questo è il nome del processo sul quale si basa la gestione della memoria del Framework. Quando l'applicazione tenta di creare un nuovo oggetto
e lo spazio disponibile nell'heap managed scarseggia, viene messo in moto questo meccanismo, attraverso l'attivazione del Garbage Collector.
Per prima cosa vengono visitati tutti gli oggetti presenti nello heap: se ce n'è uno che non è raggiungibile dall'applicazione, questo
viene distrutto. Il processo è molto sofisticato, in quanto è in grado di rilevare anche dipendenze indirette, come classi non
raggiungibili direttamente, referenziate da altre classi che sono raggiungibili direttamente; riesce anche a risolvere il problema opposto,
quello del riferimento circolare. Se uno o più oggetti non vengono distrutti perchè sono necessari al programma per funzionare, si dice
che essi sono sopravvissuti a una Garbage Collection e appartengono alla generazione 1, mentre quelli inizializzati che non hanno subito
ancora nessun processo di raccolta della memoria sono di generazione 0. L'indice generazionale viene incrementato di uno fino ad un massimo
di 2. Questi ultimi oggetti sono sopravvissuti a molti controlli, il che significa che continuano a essere utilizzati nello stesso modo: perciò
il Garbage Collector li sposta in una posizione iniziale dell'heap managed, in modo che si dovranno eseguire meno operazioni di spostamento
della memoria in seguito. La stessa cosa vale per le generazioni successive.
Questo sistema assicura che ci sia sempre spazio libero, ma non garantisce che ogni oggetto logicamente
distrutto lo sia anche fisicamente: se per quegli oggetti che allocano solo memoria il problema è relativo, per altri che utilizzano
file e risorse esterne, invece, diventa più complicato. Il compito di rilasciare le risorse spetta quindi al programmatore, che dovrebbe,
in una classe ideale, preoccuparsi che quando l'oggetto venga distrutto lo siano correttamente anche le risorse ad esso associate. Bisogna
quindi fare eseguire del codice appena prima della distruzione: come? lo vediamo ora.Finalize
Il metodo Finalize di un oggetto è speciale, poichè viene richiamato dal Garbage Collector "in persona" durante la raccolta della memoria.
Come già detto, non è possibile sapere quando un oggetto logicamente distrutto lo sarà anche fisicamente, quindi Finalize potrebbe essere
eseguito anche diversi secondi, o minuti, o addirittura ore, dopo che sia stato annullato ogni riferimento all'oggetto. Come seconda
clausola importante, è necessario non accedere mai ad oggetti esterni in una procedura Finalize: dato che il GC (acronimo di garbage
collector) può distruggere gli oggetti in qualsiasi ordine, non si può essere sicuri che l'oggetto a cui si sta facendo riferimento esista
ancora o sia già stato distrutto. Questo vale anche per oggetti singleton come Console o Application, o addirittura per i tipi String, Byte,
Date e tutti gli altri (dato che, essendo anch'essi istanze di System.Type, che definisce le caratteristiche di ciascun tipo, sono soggetti
alla GC alla fine del programma). Per sapere se il processo di distruzione è stato avviato dalla chiusura del programma si può richiamare
una semplice proprietà booleana, Environment.HasShutdownStarted. Per esemplificare i concetti, in questo paragrafo farò uso dell'oggetto
singleton GC, che rappresenta il Garbage Collector, permettendo di avviare forzatamente la raccolta della memoria e altre cose: questo
non deve mai essere fatto in un'applicazione reale, poichè potrebbe comprometterne le prestazioni.
L'output sarà:Module Module1Class OggettoSub New () Console.WriteLine("Un oggetto sta per essere creato.")End Sub 'La procedura Finalize è definita in System.Object, quindi, per 'ridefinirla dobbiamo usare il polimorfismo. Inoltre deve essere 'dichiarata Protected, poichè non può essere richiamata da altro 'ente se non dal GC e allo stesso tempo è ereditabile Protected Overrides Sub Finalize() Console.WriteLine("Un oggetto sta per essere distrutto.") 'Blocca il programma per 4 secondi circa, consentendoci di 'vedere cosa viene scritto a schermo System.Threading.Thread.CurrentThread.Sleep(4000)End Sub End Class Sub Main()Dim OAs New Oggetto Console.WriteLine("Oggetto = Nothing") Console.WriteLine("L'applicazione sta per terminare.")End Sub End Module
Un oggetto sta per essere creato. Oggetto = Nothing L'applicazione sta per terminare. Un oggetto sta per essere distrutto.Come si vede, l'oggetto viene distrutto dopo il termine dell'applicazione (siamo fortunati che Console è ancora "in vita" prima della distruzione): questo significa che c'era abbastanza spazio disponibile da non avviare la GC, che quindi è stata rimandata fino alla fine del programma. Riproviamo invece in questo modo:
Ciò che apparirà sullo schermo è:Sub Main()Dim OAs New Oggetto O = Nothing Console.WriteLine("Oggetto = Nothing") 'NON PROVATECI A CASA! 'Forza una garbage collection GC.Collect() 'Attende che tutti i metodi Finalize siano stati eseguiti GC.WaitForPendingFinalizers() Console.WriteLine("L'applicazione sta per terminare.") Console.ReadKey()End Sub
Un oggetto sta per essere creato. Oggetto = Nothing Un oggetto sta per essere distrutto. L'applicazione sta per terminare.Si vede che l'ordine delle ultime due azioni è stato cambiato a causa delle GC avviata anzi tempo prima del termine del programma.
Anche se ci siamo divertiti con Finalize, questo metodo deve essere definito solo se strettamente necessario, per alcune ragioni. La prima è che il GC impiega non uno, ma due cicli per finalizzare un oggetto in cui è stata definita Finalize dal programmatore. Il motivo consiste nella possibilità che venga usata la cosiddetta resurrezione dell'oggetto: in questa tecnica, ad una variabile globale viene assegnato il riferimento alla classe stessa usando Me; dato che in questo modo c'è ancora un riferimento valido all'oggetto, questo non deve venire distrutto. Tuttavia, per rilevare questo fenomeno, il GC impiega due cicli e si rischia di occupare memoria inutile. Inoltre, sempre per questa causa, si impiega più tempo macchina che potrebbe essere speso in altro modo.
Dispose
Si potrebbe definire Dispose come un Finalize manuale: esso permetto di rilasciare qualsiasi risorsa che non sia la memoria (ossia connessioni
a database, files, immagini, pennelli, oggetti di sistema, eccetera...) manualmente, appena prima di impostare il riferimento a Nothing. In
questo modo non si dovrà aspettare una successiva GC affinchè sia rilasciato tutto correttamente. Dispose non è un metodo definito da
tutti gli oggetti, e perciò ogni classe che intende definirlo deve implementare l'interfaccia IDisposable (per ulteriori informazioni sulle
interfacce, vedere capitolo 36): per ora prendete per buono il codice che fornisco, vedremo in seguito più approfonditamente l'agormento
delle interfacce.
Invocando il metodo Dispose di Oggetto, è possibile chiudere il file ed evitare che venga lasciato aperto. Il Vb.Net fornisce un costrutto, valido per tutti gli oggetti che implementano l'interfaccia IDisposable, che si assicura di richiamare il metodo Dispose e impostare il riferimento a Nothing automaticamente dopo l'uso. La sintassi è questa:Class Oggetto 'Implementa l'interfaccia IDisposable Implements IDisposable 'File da scrivere: Dim WAs IO.StreamWriterSub New () 'Inizializza l'oggetto W =New IO.StreamWriter("C:\test.txt")End Sub Public Sub Dispose()Implements IDisposable.Dispose 'Chiude il file W.Close()End Sub End Class
Per convenzione, se una classe implementa un'interfaccia IDisposable e contiene altre classi nidificate o altri oggetti, il suo metodo Dispose deve richiamare il Dispose di tutti gli oggetti interni, almeno per quelli che ce l'hanno. Altra convenzione è che se viene richiamata Dispose da un oggetto già distrutto logicamente, deve generarsi l'eccezione ObjectDisposedException.Using [Oggetto] 'Codice da eseguire End Using 'Che corrisponde a scrivere: 'Codice da eseguire [Oggetto].Dispose() [Oggetto] =Nothing
Usare Dispose e Finalize
Ci sono alcune circostanze che richiedono l'uso di una sola delle due, altre che non le richiedono e altre ancora che dovrebbero rcihiederle
entrambe. Segue una piccola lista di suggerimenti su come mettere in pratica questi meccanismi:
- Nè Dispose, nè Finalize: la classe impiega solo la memoria come unica risorsa o, se ne impiegate altre, le rilascia prima di terminare le proprie operazioni.
- Solo Dispose: la classe impiega risorse facendo riferimento ad altri oggetti .Net e si vuole fornire al chiamante la possibilità di rilasciare tali risorse il prima possibile.
- Dispose e Finalize: la classe impiega direttamente una risorsa, ad esempio invocando un metodo di una libreria unmanaged, che richiede un rilascio esplicito; in più si vuole fornire al client la possibilità di deallocare manualmente gli oggetti.
- Solo Finalize: si deve eseguire un certo codice prima della distruzione.
L'output:Module Module1Class FileWriterImplements IDisposablePrivate WriterAs IO.StreamWriter 'Indica se l'oggetto è già stato distrutto con Dispose Private DisposedAs Boolean 'Indica se il file è aperto Private OpenedAs Boolean Sub New () Disposed = False Opened = False Console.WriteLine("FileWriter sta per essere creato.") 'Questa procedura comunica al GC di non richiamare più 'il metodo Finalize per questo oggetto. Scriviamo ciò 'perchè se file non viene esplicitamente aperto con Open 'non c'è alcun bisogno di chiuderlo GC.SuppressFinalize(Me )End Sub 'Apre il file Public Sub Open(ByVal FileNameAs String ) Writer =New IO.StreamWriter(FileName) Opened = True Console.WriteLine("FileWriter sta per essere aperto.") 'Registra l'oggetto per eseguire Finalize: ora il file è 'aperto e può quindi essere chiuso GC.ReRegisterForFinalize(Me )End Sub 'Scrive del testo nel file Public Sub Write(ByVal TextAs String )If OpenedThen Writer.Write(Text)End If End Sub 'Una procedura analoga a Open aiuta a impostare meglio l'oggetto 'e non fa altro che richiamare Dispose: è più una questione di 'completezza Public Sub Close() Dispose()End Sub 'Questa versione è in overload perchè l'altra viene chiamata solo 'dall'utente (è Public), mentre questa implementa tutto il codice 'che è necessario eseguire per rilasciare le risorse 'Il parametro Disposing indica se l'oggetto sta per essere distrutto 'quindi manualmente, o finalizzato, quindi nel processo di GC: nel 'secondo caso altri oggetti che questa classe utilizza potrebbero 'non esistere più, perciò si deve controllare se è possibile 'invocarli correttamente Protected Overridable Overloads Sub Dispose(ByVal Disposing _As Boolean ) 'Esegue il codice solo se l'oggetto esiste ancora If DisposedThen 'Se è distrutto, esce dalla procedura Exit Sub End If If DisposingThen 'Qui possiamo chiamare altri oggetti con la sicurezza che 'esistano ancora Console.WriteLine("FileWriter sta per essere distrutto.")Else Console.WriteLine("FileWriter sta per essere finalizzato.")End If 'Chiude il file Writer.Close() Disposed = True Opened = FalseEnd Sub Public Overloads Sub Dispose()Implements IDisposable.Dispose 'L'oggetto è stato distrutto Dispose(True) 'Quindi non deve più essere finalizzato GC.SuppressFinalize(Me )End Sub Protected Overrides Sub Finalize() 'Processo di finalizzazione: Dispose(False)End Sub End Class Sub Main()Dim FAs New FileWriter 'Questo blocco mostra l'esecuzione di Dispose F.Open("C:\test.txt") F.Write("Ciao") F.Close() 'Questo mostra l'esecuzione di Finalize F =New FileWriter F.Open("C:\test2.txt") F =Nothing GC.Collect() GC.WaitForPendingFinalizers() Console.ReadKey()End Sub End Module
FileWriter sta per essere creato. FileWriter sta per essere aperto. FileWriter sta per essere distrutto. FileWriter sta per essere creato. FileWriter sta per essere aperto. FileWriter sta per essere finalizzato.
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



