C# using e IDisposable

Na aula finally, a Ana perdeu a lista de compras porque o arquivo não foi fechado, e consertou com um try/finally. Funciona, mas são oito linhas para dizer "feche quando terminar". O C# tem um atalho para isso: o comando using. Nesta aula você vai usá-lo nas duas formas (com bloco e a declaração using var), entender a interface IDisposable que está por trás e criar uma classe sua que pode ser usada com using.

Você vai aprender
  • O que é um recurso e por que ele precisa ser liberado.
  • O bloco using (var x = …) { … } e o que o compilador gera a partir dele.
  • A declaração using var x = …; e até onde ela vale.
  • A interface IDisposable e o método Dispose(), inclusive numa classe sua.
  • A ordem em que vários recursos são fechados.
  • A armadilha do using var nas instruções do topo do arquivo.
  • Os erros CS1674 e ObjectDisposedException.
Dois using diferentes

A aula Namespaces e using mostrou a diretiva using System.Text;, no topo do arquivo, que encurta nomes. Esta aula é sobre o comando using, que aparece no meio do código, sempre com uma variável: using var escritor = …;. A palavra é a mesma; o assunto não tem nada a ver.

Todos os programas rodaram na máquina da Ana, com o .NET 10 (C# 14), usando dotnet run arquivo.cs na pasta ~/cs.


A ideia antes do código

A Ana pegou um livro emprestado na biblioteca da escola. O combinado é simples: usou, devolveu. Se ela esquecer, ninguém mais consegue pegar aquele livro, e no fim do ano a biblioteca está vazia. Melhor ainda seria um empréstimo que se devolvesse sozinho quando ela terminasse de ler.

No computador, as coisas "emprestadas" se chamam recursos: um arquivo aberto, uma conexão com um banco de dados, uma conexão de rede. O sistema operacional empresta e espera de volta. Um programa que abre arquivos e não fecha acaba sem poder abrir mais nenhum, e os dados que estavam esperando no buffer (como na aula finally) se perdem.

O using é o empréstimo que se devolve sozinho: você diz o que vai usar, e o C# garante que, no fim, aquilo é devolvido, dando certo ou dando errado.


1. O bloco using

using (var escritor = new StreamWriter("compras.txt"))
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
}
Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));
$ dotnet run bloco.cs
No arquivo:
arroz
feijão
using (
O comando using ("usando"): "enquanto eu estiver usando isto…".
var escritor = new StreamWriter("compras.txt")
Abre o recurso e guarda numa variável, como sempre. A variável só existe dentro do bloco.
{ … }
O bloco em que o recurso é usado.
} (o fim do bloco)
Aqui o C# chama escritor.Dispose() sozinho. Para um StreamWriter, Dispose() grava o que estava no buffer e fecha o arquivo, o mesmo que o Close().

Depois do bloco, o arquivo já está fechado e gravado: o File.ReadAllText (aula Arquivos) lê as duas linhas.

O que o compilador faz com o using

O using não é mágica: o compilador o transforma no try/finally que você já conhece. As duas versões abaixo são o mesmo programa:

Com usingO que o compilador gera (simplificado)
using (var escritor = new StreamWriter("compras.txt"))
{
    escritor.WriteLine("arroz");
}
var escritor = new StreamWriter("compras.txt");
try
{
    escritor.WriteLine("arroz");
}
finally
{
    escritor?.Dispose();
}

Por isso o using herda todas as garantias do finally: o Dispose() roda no fim do bloco, com return, com break e com exceção.

Missão: o programa não fecha o arquivo, e depois de No arquivo: não aparece nada. Coloque as linhas que escrevem dentro de um bloco using (var escritor = …) { … }, sem chamar Close().

var escritor = new StreamWriter("compras.txt");
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");

Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));
using (var escritor = new StreamWriter("compras.txt"))
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
}

Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));

2. A interface IDisposable

Como o using sabe o que fazer no fim? Ele não sabe: ele só chama Dispose(). Quem sabe o que significa "devolver" é a própria classe. Toda classe que guarda um recurso assina um contrato, a interface IDisposable ("descartável"), que tem um método só:

public interface IDisposable
{
    void Dispose();
}

StreamWriter, StreamReader, FileStream, as conexões de banco de dados e muitas outras classes implementam IDisposable. Uma classe sua também pode. O caixa da lojinha abre no construtor e fecha no Dispose():

try
{
    using (var caixa = new Caixa())
    {
        caixa.Vender(10m);
        caixa.Vender(-5m);
        caixa.Vender(20m);
    }
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine($"Erro: {e.Message}");
}
Console.WriteLine("Fim");

class Caixa : IDisposable
{
    public Caixa()
    {
        Console.WriteLine("Abrindo o caixa");
    }

    public void Vender(decimal valor)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(valor);
        Console.WriteLine($"Venda: {valor:C}");
    }

    public void Dispose()
    {
        Console.WriteLine("Fechando o caixa");
    }
}
$ dotnet run caixa.cs
Abrindo o caixa
Venda: R$ 10,00
Fechando o caixa
Erro: valor ('-5') must be a non-negative value. (Parameter 'valor')
Actual value was -5.
Fim
class Caixa : IDisposable
A classe promete ter um método public void Dispose(). É essa promessa que permite usá-la num using.
public void Dispose()
O que "devolver" quer dizer para um Caixa. Aqui, só uma mensagem; num programa de verdade, fechar arquivos ou conexões que a classe abriu.
caixa.Vender(-5m);
Lança ArgumentOutOfRangeException (o atalho ThrowIfNegative, da aula throw). A venda de 20 nunca acontece.
Fechando o caixa
Aparece antes do Erro:. A exceção, ao sair do bloco using, passou pelo finally escondido, que chamou Dispose(). Só depois ela chegou ao catch, lá fora.
Quem implementa IDisposable?

Na prática, você vai usar muito mais do que implementar. Uma classe sua só precisa de IDisposable se ela guardar, num campo, algo que também é IDisposable (um StreamWriter, por exemplo); aí o Dispose() dela chama o Dispose() desse campo. Classes que só guardam números, textos e listas não precisam. E se você vir na internet um Dispose(bool disposing) com ~Classe(), é um padrão antigo para quem lida direto com recursos do sistema operacional, raro hoje.

Missão: o código não compila (CS1674), porque Caixa não é IDisposable. Faça a classe implementar IDisposable, com um método public void Dispose() que mostre Fechando o caixa.

using (var caixa = new Caixa())
{
    caixa.Vender(10m);
}
Console.WriteLine("Fim");

class Caixa
{
    public Caixa()
    {
        Console.WriteLine("Abrindo o caixa");
    }

    public void Vender(decimal valor)
    {
        Console.WriteLine($"Venda: {valor:C}");
    }
}
using (var caixa = new Caixa())
{
    caixa.Vender(10m);
}
Console.WriteLine("Fim");

class Caixa : IDisposable
{
    public Caixa()
    {
        Console.WriteLine("Abrindo o caixa");
    }

    public void Vender(decimal valor)
    {
        Console.WriteLine($"Venda: {valor:C}");
    }

    public void Dispose()
    {
        Console.WriteLine("Fechando o caixa");
    }
}

3. A declaração using var C# 8

O bloco using obriga a abrir chaves e recuar o código. Desde o C# 8, há uma forma sem bloco: using na frente da declaração da variável. O recurso é liberado quando a variável sai de escopo, isto é, no fim do bloco em que ela foi declarada (o método, o if, o laço…):

Gravar("compras.txt");
Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));

void Gravar(string caminho)
{
    using var escritor = new StreamWriter(caminho);
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
}
$ dotnet run metodo.cs
No arquivo:
arroz
feijão
using var escritor = new StreamWriter(caminho);
Abre o arquivo e marca a variável para Dispose() automático.
} (o fim do método Gravar)
A variável sai de escopo e o arquivo é fechado. Quando o File.ReadAllText roda, tudo já está gravado.

É a forma que o guia usa com arquivos (aula StreamReader e StreamWriter): curta, e impossível de esquecer, porque o fechamento fica na mesma linha da abertura.

A armadilha: using var nas instruções do topo

Nas instruções de nível superior, o "bloco" é o programa inteiro. Um using var escrito ali só é liberado quando o programa termina:

Este código tem um problema de propósito.
using var escritor = new StreamWriter("compras.txt");
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");
Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));
Console.WriteLine("(fim)");
$ dotnet run declaracao.cs
No arquivo:
(fim)
$ cat compras.txt
arroz
feijão

Durante o programa, o arquivo estava vazio: o escritor ainda estava aberto, com as linhas no buffer. Elas só foram gravadas no fim, depois do (fim), quando o Dispose() rodou; por isso o cat as mostra. Nada se perdeu, mas o próprio programa leu o arquivo cedo demais. A solução é dar ao escritor um escopo menor: um bloco using (…) { } ou um método, como o Gravar acima.


4. Vários recursos: fechados de trás para frente

using var gaveta = new Recurso("gaveta");
using var cofre = new Recurso("cofre");
Console.WriteLine("Trabalhando...");

class Recurso : IDisposable
{
    private readonly string _nome;
    public Recurso(string nome)
    {
        _nome = nome;
        Console.WriteLine($"Abrindo {_nome}");
    }
    public void Dispose() => Console.WriteLine($"Fechando {_nome}");
}
$ dotnet run dois.cs
Abrindo gaveta
Abrindo cofre
Trabalhando...
Fechando cofre
Fechando gaveta

Quem abriu por último fecha primeiro, como uma pilha de pratos. Faz sentido: o segundo recurso pode depender do primeiro (um leitor que lê de um arquivo aberto antes, por exemplo), então o primeiro tem de continuar aberto até o segundo terminar.


Formas diferentes, qual usar

SituaçãoForma
Recurso usado num método, até o fim deleusing var x = …;
Recurso que precisa ser fechado antes do fim do método (para ler o arquivo logo depois, por exemplo)using (var x = …) { … }
Instruções do topo do arquivoBloco using (…) { }, ou um método com using var
Fechar e também tratar errostry { using var x = …; … } catch (IOException e) { … }
Sua classe guarda um campo IDisposableImplemente IDisposable e chame o Dispose() do campo
Código antigo com try/finally + Close()Funciona; troque por using quando for mexer nele
Recursos assíncronos (IAsyncDisposable)await using var x = …; (aula async e await)

Quando dá errado

CS1674: "o tipo usado em uma instrução 'using' deve implementar 'System.IDisposable'"
using (var lista = new List<string>())
{
    lista.Add("arroz");
}
$ dotnet run naodisp.cs
/home/ana/cs/naodisp.cs(1,8): error CS1674: 'List<string>': o tipo usado em uma instrução 'using' deve implementar 'System.IDisposable'.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: o using precisa chamar Dispose() no fim, e uma List<string> não tem esse método: ela não guarda nenhum recurso do sistema, só memória, que o .NET recolhe sozinho (aula Memória e Coletor de Lixo).

Como consertar: tire o using; ele não é necessário aqui. Se a classe é sua e guarda recursos, implemente IDisposable.

ObjectDisposedException: usar depois de fechar
var escritor = new StreamWriter("compras.txt");
using (escritor)
{
    escritor.WriteLine("arroz");
}
escritor.WriteLine("feijão");
$ dotnet run fechado.cs
Unhandled exception. System.ObjectDisposedException: Cannot write to a closed TextWriter.
Object name: 'StreamWriter'.
   at System.IO.StreamWriter.<ThrowIfDisposed>g__ThrowObjectDisposedException|80_0()
   at System.IO.StreamWriter.WriteLine(String value)
   at Program.<Main>$(String[] args) in /home/ana/cs/fechado.cs:line 6

O que significa: "não é possível escrever num TextWriter fechado". O using (escritor) fechou o arquivo no fim do bloco, e a linha 6 tentou escrever depois. Um objeto descartado (disposed) não volta a funcionar. Este é o motivo de declarar a variável dentro do using (using (var escritor = …)): assim ela nem existe depois do bloco, e o compilador impede o erro.

Como consertar: faça todo o trabalho dentro do bloco, ou abra o recurso de novo.

O arquivo está vazio enquanto o programa roda

O que significa: provavelmente um using var nas instruções do topo, ou num método que ainda não terminou. O Dispose() ainda não rodou.

Como consertar: use o bloco using (…) { } ou mova a escrita para um método.


Referência

SintaxeO que faz
using (var x = new T()) { … }Chama x.Dispose() no fim do bloco, com ou sem exceção
using var x = new T();Chama x.Dispose() no fim do bloco em que x foi declarada C# 8
class T : IDisposable { public void Dispose() { … } }Permite usar T com using
Vários usingFechados na ordem inversa da abertura
await using var x = …;Para recursos IAsyncDisposable C# 8
CS1674O tipo do using não é IDisposable
ObjectDisposedExceptionUsou o objeto depois do Dispose()

Exercícios

1. A interface

Complete para a classe poder ser usada com using:

class Conexao :

2. O método do contrato

Complete o método que o using chama no fim:

public void () { … }

3. A forma curta

Complete a declaração do C# 8:

var leitor = new StreamReader("notas.txt");

4. Num bloco using (var caixa = new Caixa()) { … }, uma linha do meio lança uma exceção. O que acontece?

5. Com using var a = …; e depois using var b = …; no mesmo método, em que ordem eles são fechados?

6. Qual destes não pode ser usado num using?

Missão final: o using var está nas instruções do topo, então o arquivo só é gravado no fim do programa, e o No arquivo: sai vazio. Troque por um bloco using (var escritor = …) { … } em volta das duas linhas que escrevem, para que arroz e feijão apareçam logo depois de No arquivo:.

using var escritor = new StreamWriter("compras.txt");
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");
Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));
Console.WriteLine("(fim)");
using (var escritor = new StreamWriter("compras.txt"))
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
}
Console.WriteLine("No arquivo:");
Console.Write(File.ReadAllText("compras.txt"));
Console.WriteLine("(fim)");

Resumo

  • Recursos (arquivos, conexões) são emprestados pelo sistema e precisam ser devolvidos. Quem devolve é o método Dispose() da interface IDisposable.
  • using (var x = …) { … } chama Dispose() no fim do bloco, com ou sem exceção. Por baixo, é um try/finally.
  • using var x = …; C# 8 chama Dispose() no fim do bloco onde a variável foi declarada. Nas instruções do topo, isso é o fim do programa.
  • Vários recursos são fechados na ordem inversa da abertura.
  • Uma classe sua pode implementar IDisposable; faça isso quando ela guardar outro recurso IDisposable.
  • CS1674: o tipo não é IDisposable. ObjectDisposedException: usou depois de fechar.