Uso de objetos que implementan IDisposable
El recolector de elementos no utilizados (GC) de Common Language Runtime reclama la memoria usada por los objetos administrados. Normalmente, los tipos que usan recursos no administrados implementan la interfaz IDisposable o IAsyncDisposable para permitir que se recuperen los recursos no administrados. Cuando termine de usar un objeto que implemente IDisposable, debe llamar a la implementación Dispose o DisposeAsync del objeto para realizar explícitamente la limpieza. Puede hacerlo de una de las maneras siguientes:
- Mediante la instrucción o declaración
using
de C# (Using
en Visual Basic). - Mediante la implementación de un bloque
try/finally
y una llamada al método Dispose o DisposeAsync enfinally
.
Importante
El GC no elimina los objetos, ya que no tiene conocimiento de IDisposable.Dispose() ni de IAsyncDisposable.DisposeAsync(). El GC solo sabe si un objeto es finalizable (es decir, si define un método Object.Finalize()) y cuándo se debe llamar al finalizador del objeto. Para más información, consulte Cómo funciona la finalización. Para más información sobre la implementación de Dispose
y de DisposeAsync
, consulte:
Los objetos que implementan System.IDisposable o System.IAsyncDisposable siempre deben eliminarse correctamente, independientemente del ámbito de las variables, a menos que se indique lo contrario explícitamente. Los tipos que definen un finalizador para liberar recursos no administrados suelen llamar a GC.SuppressFinalize desde su implementación Dispose
o DisposeAsync
. La llamada a SuppressFinalize indica al GC que el finalizador ya se ha ejecutado y que el objeto no debe promoverse para la finalización.
La instrucción using
La instrucción using
de C# y la instrucción Using
de Visual Basic simplifican el código que se debe escribir para limpiar un objeto. La instrucción using
obtiene uno o más recursos, ejecuta las instrucciones especificadas y desecha el objeto automáticamente. Pero la instrucción using
solo resulta útil para los objetos que se usan dentro del ámbito del método en el que se construyen.
En el ejemplo siguiente se utiliza la instrucción using
para crear y liberar un objeto System.IO.StreamReader.
using System.IO;
class UsingStatement
{
static void Main()
{
var buffer = new char[50];
using (StreamReader streamReader = new("file1.txt"))
{
int charsRead = 0;
while (streamReader.Peek() != -1)
{
charsRead = streamReader.Read(buffer, 0, buffer.Length);
//
// Process characters read.
//
}
}
}
}
Imports System.IO
Module UsingStatement
Public Sub Main()
Dim buffer(49) As Char
Using streamReader As New StreamReader("File1.txt")
Dim charsRead As Integer
Do While streamReader.Peek() <> -1
charsRead = streamReader.Read(buffer, 0, buffer.Length)
'
' Process characters read.
'
Loop
End Using
End Sub
End Module
Una using
declaración es una sintaxis alternativa disponible donde se quitan las llaves, y cuyo ámbito es implícito.
using System.IO;
class UsingDeclaration
{
static void Main()
{
var buffer = new char[50];
using StreamReader streamReader = new("file1.txt");
int charsRead = 0;
while (streamReader.Peek() != -1)
{
charsRead = streamReader.Read(buffer, 0, buffer.Length);
//
// Process characters read.
//
}
}
}
Aunque la clase StreamReader implementa la interfaz IDisposable, que indica que usa un recurso no administrado, en el ejemplo no se llama al método StreamReader.Dispose de manera explícita. Cuando el compilador de C# o de Visual Basic encuentra la instrucción using
, emite un lenguaje intermedio (IL), que equivale al código siguiente que contiene un bloque try/finally
de manera explícita.
using System.IO;
class TryFinallyGenerated
{
static void Main()
{
var buffer = new char[50];
StreamReader? streamReader = null;
try
{
streamReader = new StreamReader("file1.txt");
int charsRead = 0;
while (streamReader.Peek() != -1)
{
charsRead = streamReader.Read(buffer, 0, buffer.Length);
//
// Process characters read.
//
}
}
finally
{
// If non-null, call the object's Dispose method.
streamReader?.Dispose();
}
}
}
Imports System.IO
Module TryFinallyGenerated
Public Sub Main()
Dim buffer(49) As Char
Dim streamReader As New StreamReader("File1.txt")
Try
Dim charsRead As Integer
Do While streamReader.Peek() <> -1
charsRead = streamReader.Read(buffer, 0, buffer.Length)
'
' Process characters read.
'
Loop
Finally
If streamReader IsNot Nothing Then DirectCast(streamReader, IDisposable).Dispose()
End Try
End Sub
End Module
La instrucción using
de C# también permite adquirir varios recursos en una sola instrucción, lo que internamente equivale a instrucciones using
anidadas. En el ejemplo siguiente se crean instancias de dos objetos StreamReader para leer el contenido de dos archivos.
using System.IO;
class SingleStatementMultiple
{
static void Main()
{
var buffer1 = new char[50];
var buffer2 = new char[50];
using StreamReader version1 = new("file1.txt"),
version2 = new("file2.txt");
int charsRead1, charsRead2 = 0;
while (version1.Peek() != -1 && version2.Peek() != -1)
{
charsRead1 = version1.Read(buffer1, 0, buffer1.Length);
charsRead2 = version2.Read(buffer2, 0, buffer2.Length);
//
// Process characters read.
//
}
}
}
Bloque try/finally
En lugar de ajustar un bloque try/finally
en una instrucción using
, puede elegir implementar el bloque try/finally
directamente. Puede adoptar este estilo como su método de creación de código personal, o bien puede hacer esto por una de las razones siguientes:
- Para incluir un bloque
catch
para controlar las excepciones iniciadas en el bloquetry
. De lo contrario, las excepciones iniciadas en la instrucciónusing
no se controlan. - Para crear instancias de un objeto que implementa IDisposable cuyo ámbito no es local en el bloque en el que se declara.
El siguiente ejemplo es similar al anterior, a excepción de que se usa un bloque try/catch/finally
para crear instancias, usar y eliminar un objeto StreamReader, y para controlar las excepciones que produce el constructor StreamReader y su método ReadToEnd. El código del bloque finally
comprueba que el objeto que implementa IDisposable no es null
antes de llamar al método Dispose. Si no se realiza este paso, se puede producir una excepción NullReferenceException en tiempo de ejecución.
using System;
using System.Globalization;
using System.IO;
class TryExplicitCatchFinally
{
static void Main()
{
StreamReader? streamReader = null;
try
{
streamReader = new StreamReader("file1.txt");
string contents = streamReader.ReadToEnd();
var info = new StringInfo(contents);
Console.WriteLine($"The file has {info.LengthInTextElements} text elements.");
}
catch (FileNotFoundException)
{
Console.WriteLine("The file cannot be found.");
}
catch (IOException)
{
Console.WriteLine("An I/O error has occurred.");
}
catch (OutOfMemoryException)
{
Console.WriteLine("There is insufficient memory to read the file.");
}
finally
{
streamReader?.Dispose();
}
}
}
Imports System.Globalization
Imports System.IO
Module TryExplicitCatchFinally
Sub Main()
Dim streamReader As StreamReader = Nothing
Try
streamReader = New StreamReader("file1.txt")
Dim contents As String = streamReader.ReadToEnd()
Dim info As StringInfo = New StringInfo(contents)
Console.WriteLine($"The file has {info.LengthInTextElements} text elements.")
Catch e As FileNotFoundException
Console.WriteLine("The file cannot be found.")
Catch e As IOException
Console.WriteLine("An I/O error has occurred.")
Catch e As OutOfMemoryException
Console.WriteLine("There is insufficient memory to read the file.")
Finally
If streamReader IsNot Nothing Then streamReader.Dispose()
End Try
End Sub
End Module
Puede seguir este patrón básico si elige implementar o debe implementar un bloque try/finally
, ya que el lenguaje de programación no admite una instrucción using
pero sí llamadas directas al método Dispose.
Miembros de instancia de IDisposable
Si una clase posee un campo o una propiedad de instancia y su tipo implementa IDisposable, la clase también debe implementar IDisposable. Para más información, consulte cómo implementar una eliminación en cascada.