Guida al Visual Basic .NET
Capitolo 33° - I Distruttori
Avvertenza: questo è un capitolo molto tecnico. Forse vi sarà più utile in futuro. 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. Garbage CollectionQuesto è 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.FinalizeIl 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.Module Module1 Class Oggetto Sub 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 O As New Oggetto Console.WriteLine("Oggetto = Nothing") Console.WriteLine("L'applicazione sta per terminare.") End Sub End ModuleL'output sarà: 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: Sub Main() Dim O As 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 SubCiò che apparirà sullo schermo è: 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. DisposeSi 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.Class Oggetto 'Implementa l'interfaccia IDisposable Implements IDisposable 'File da scrivere: Dim W As IO.StreamWriter Sub New() 'Inizializza l'oggetto W = New IO.StreamWriter("C: est.txt") End Sub Public Sub Dispose() Implements IDisposable.Dispose 'Chiude il file W.Close() End Sub End ClassInvocando 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: Using [Oggetto] 'Codice da eseguire End Using 'Che corrisponde a scrivere: 'Codice da eseguire [Oggetto].Dispose() [Oggetto] = NothingPer 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. Usare Dispose e FinalizeCi 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:
Module Module1 Class FileWriter Implements IDisposable Private Writer As IO.StreamWriter 'Indica se l'oggetto è già stato distrutto con Dispose Private Disposed As Boolean 'Indica se il file è aperto Private Opened As 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 FileName As 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 Text As String) If Opened Then 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 Disposed Then 'Se è distrutto, esce dalla procedura Exit Sub End If If Disposing Then '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 = False End 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 F As New FileWriter 'Questo blocco mostra l'esecuzione di Dispose F.Open("C: est.txt") F.Write("Ciao") F.Close() 'Questo mostra l'esecuzione di Finalize F = New FileWriter F.Open("C: est2.txt") F = Nothing GC.Collect() GC.WaitForPendingFinalizers() Console.ReadKey() End Sub End ModuleL'output: 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.
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|