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.
- 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
IDisposablee o métodoDispose(), inclusive numa classe sua. - A ordem em que vários recursos são fechados.
- A armadilha do
using varnas instruções do topo do arquivo. - Os erros
CS1674eObjectDisposedException.
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 umStreamWriter,Dispose()grava o que estava no buffer e fecha o arquivo, o mesmo que oClose().
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 using | O 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 numusing. - 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 atalhoThrowIfNegative, da aula throw). A venda de 20 nunca acontece. - Fechando o caixa
- Aparece antes do
Erro:. A exceção, ao sair do blocousing, passou pelofinallyescondido, que chamouDispose(). Só depois ela chegou aocatch, lá fora.
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.ReadAllTextroda, 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:
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ção | Forma |
|---|---|
| Recurso usado num método, até o fim dele | using 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 arquivo | Bloco using (…) { }, ou um método com using var |
| Fechar e também tratar erros | try { using var x = …; … } catch (IOException e) { … } |
Sua classe guarda um campo IDisposable | Implemente 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
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.
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 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
| Sintaxe | O 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 using | Fechados na ordem inversa da abertura |
await using var x = …; | Para recursos IAsyncDisposable C# 8 |
CS1674 | O tipo do using não é IDisposable |
ObjectDisposedException | Usou o objeto depois do Dispose() |
Exercícios
1. A interface
Complete para a classe poder ser usada com using:
2. O método do contrato
Complete o método que o using chama no fim:
3. A forma curta
Complete a declaração do C# 8:
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 interfaceIDisposable. using (var x = …) { … }chamaDispose()no fim do bloco, com ou sem exceção. Por baixo, é umtry/finally.using var x = …;C# 8 chamaDispose()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 recursoIDisposable. CS1674: o tipo não éIDisposable.ObjectDisposedException: usou depois de fechar.