Menu

Instrução using em C#: IDisposable, Dispose e using var

A instrução using garante que o Dispose seja chamado em arquivos, streams, conexões e outros objetos IDisposable, mesmo quando uma exceção é lançada. Veja para o que ela é compilada, a ordem em que os objetos são descartados, a declaração using do C# 8 e como implementar IDisposable na sua própria classe.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Alguns objetos guardam recursos que o garbage collector não gerencia: arquivos abertos, sockets de rede, conexões de banco de dados, handles do sistema operacional. Eles implementam IDisposable, e você libera o recurso chamando Dispose(). A instrução using faz essa chamada por você em um ponto fixo, mesmo se uma exceção for lançada.

Esta página é sobre a instrução using. As linhas using System; no topo de um arquivo são outro recurso, a diretiva using, que importa um namespace.

Um bloco using

using (declaration) { body } cria o objeto, executa o corpo e depois chama Dispose. Uma classe pequena que imprime algo no Dispose mostra exatamente quando isso acontece:

Saída:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

O segundo bloco lança uma exceção, e "close api" ainda é impresso, antes de o catch tratar a exceção. Essa garantia é o motivo de usar using em vez de chamar Dispose() você mesmo na última linha: uma chamada manual é pulada por qualquer exceção lançada antes dela.

Para o que o using é compilado

O compilador expande um bloco using em um try/finally. Estes dois são equivalentes:

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

Três detalhes vêm dessa expansão. A variável tem escopo no bloco e é somente leitura dentro dele (você não pode reatribuir writer). O objeto precisa implementar IDisposable, senão o código não compila. E um valor null é permitido: o Dispose simplesmente é pulado, o que é útil quando uma fábrica pode não retornar nada.

Arquivos e streams

Classes de arquivo e de stream são o caso do dia a dia. Um StreamWriter guarda texto em um buffer na memória, e o Dispose descarrega o buffer no disco e fecha o handle do arquivo:

Saída:

id,total
1001,59.90
1002,12.50

Sem Dispose, as últimas linhas do writer podem ainda estar no buffer, e no Windows o handle aberto impede outros programas (e o seu próximo File.Delete) de mexer no arquivo. Auxiliares como File.WriteAllText e File.ReadAllLines abrem e descartam internamente, então não precisam de using. Veja arquivos para mais.

Vários recursos: ordem de descarte

Empilhe instruções using sem chaves entre elas para abrir vários recursos em um bloco. Eles são descartados na ordem inversa da criação:

Saída:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

A ordem inversa é o que recursos em camadas precisam: um writer envolve um buffer que envolve um arquivo, então o writer precisa descarregar no buffer antes de o buffer gravar no arquivo, antes de o arquivo fechar.

A declaração using (C# 8)

O C# 8 adicionou uma forma sem chaves. A variável é descartada quando o escopo que a contém termina, normalmente o fim do método:

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

Ela remove um nível de indentação, o que faz diferença quando um método usa dois ou três recursos. A regra da ordem de descarte é a mesma: declarações em um mesmo escopo são descartadas na ordem inversa. A contrapartida é que o recurso fica aberto até o escopo terminar, então quando um arquivo deve ser fechado antes de o método fazer mais trabalho, mantenha a forma de bloco ou coloque a declaração em um bloco { } próprio.

Para objetos cuja limpeza é assíncrona (IAsyncDisposable, como muitas conexões de banco de dados e streams), o C# 8 também tem await using, que aguarda DisposeAsync() no mesmo ponto.

Implementando IDisposable na sua própria classe

Implemente IDisposable quando sua classe é dona de algo descartável (ela criou um stream, um timer, uma conexão) e por isso precisa descartá-lo também. No caso comum, é um método curto:

Saída:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

As regras que isto segue: Dispose descarta tudo de que o objeto é dono; chamá-lo mais de uma vez não faz nada na segunda vez; e usar o objeto depois do descarte lança ObjectDisposedException. using (log) também mostra que o bloco pode receber uma variável existente em vez de uma declaração.

O "dispose pattern" mais longo, com um método protected virtual void Dispose(bool disposing) e um finalizador, só é necessário quando uma classe guarda diretamente um handle não gerenciado (um ponteiro bruto vindo de código nativo). No .NET moderno, envolva esses handles em uma subclasse de SafeHandle, e a versão simples acima é tudo de que sua classe precisa.

Erros comuns

  • Retornar um objeto criado em um bloco using. Ele é descartado quando o bloco termina, então quem chama recebe um stream fechado. Retorne os dados, ou deixe quem chama ser dono do using.
  • Esquecer o using em streams e writers. Dados em buffer podem se perder e os arquivos ficam bloqueados.
  • Descartar um objeto compartilhado. Descarte só o que é seu. Um HttpClient de vida longa, por exemplo, é feito para ser compartilhado e reutilizado, não criado e descartado a cada requisição.
  • Usar um objeto depois do bloco. Métodos em um objeto descartado lançam ObjectDisposedException.
  • Depender do garbage collector. Ele libera memória, não arquivos nem sockets, e nunca chama Dispose.

Perguntas frequentes

O que a instrução using faz em C#?

using (var x = ...) { ... } chama x.Dispose() quando o bloco termina, seja normalmente, por um return ou porque uma exceção foi lançada. O compilador a transforma em um try/finally com a chamada a Dispose no finally, então o recurso é liberado em um ponto conhecido, e não quando o garbage collector resolver executar.

O que é IDisposable em C#?

IDisposable é uma interface com um método, void Dispose(). Uma classe a implementa quando guarda algo que precisa ser liberado explicitamente: um handle de arquivo, um socket de rede, uma conexão de banco de dados, um timer. Todo objeto cujo tipo implementa IDisposable deve ser descartado quando você terminar de usá-lo, normalmente com using.

Qual a diferença entre using e using var?

using (var x = ...) { } descarta no fim do seu bloco. A declaração using using var x = ...; (C# 8 em diante) não tem bloco próprio: ela descarta quando o escopo que a contém termina, normalmente o fim do método. Ela economiza aninhamento quando um recurso deve viver pelo resto do método.

Em que ordem vários objetos using são descartados?

Na ordem inversa da criação. Com using (var a = ...) using (var b = ...) { }, b é descartado primeiro e depois a. É a ordem que você quer quando b depende de a, como um StreamWriter que envolve um FileStream.

O garbage collector chama o Dispose?

Não. O garbage collector libera memória e pode executar um finalizador em algum momento imprevisível depois, mas nunca chama Dispose. Um arquivo não descartado pode ficar bloqueado, e os dados em buffer podem nunca ser gravados. É por isso que o using existe.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR