Object Lifetime and Disposal
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is
a complete console program that starts with Option Strict On, was built with warnings treated as errors (except the
one that is meant to fail) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The
project’s root namespace is App, which is why a type name in an exception message reads App.LoggingScannerSession.
Visual Basic objects live on the .NET garbage-collected heap, so the language itself has very little to say about memory:
there is no delete, Set x = Nothing is no longer a cleanup ritual, and nothing is reference-counted. What remains is
the part the collector cannot do: releasing non-memory resources (file handles, sockets, database connections, native
handles) at a predictable moment. That is the job of IDisposable, the Using statement and, as a safety net, the
finalizer. This page shows how Visual Basic spells each of them, where the language differs from C# and which traps
are specific to VB. The runtime mechanics (generations, the large object heap, server and workstation modes, SafeHandle
internals, pooling and diagnostics) are the same for every .NET language and are described once, in
C#: Memory Management and Disposal.
How objects are created and destroyed
An object is created by New, which allocates it on the managed heap, runs the field initializers and then the
constructor (Sub New). It stays alive for as long as it is reachable: referenced from a static field, from a local
variable of a method that is still running (and the JIT may consider a local dead before its scope ends), from another
reachable object, or from a GC handle. When nothing reachable refers to it, it becomes garbage; the collector reclaims
its memory at some later point that you do not control and cannot observe from the language.
Two different things can therefore happen at the end of an object’s life, and they must not be confused.
| Concern | Memory | Other resources (handle, connection, lock) |
|---|---|---|
Who releases it |
the garbage collector |
the owner of the object, through |
When |
at an unspecified time after the object is unreachable |
at the end of a |
Visual Basic syntax |
nothing: just stop referencing the object |
|
Safety net |
not needed |
a finalizer ( |
The older VB6 Class_Terminate event, which ran immediately when the last reference disappeared, has no equivalent: .NET has
no reference counting. Code that was written for VB6 and relied on Set obj = Nothing to close a file, release a COM
server or run Class_Terminate has to be rewritten with Using (see
Migrating Legacy Visual Basic).
Class_Initialize became Sub New, which is described in
Classes and Objects.
Initialisation order and the virtual-call trap
The order in which a derived object is built is fixed, and it is the reverse of the order in which a C# programmer expects
the fields to be initialised. In Visual Basic the field initializers of a class run at the start of its own constructor,
after the call to MyBase.New has returned. In C# the field initializers of a class run before the base constructor
call. The consequence is that a virtual method called from a base constructor sees a derived object whose fields are
still Nothing or zero, in both languages, but in Visual Basic the derived field initializers have not run either:
Option Strict On
Public Class Base
Private ReadOnly _field As Integer = Trace("Base field initializer")
Public Sub New()
Console.WriteLine("Base.New")
Describe()
End Sub
Protected Overridable Sub Describe()
Console.WriteLine("Base.Describe")
End Sub
Protected Shared Function Trace(text As String) As Integer
Console.WriteLine(text)
Return 1
End Function
End Class
Public Class Derived
Inherits Base
Private ReadOnly _label As String = Describe2()
Public Sub New()
MyBase.New()
Console.WriteLine("Derived.New, label = " & _label)
End Sub
Private Shared Function Describe2() As String
Console.WriteLine("Derived field initializer")
Return "set"
End Function
Protected Overrides Sub Describe()
Console.WriteLine("Derived.Describe, label = " & If(_label, "<Nothing>"))
End Sub
End Class
Module Program
Sub Main()
Dim d As New Derived()
End Sub
End Module
Base field initializer
Base.New
Derived.Describe, label = <Nothing>
Derived field initializer
Derived.New, label = set
Derived.Describe ran while the object was only half built: _label was still Nothing. Never call an overridable member
(Overridable, MustOverride, Overrides) from a constructor; the compiler does not warn about it unless a code-analysis
rule such as CA2214 is enabled (see
XML Documentation, Analyzers and Code Style).
For the other half of the story, constructors that throw, see A constructor that throws.
Garbage collection in brief
The collector is generational, tracing and compacting, and it runs on its own schedule: when a generation’s allocation
budget is exhausted, when the machine is short of memory, or when you call GC.Collect. What matters for Visual Basic
code is a short list of consequences, all explained with the supporting numbers in the C# page.
-
Allocation is cheap and short-lived objects are cheaper still. Do not pool or cache small objects "to help the collector".
-
You cannot predict when an object is collected, and you cannot rely on a finalizer having run at any particular moment.
-
A local variable does not keep an object alive after its last use in optimised code, whatever its declared scope.
GC.KeepAlive(obj)marks a point up to which the object must stay reachable. -
GC.Collect()is for tests and diagnostics; calling it in production code is almost never justified.
The members you will meet are GC.Collect, GC.WaitForPendingFinalizers, GC.GetGeneration, GC.CollectionCount,
GC.GetTotalMemory, GC.SuppressFinalize, GC.ReRegisterForFinalize and GC.KeepAlive. They are ordinary static methods
of System.GC and need no Visual Basic-specific syntax.
The Using statement
Using guarantees that Dispose is called on a resource when control leaves the block, by any route: falling off the end,
Return, Exit Sub/Exit Function, GoTo, or an exception. It has been part of Visual Basic since Visual Basic 2005
(8.0). The compiler expands it into a Try ... Finally that calls Dispose if the resource is not Nothing.
Using resource1 [, resource2 ...]
' statements
End Using
resource: name [As [New] Type] = expression ' declared, named resource
name As New Type(arguments) ' declared, constructed in place
expression ' an existing variable or expression (the "expression form")
-
The resource type must implement
System.IDisposable; otherwise the compiler reports BC36010 (shown below). -
A declared resource variable is scoped to the block and is read-only inside it: assigning to it is BC30064.
-
Several resources can be listed in one statement, separated by commas. They are disposed in the reverse order of declaration, exactly as nested
Usingblocks would be. -
A resource may be
Nothing; theFinallyskips theDisposecall. This is whyUsing file As FileStream = TryOpen()is safe. -
In the expression form the variable is declared elsewhere and keeps existing after the block, but it has been disposed. The compiler captures the value of the expression when the block starts, so reassigning the variable inside the block does not change what is disposed.
-
There is no
Exit Usingstatement;Exitcan be followed only bySub,Function,Property,Do,For,While,SelectorTry(BC30240). UseReturninside a procedure, or restructure the block. -
There is no equivalent of the C# 8
usingdeclaration (using var x = ...;), which disposes at the end of the enclosing scope. A VBUsingis always a block. -
There is no
Await Using(see IAsyncDisposable without Await Using), and noUsingover aref structpattern-basedDispose, because Visual Basic cannot declareref structtypes.
Option Strict On
Public Class ShelfConnection
Implements IDisposable
Private ReadOnly _name As String
Public Sub New(name As String)
_name = name
Console.WriteLine("open " & _name)
End Sub
Public Sub Query(text As String)
Console.WriteLine("query " & _name & ": " & text)
If text = "boom" Then
Throw New InvalidOperationException("query failed on " & _name)
End If
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Console.WriteLine("close " & _name)
End Sub
End Class
Module Program
Sub Main()
' 1. Normal flow: two resources, disposed in reverse order of declaration.
Using primary As New ShelfConnection("primary"), replica As New ShelfConnection("replica")
primary.Query("SELECT 1")
replica.Query("SELECT 2")
End Using
Console.WriteLine("---")
' 2. An exception leaves the block: Dispose still runs before the Catch.
Try
Using connection As New ShelfConnection("failing")
connection.Query("boom")
Console.WriteLine("not reached")
End Using
Catch ex As InvalidOperationException
Console.WriteLine("caught: " & ex.Message)
End Try
Console.WriteLine("---")
' 3. A Using over an existing variable (the expression form) and over Nothing.
Dim existing As New ShelfConnection("expression")
Using existing
existing.Query("SELECT 3")
End Using
Dim nothingHere As ShelfConnection = Nothing
Using nothingHere
Console.WriteLine("Nothing is allowed: no Dispose call, no exception")
End Using
End Sub
End Module
open primary
open replica
query primary: SELECT 1
query replica: SELECT 2
close replica
close primary
---
open failing
query failing: boom
close failing
caught: query failed on failing
---
open expression
query expression: SELECT 3
close expression
Nothing is allowed: no Dispose call, no exception
Source: Using Statement
The three scenarios cover the whole contract. In the first, the replica is closed before the primary. In the second,
close failing is printed before caught, because the Finally that Using generates runs while the exception is
propagating, before the Catch that handles it. In the third, the expression form disposes a variable that was created
earlier, and Using over Nothing does nothing.
What the compiler rejects
Option Strict On
Public Class Tick
Implements IDisposable
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
End Class
Public Class NotDisposable
End Class
Module Program
Sub Main()
Using a As New NotDisposable()
End Using
Using t As New Tick()
t = New Tick()
End Using
Using b As IAsyncDisposable = Nothing
End Using
End Sub
End Module
This program is meant to fail, so it was built with warnings not treated as errors and the project was not run. The compiler printed:
Program.vb(14,15): error BC36010: 'Using' operand of type 'NotDisposable' must implement 'System.IDisposable'.
Program.vb(17,13): error BC30064: 'ReadOnly' variable cannot be the target of an assignment.
Program.vb(19,15): error BC36010: 'Using' operand of type 'IAsyncDisposable' must implement 'System.IDisposable'.
Source: BC36010: Using operand of type must implement System.IDisposable; BC30064: ReadOnly variable cannot be the target of an assignment
The third error is the important one: IAsyncDisposable does not satisfy Using, because the statement only knows the
synchronous interface. A type that implements both interfaces can be used in a Using, which then calls Dispose(), not
DisposeAsync().
Where to put the Try
Using replaces the Finally, not the Catch. Put the Try ... Catch around the Using, so that the resource is already
closed when the handler runs, as in scenario 2 above. Put the Using inside the Try for the usual cases, and inside a
Catch only for resources that the handler itself needs (for example a log writer). Do not wrap a Using in a Try that
catches Exception and swallows it: the resource is closed, but the error disappears (see
Structured Exception Handling).
An exception from Dispose replaces the original one
If both the body and Dispose throw, the exception from Dispose wins and the original one is lost, because the exception
thrown in the Finally replaces the exception in flight:
Option Strict On
Public Class Noisy
Implements IDisposable
Public Sub Dispose() Implements IDisposable.Dispose
Throw New InvalidOperationException("Dispose failed")
End Sub
End Class
Module Program
Sub Main()
Try
Using n As New Noisy()
Throw New ArgumentException("body failed")
End Using
Catch ex As Exception
Console.WriteLine("caught " & ex.GetType().Name & ": " & ex.Message)
End Try
End Sub
End Module
caught InvalidOperationException: Dispose failed
Source: Implement a Dispose method
The ArgumentException("body failed") thrown in the block never reaches the handler. This is why the guidelines say that
Dispose should not throw: it must be callable from a cleanup path. Real libraries swallow expected I/O failures inside
Dispose (for example when flushing a closed stream) for exactly this reason.
Deferred execution and a disposed resource
The most common Using bug in Visual Basic code that uses LINQ or iterators is returning a lazy sequence from inside the
block. The block ends when the method returns, the resource is disposed, and the sequence is enumerated afterwards by the
caller:
Option Strict On
Imports System.IO
Public Class Catalog
Public Shared Function Titles(text As String) As IEnumerable(Of String)
Using reader As New StringReader(text)
Return ReadAll(reader) ' deferred: the reader is disposed before anybody enumerates
End Using
End Function
Private Shared Iterator Function ReadAll(reader As TextReader) As IEnumerable(Of String)
Dim line = reader.ReadLine()
While line IsNot Nothing
Yield line
line = reader.ReadLine()
End While
End Function
Public Shared Function TitlesEager(text As String) As List(Of String)
Using reader As New StringReader(text)
Return ReadAll(reader).ToList() ' materialised while the reader is open
End Using
End Function
End Class
Module Program
Sub Main()
Dim text = "Dune" & vbLf & "Emma"
Console.WriteLine(String.Join(", ", Catalog.TitlesEager(text)))
Try
For Each title In Catalog.Titles(text)
Console.WriteLine(title)
Next
Catch ex As ObjectDisposedException
Console.WriteLine("lazy version failed: " & ex.GetType().Name)
End Try
End Sub
End Module
Dune, Emma
lazy version failed: ObjectDisposedException
Materialise (ToList, ToArray) before leaving the block, or return the resource’s owner so that the caller disposes it.
The same applies to Iterator functions that wrap a Using (the Using inside an iterator is disposed when the iterator is
disposed or completed, which a For Each does for you; see
Iterators and Yield) and to Async methods that return before
the awaited work that uses the resource has finished.
A constructor that throws
Using x As New T(...) calls the constructor before the protected region starts. If the constructor throws, there is no
object to dispose, and Dispose is not called:
Option Strict On
Public Class Reservation
Implements IDisposable
Public Sub New(copies As Integer)
Console.WriteLine("constructor starts")
If copies < 1 Then
Throw New ArgumentOutOfRangeException(NameOf(copies))
End If
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Console.WriteLine("Dispose called")
End Sub
End Class
Module Program
Sub Main()
Try
Using r As New Reservation(0)
Console.WriteLine("inside Using")
End Using
Catch ex As ArgumentOutOfRangeException
Console.WriteLine("constructor failed; Dispose was not called: " & ex.ParamName)
End Try
End Sub
End Module
constructor starts
constructor failed; Dispose was not called: copies
Source: Using Statement
A constructor that allocates two resources therefore has to clean up the first itself when the second allocation fails
(a Try ... Catch that disposes and rethrows inside Sub New), or must acquire the second one lazily. Listing two
resources in one Using statement is safe when the second constructor fails, because the first is already protected; it
does not help when a single constructor acquires two things.
Implementing IDisposable
Implement IDisposable when a class owns something that has to be released deterministically: a disposable field, an
unmanaged handle, a subscription, a lock or a temporary file. Do not implement it for types that only borrow a disposable
passed in from outside; the owner disposes it.
The simple case: a sealed class that owns managed disposables
Most classes only own other IDisposable objects. They need no finalizer and no Dispose(Boolean) method:
-
Mark the class
NotInheritable, or the dispose methodOverridable. -
Dispose each owned field in
Dispose, and makeDisposesafe to call more than once. -
Throw
ObjectDisposedExceptionfrom public members used after disposal.
The full dispose pattern
A class that is designed for inheritance, or that owns an unmanaged resource directly, uses the pattern below. The
members are Public Sub Dispose() (the interface implementation), Protected Overridable Sub Dispose(disposing As Boolean)
(the real work) and, only when a native handle is held directly, Protected Overrides Sub Finalize(). In Visual Basic the
interface member is attached with Implements IDisposable.Dispose instead of C#'s implicit matching, and the overload
Dispose(Boolean) shares the name with the parameterless one, which is allowed because overloads may differ by parameter
list.
Option Strict On
' The dispose pattern for a class that owns an unmanaged handle (simulated here).
Public Class ScannerSession
Implements IDisposable
Private _disposed As Boolean
Private _buffer As List(Of String) = New List(Of String)() ' managed resource
Private _handle As IntPtr = New IntPtr(42) ' stands in for an unmanaged resource
Public Sub Scan(code As String)
ObjectDisposedException.ThrowIf(_disposed, Me)
_buffer.Add(code)
Console.WriteLine("scanned " & code & " (" & _buffer.Count & " in buffer)")
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overridable Sub Dispose(disposing As Boolean)
If _disposed Then
Return
End If
If disposing Then
Console.WriteLine("Dispose(True): releasing managed state")
_buffer.Clear()
Else
Console.WriteLine("Dispose(False): running from the finalizer")
End If
Console.WriteLine("releasing handle " & _handle.ToString())
_handle = IntPtr.Zero
_disposed = True
End Sub
Protected Overrides Sub Finalize()
Try
Dispose(False)
Finally
MyBase.Finalize()
End Try
End Sub
End Class
Public Class LoggingScannerSession
Inherits ScannerSession
Protected Overrides Sub Dispose(disposing As Boolean)
Console.WriteLine("derived: disposing=" & disposing.ToString())
MyBase.Dispose(disposing)
End Sub
End Class
Module Program
Sub Main()
Dim session As New LoggingScannerSession()
session.Scan("9780306406157")
session.Dispose()
session.Dispose() ' idempotent: second call does nothing
Try
session.Scan("9780306406157")
Catch ex As ObjectDisposedException
Console.WriteLine("after Dispose: " & ex.GetType().Name & " for " & ex.ObjectName)
End Try
End Sub
End Module
scanned 9780306406157 (1 in buffer)
derived: disposing=True
Dispose(True): releasing managed state
releasing handle 42
derived: disposing=True
after Dispose: ObjectDisposedException for App.LoggingScannerSession
Source: Implement a Dispose method
Points worth reading in the output:
-
The second
session.Dispose()printedderived: disposing=Trueagain but nothing else: the_disposedflag in the base class made the real work idempotent, while the overriding method in the derived class ran each time. A derived class must callMyBase.Dispose(disposing)and must not assume that its own override runs once. -
Dispose()calledGC.SuppressFinalize(Me), so noDispose(False)line appears: the finalizer never ran. Without that call the object would stay in the finalization queue, survive one extra collection and run its cleanup a second time. -
ObjectDisposedException.ThrowIf(_disposed, Me)is the idiomatic guard. It is a static method of the exception class, available since .NET 7, and it fills inObjectNamefrom the object’s type. Older code writesIf _disposed Then Throw New ObjectDisposedException(Me.GetType().FullName).
| Rule | Reason |
|---|---|
|
|
|
a finalizer must not touch other managed objects, which may already be finalized |
Do not throw from |
an exception in the finalizer thread ends the process; one in |
Call |
avoids a pointless trip through the finalization queue |
Make the dispose-related members |
subclasses override the work; callers use the parameterless method |
Do not implement a finalizer for a class that only owns managed disposables |
it adds cost and risk for nothing |
SafeHandle instead of a finalizer
Writing Finalize is rarely the right choice today. A class that wraps a native handle should hold a
System.Runtime.InteropServices.SafeHandle (or one of its ready-made subclasses) in a field. SafeHandle has its own critical
finalizer and is reference-counted across P/Invoke calls, so the owning class needs only the simple pattern. How handles
are declared for Declare and DllImport calls is on
Native Interop: Declare and P/Invoke.
Source: SafeHandle Class
Finalize
A finalizer is the method the collector calls on a separate thread after it has found the object unreachable. In Visual Basic
it is a Protected Overrides Sub Finalize() that overrides Object.Finalize; there is no destructor syntax (~Name) as in C#.
-
The accessibility must be
Protected, and it must override: declaringPublic Sub Finalize()does not create a finalizer and the compiler reports a warning, BC40005 (the sub shadows the overridable base method). -
Visual Basic does not insert the call to the base class’s finalizer for you (example below); the documented pattern calls
MyBase.Finalize()in aFinallyso that the chain runs even when your own cleanup throws. -
The finalizers of a hierarchy run from the most derived class towards the base class, as the output of the first example shows.
-
Objects with a finalizer are allocated in a slower way, survive at least one extra collection and are promoted by it. Add one only when you hold an unmanaged resource that no
SafeHandlecovers. -
The finalizer thread runs while your code runs. A finalizer must not rely on state that other finalizers may have already cleaned up, and it must not block.
Option Strict On
Public Class Tracked
Private ReadOnly _name As String
Public Sub New(name As String)
_name = name
End Sub
Protected Overrides Sub Finalize()
Try
Console.WriteLine("finalizer of " & _name)
Finally
MyBase.Finalize()
End Try
End Sub
End Class
Public Class DerivedTracked
Inherits Tracked
Public Sub New()
MyBase.New("derived")
End Sub
Protected Overrides Sub Finalize()
Try
Console.WriteLine("finalizer of DerivedTracked")
Finally
MyBase.Finalize()
End Try
End Sub
End Class
Module Program
' Allocation in its own method that is not inlined, so that no local keeps the object alive.
<System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
Private Sub Allocate()
Dim unused As New DerivedTracked()
Console.WriteLine("allocated, generation " & GC.GetGeneration(unused).ToString())
End Sub
Sub Main()
Allocate()
Console.WriteLine("before collect")
GC.Collect()
GC.WaitForPendingFinalizers()
Console.WriteLine("after finalizers; gen0 collections so far >= 1: " & (GC.CollectionCount(0) >= 1).ToString())
End Sub
End Module
allocated, generation 0
before collect
finalizer of DerivedTracked
finalizer of derived
after finalizers; gen0 collections so far >= 1: True
Source: Object.Finalize Method
The method that allocates the object is marked NoInlining and returns before GC.Collect(), so that no local keeps it
reachable. In a Debug build the JIT extends a local’s lifetime to the end of its method, which is why finalizer demos that
allocate and collect in the same method often print nothing. GC.WaitForPendingFinalizers() blocks until the finalizer thread
has drained the queue. Neither call belongs in production code.
The base-class chain is not automatic. If an override omits MyBase.Finalize(), the base class’s finalizer silently never runs:
Option Strict On
Public Class Tick
Implements IDisposable
Private ReadOnly _n As String
Public Sub New(n As String)
_n = n
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Console.WriteLine("dispose " & _n)
End Sub
End Class
Public Class BaseF
Protected Overrides Sub Finalize()
Console.WriteLine("BaseF.Finalize")
MyBase.Finalize()
End Sub
End Class
Public Class DerivedF
Inherits BaseF
Protected Overrides Sub Finalize()
Console.WriteLine("DerivedF.Finalize without MyBase.Finalize call")
End Sub
End Class
Module Program
<System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
Private Sub Allocate()
Dim x As New DerivedF()
End Sub
Sub Main()
Dim t As New Tick("first")
Using t
t = New Tick("second")
End Using
Console.WriteLine("t is now " & t.GetType().Name)
Allocate()
GC.Collect()
GC.WaitForPendingFinalizers()
End Sub
End Module
dispose first
t is now Tick
DerivedF.Finalize without MyBase.Finalize call
Source: Object.Finalize Method
The output also shows two other facts. BaseF.Finalize is missing because the derived override never called it. And the
expression form Using t disposed the first Tick (dispose first): the compiler had captured the value when the block
started, so reassigning t inside the block did not redirect the disposal, and the second Tick is never disposed. The
variable is not a safe place to swap resources.
on process exit: cleanup that must happen has to be done by Dispose, a Using or a handler for
AppDomain.CurrentDomain.ProcessExit, not by relying on Finalize.
IAsyncDisposable without Await Using
Some types (streams with an asynchronous flush, database connections and contexts, asynchronous enumerators) implement
IAsyncDisposable, whose method DisposeAsync returns a ValueTask. C# consumes it with await using. Visual Basic 17.13 has
no such statement. Three language rules shape what you have to write instead.
-
Usingaccepts onlyIDisposable(BC36010 above); it cannot callDisposeAsync. -
Awaitis not allowed inside aFinallyorCatchblock (BC36943), so the obvious replacement,Try ... Finally Await x.DisposeAsync() End Try, does not compile. -
An
Async Functioncannot returnValueTask(BC36945: onlySub,TaskandTask(Of T)), so a VB class implementingIAsyncDisposablemust writeDisposeAsyncas an ordinary function that wraps aTask.
The workaround is to remember the exception, dispose after the Try block and rethrow with ExceptionDispatchInfo, which keeps
the original stack trace:
Option Strict On
Imports System.Runtime.ExceptionServices
Imports System.Threading.Tasks
Public Class AsyncShelf
Implements IAsyncDisposable
Public Async Function LoadAsync(fail As Boolean) As Task(Of Integer)
Await Task.Delay(10)
If fail Then
Throw New InvalidOperationException("load failed")
End If
Return 3
End Function
' Async Function ... As ValueTask is rejected by the compiler (BC36945): wrap a Task instead.
Public Function DisposeAsync() As ValueTask Implements IAsyncDisposable.DisposeAsync
Return New ValueTask(CloseAsync())
End Function
Private Shared Async Function CloseAsync() As Task
Await Task.Delay(10)
Console.WriteLine("DisposeAsync finished")
End Function
End Class
Module Program
Sub Main()
Console.WriteLine(RunAsync(False).GetAwaiter().GetResult())
Try
RunAsync(True).GetAwaiter().GetResult()
Catch ex As InvalidOperationException
Console.WriteLine("rethrown after DisposeAsync: " & ex.Message)
End Try
End Sub
' There is no Await Using, and Await is not allowed in Finally (BC36943):
' remember the failure, dispose after the Try block, then rethrow.
Private Async Function RunAsync(fail As Boolean) As Task(Of Integer)
Dim shelf As New AsyncShelf()
Dim result As Integer
Dim failure As Exception = Nothing
Try
result = Await shelf.LoadAsync(fail)
Catch ex As Exception
failure = ex
End Try
Await shelf.DisposeAsync()
If failure IsNot Nothing Then
ExceptionDispatchInfo.Capture(failure).Throw()
End If
Return result
End Function
End Module
DisposeAsync finished
3
DisposeAsync finished
rethrown after DisposeAsync: load failed
Source: Implement a DisposeAsync method
Types that implement both interfaces (most streams do) can be used with a normal Using; that calls Dispose(), which may block
a thread while it flushes. For a method that is already asynchronous, prefer the explicit pattern above when the type’s
DisposeAsync does real work. BC36943 and BC36945 had no page of their own on Learn when this page was checked; the rules above are the
compiler’s own messages.
Source: IAsyncDisposable Interface
For Async/Await itself see Async and Await.
Nothing, Erase and the end of a variable’s life
Visual Basic programmers coming from VB6 often clear references out of habit. In .NET this rarely matters.
-
x = Nothingsets one variable to the default of its type. It does not free the object, does not callDispose, and does not affect any other variable that refers to the same object. -
A local variable that is no longer used is not a root in optimised code, so setting it to
Nothingbefore the end of the method changes nothing. In a Debug build it changes how long the object is reachable, which is rarely what you want. -
Setting a field or static to
Nothingdoes matter: it removes a root that would otherwise live as long as its owner. That is how a long-lived cache or singleton releases data, and how a class breaks a cycle it created on purpose. -
Erase arraysets an array variable toNothing. It releases nothing else, andArray.Clearis the call that resets elements without changing the variable.
WeakReference(Of T) is the supported way to hold an object without keeping it alive, for example in a cache the collector
may trim:
Option Strict On
Public Class Weak
' A cache entry that the garbage collector may drop.
Public Shared Sub Demo()
Dim strong As New List(Of Integer) From {1, 2, 3}
Dim weakRef As New WeakReference(Of List(Of Integer))(strong)
Dim target As List(Of Integer) = Nothing
Console.WriteLine("alive while referenced: " & weakRef.TryGetTarget(target).ToString())
target = Nothing
GC.KeepAlive(strong) ' keeps strong reachable up to here, even in optimised code
End Sub
<System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
Public Shared Function Create() As WeakReference(Of List(Of Integer))
Return New WeakReference(Of List(Of Integer))(New List(Of Integer) From {1, 2, 3})
End Function
End Class
Module Program
Sub Main()
Weak.Demo()
Dim weakRef = Weak.Create()
GC.Collect()
GC.WaitForPendingFinalizers()
Dim target As List(Of Integer) = Nothing
Console.WriteLine("after collection: " & weakRef.TryGetTarget(target).ToString())
' Setting a local to Nothing does not free anything by itself; it only drops one reference.
Dim big As Integer() = New Integer(99999) {}
Dim alias1 = big
big = Nothing
Console.WriteLine("alias still sees " & alias1.Length.ToString() & " elements")
' Erase on an array variable sets the variable to Nothing.
Dim numbers As Integer() = {1, 2, 3}
Erase numbers
Console.WriteLine("after Erase, numbers Is Nothing: " & (numbers Is Nothing).ToString())
End Sub
End Module
alive while referenced: True
after collection: False
alias still sees 100000 elements
after Erase, numbers Is Nothing: True
Source: Weak References
The weak reference is True while strong is reachable and False after a collection once the only strong reference has gone.
Setting big = Nothing left alias1 intact, which shows that Nothing removes a reference, not an object. Use TryGetTarget
rather than a separate "is alive" check followed by a read, since the target can disappear between the two calls. Never use a
weak reference to track the lifetime of an object whose cleanup matters; that is `Dispose’s job.
Source: Erase Statement
Event handlers keep objects alive
The garbage collector follows delegates. AddHandler publisher.Changed, AddressOf OnChanged stores a delegate inside the publisher
that references the subscriber, so a subscriber stays reachable for as long as the publisher does. The same is true of
WithEvents fields, which VB turns into AddHandler calls inside the property setter. A short-lived subscriber attached to
a long-lived publisher (a static event, a singleton, a main window) therefore leaks until it calls RemoveHandler.
Option Strict On
Public Class Publisher
Public Event Changed As EventHandler
End Class
Public Class Subscriber
Public Sub New(source As Publisher)
AddHandler source.Changed, AddressOf OnChanged
End Sub
Private Sub OnChanged(sender As Object, e As EventArgs)
End Sub
End Class
Module Program
<System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)>
Private Function Attach(source As Publisher) As WeakReference
Return New WeakReference(New Subscriber(source))
End Function
Sub Main()
Dim publisher As New Publisher()
Dim weak = Attach(publisher)
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
Console.WriteLine("subscriber still alive after collection: " & weak.IsAlive.ToString())
GC.KeepAlive(publisher)
End Sub
End Module
subscriber still alive after collection: True
Source: AddHandler Statement
Nothing refers to the Subscriber object from the program except the event’s delegate list, and it survives the collection. The
fix is symmetrical bookkeeping: RemoveHandler in Dispose (making the subscriber IDisposable), a weak-event pattern for UI
frameworks, or a lifetime that is the same for publisher and subscriber. See
Events and Handlers.
Structures, interfaces and the compiler
-
A
Structurecan implementIDisposable, but a structure is copied on assignment and when boxed to the interface, so aDisposethat changes the structure’s own fields may act on a copy; keep disposable types as classes. See Structures and Value Types. -
Interface members that return a disposable resource must document who disposes it. A method named
Open...,Create...orGet...Streamreturns ownership to the caller, who must dispose; a property that exposes an owned field does not. -
The
IDisposableimplementation of an interface-typed variable is not visible unless the interface inheritsIDisposable; useTryCast(x, IDisposable)to dispose conditionally. See Interfaces. -
Code analyzers report undisposed locals (CA2000), types that own disposable fields without implementing
IDisposable(CA1001) and incorrect implementations (CA1063, CA2213, CA2215); enable them as described on XML Documentation, Analyzers and Code Style.
Typical resources and who disposes them
| Resource | What to do | Page |
|---|---|---|
|
|
|
|
|
|
|
create once and reuse, or obtain from |
|
|
|
|
COM objects and Office automation |
release explicitly with |
|
Windows Forms controls and forms |
a modal |
Differences from C# and from older Visual Basic
| Topic | Visual Basic | C# |
|---|---|---|
Block form |
|
|
Declaration form (disposed at end of scope) |
not available |
|
Several resources |
one statement, comma separated |
nested blocks or a chain of |
Async disposal |
no |
|
|
not applicable (no |
pattern-based |
Finalizer |
|
|
Field initializers vs base constructor |
after |
before the base constructor call |
|
no |
|
|
none; use |
not applicable |
|
only removes a reference |
not applicable |
|
a |
|
Older material you may meet
Textbooks and tutorials written for the .NET Framework era describe a world that has mostly gone:
-
The
Disposepattern is still the same, butFinalize-based cleanup was often shown as the first line of defence. Today the preferred order is: use aUsing; own aSafeHandleinstead of a raw handle; writeFinalizeonly as a last resort. -
Examples that close files with
FileClose/FileOpenandSet x = Nothingbelong to the VB6 compatibility library (see The VB Family: VB6, VBA and VBScript). New code usesUsingwithStreamReader/StreamWriter. -
Book listings that open a stream or
File.Createwithout disposing it are leaking a handle and, for writers, may lose buffered data. The fix is always the same: put the object in aUsing. -
Singleton examples that rely on a finalizer to release resources at shutdown do not work on .NET (Core), which does not run finalizers at exit.
-
Marshal.ReleaseComObjectexamples for Office were written for a world where every COM reference had to be counted; the rules are on the COM page. -
GC.Collect()"to free memory before an operation" appears in old tutorials; modern guidance is not to call it.
Pitfalls
-
Forgetting
Usingon a reader, writer, connection or process object. The file stays locked until the finalizer runs, which may be never in a short-lived process. -
Returning a lazy
IEnumerable(Of T)from inside aUsing. -
Throwing from
Dispose, which masks the original exception. -
Subscribing with
AddHandler(orWithEvents) and never unsubscribing. -
Calling an overridable member from
Sub New. -
Disposing an object you do not own, such as a stream passed in by a caller, or a shared
HttpClient. -
Expecting
Await Using, orAwaitinFinally, to compile. -
Relying on
Finalizeto run at process exit on .NET. -
Using
GC.Collect()orGC.WaitForPendingFinalizers()to "fix" a leak. A leak is a reachable object: find the root with a memory profiler (see Debugging and Diagnostics).
References
-
Fundamentals of garbage collection, Cleaning up unmanaged resources and Using objects that implement IDisposable
-
Implement a Dispose method and Implement a DisposeAsync method
-
Dispose Pattern (.NET Framework Design Guidelines)
-
IDisposable, IAsyncDisposable, GC, WeakReference(Of T), ObjectDisposedException and SafeHandle