Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - I Distruttori

Guida al Visual Basic .NET

Capitolo 33° - I Distruttori

<< Precedente Prossimo >>
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 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.
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 Module 
L'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 Sub 
Ciò 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.


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.
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 Class 
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:
Using [Oggetto]
  'Codice da eseguire
End Using

'Che corrisponde a scrivere:
'Codice da eseguire
[Oggetto].Dispose()
[Oggetto] = Nothing 
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.


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.
A questo punto ci si deve preoccupare di due problemi che possono presentarsi: Finalize può essere chiamato anche dopo che l'oggetto è stato distrutto e le sue risorse deallocate con Dispose, quindi potrebbe tantare di distruggere un oggetto inesistente; il codice che viene eseguito in Finalize potrebbe far riferimento a oggetti inesistenti. Le convenzioni permettono di aggirare il problema facendo uso di versioni in overload di Dispose e di una variabile privata a livello di classe. La variabile booleana Disposed ha il compito di memorizzare se l'oggetto è stato distrutto: in questo modo eviteremo di ripetere il codice in Finalize. Il metodo in overload di Dispose accetta un parametro di tipo booleano, di solito chiamato Disposing, che indica se l'oggetto sta subendo un processo di distruzione manuale o di finalizzazione: procedendo con questo metodo si è certi di richiamare eventuali altri oggetti nel caso non ci sia finalizzazione. Il codice seguente implementa una semplicissima classe FileWriter e, tramite messaggi a schermo, visualizza quando e come l'oggetto viene rimosso dalla memoria:
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 Module 
L'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. 


<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...