Qual é a diferença entre usar IDisposable e um destruidor em C #?

101

Quando eu implementaria IDispose em uma classe em oposição a um destruidor? Eu li este artigo , mas ainda estou perdendo o ponto.

Minha suposição é que se eu implementar IDispose em um objeto, posso explicitamente 'destruí-lo' em vez de esperar que o coletor de lixo faça isso. Isso está correto?

Isso significa que sempre devo chamar explicitamente Dispose em um objeto? Quais são alguns exemplos comuns disso?

Jordan Parmer
fonte
5
Na verdade, você deve chamar Dispose em cada objeto Disposable. Você pode fazer isso facilmente usando a usingconstrução.
Luc Touraille
Ah, isso faz sentido. Sempre me perguntei por que a instrução 'using' era usada para fluxos de arquivos. Eu sei que tem algo a ver com o escopo do objeto, mas não o coloquei em contexto com a interface IDisposable.
Jordan Parmer
5
Um ponto importante a lembrar é que um finalizador nunca deve acessar nenhum membro gerenciado de uma classe, pois esses membros podem não ser mais referências válidas.
Dan Bryant

Respostas:

126

Um finalizador (também conhecido como destruidor) faz parte da coleta de lixo (GC) - é indeterminado quando (ou mesmo se) isso acontece, pois o GC ocorre principalmente como resultado da pressão da memória (ou seja, precisa de mais espaço). Os finalizadores geralmente são usados ​​apenas para limpar recursos não gerenciados , uma vez que os recursos gerenciados terão sua própria coleção / descarte.

Portanto, IDisposableé usado para limpar objetos de forma determinística , ou seja, agora. Não coleta a memória do objeto (que ainda pertence ao GC) - mas é usado, por exemplo, para fechar arquivos, conexões de banco de dados, etc.

Existem muitos tópicos anteriores sobre isso:

Por fim, observe que não é incomum que um IDisposableobjeto também tenha um finalizador; neste caso, Dispose()geralmente chama GC.SuppressFinalize(this), o que significa que o GC não executa o finalizador - ele simplesmente joga a memória fora (muito mais barato). O finalizador ainda será executado se você esquecer Dispose()o objeto.

Marc Gravell
fonte
Obrigado! Isso faz todo o sentido. Agradeço muito a ótima resposta.
Jordan Parmer
27
Uma coisa extra a dizer. Não adicione um finalizador à sua aula, a menos que você realmente precise de um. Se você adicionar um finalizador (destruidor), o GC precisa chamá-lo (mesmo um finalizador vazio) e, para chamá-lo, o objeto sempre sobreviverá a uma coleta de lixo de geração 1. Isso irá impedir e desacelerar o GC. É isso que Marc diz para chamar SuppressFinalize no código acima
Kevin Jones,
1
Portanto, Finalizar é liberar recursos não gerenciados. Mas Dispose poderia ser usado para liberar recursos gerenciados e não gerenciados?
Dark_Knight
2
@Dark yes; porque 6 níveis abaixo da cadeia de gerenciamento pode ser um não gerenciado que precisa de limpeza imediata
Marc Gravell
1
@KevinJones Objetos com finalizador têm a garantia de sobreviver à geração 0, não 1, certo? Eu li isso em um livro chamado .NET Performance.
David Klempfner
25

A função do Finalize()método é garantir que um objeto .NET possa limpar recursos não gerenciados quando o lixo for coletado . No entanto, objetos como conexões de banco de dados ou manipuladores de arquivos devem ser liberados o mais rápido possível, em vez de depender da coleta de lixo. Para isso você deve implementar IDisposableinterface e liberar seus recursos no Dispose()método.

Igal Tabachnik
fonte
9

Há uma descrição muito boa no MSDN :

O principal uso dessa interface é liberar recursos não gerenciados . O coletor de lixo libera automaticamente a memória alocada para um objeto gerenciado quando esse objeto não é mais usado. No entanto, não é possível prever quando ocorrerá a coleta de lixo . Além disso, o coletor de lixo não tem conhecimento de recursos não gerenciados , como identificadores de janela ou arquivos e fluxos abertos .

Use o método Dispose desta interface para liberar explicitamente recursos não gerenciados em conjunto com o coletor de lixo. O consumidor de um objeto pode chamar esse método quando o objeto não for mais necessário.

Abatishchev
fonte
1
Um dos principais pontos fracos dessa descrição é que o MS dá exemplos de recursos não gerenciados, mas pelo que vi nunca realmente define o termo. Como os objetos gerenciados geralmente só podem ser usados ​​dentro do código gerenciado, pode-se pensar que as coisas usadas no código não gerenciado são recursos não gerenciados, mas isso não é verdade. Muitos códigos não gerenciados não usam nenhum recurso, e alguns tipos de recursos não gerenciados, como eventos, existem apenas no universo do código gerenciado.
supercat de
1
Se um objeto de curta duração se inscreve em um evento de um objeto de longa duração (por exemplo, ele pede para ser notificado de quaisquer mudanças que ocorram durante a vida útil do objeto de curta duração), tal evento deve ser considerado um recurso não gerenciado, uma vez que falha em cancelar a inscrição do evento faria com que o tempo de vida do objeto de vida curta fosse estendido ao do objeto de vida longa. Se muitos milhares ou milhões de objetos de curta duração se inscrevessem em um evento, mas fossem abandonados sem cancelar, isso poderia causar um vazamento de memória ou CPU (já que o tempo necessário para processar cada inscrição aumentaria).
supercat de
1
Outro cenário envolvendo recursos não gerenciados dentro do código gerenciado seria a alocação de objetos de pools. Especialmente se o código precisar ser executado no .NET Micro Framework (cujo coletor de lixo é muito menos eficiente do que o das máquinas desktop), pode ser útil que o código tenha, por exemplo, uma matriz de estruturas, cada uma das quais pode ser marcada como "usada" ou "grátis". Uma solicitação de alocação deve encontrar uma estrutura atualmente marcada como "livre", marcá-la como "usada" e retornar um índice para ela; uma solicitação de liberação deve marcar uma estrutura como "livre". Se uma solicitação de alocação retornar, por exemplo, 23, então ...
supercat
1
... se o código nunca notificar o proprietário do array de que ele não precisa mais do item 23, esse slot de array nunca será usado por nenhum outro código. Essa alocação manual de slots de array não é usada com muita frequência no código de desktop, pois o GC é bastante eficiente, mas no código executado no Micro Framework pode fazer uma grande diferença.
supercat de
8

A única coisa que deve estar em um destruidor C # é esta linha:

Dispose(False);

É isso aí. Nada mais deveria estar nesse método.

Jonathan Allen
fonte
3
Este é o padrão de design proposto pela Microsoft na documentação .NET, mas não o use quando seu objeto não for identificável. msdn.microsoft.com/en-us/library/fs2xkftw%28v=vs.110%29.aspx
Zbyl
1
Não consigo pensar em nenhum motivo para oferecer uma classe com um finalizador que também não tenha um método Dispose.
Jonathan Allen
4

Sua pergunta sobre se você deve sempre ligar ou não Disposeé geralmente um debate acalorado. Veja este blog para uma perspectiva interessante de indivíduos respeitados na comunidade .NET.

Pessoalmente, acho que a posição de Jeffrey Richter de que pagar Disposenão é obrigatório é incrivelmente fraca. Ele dá dois exemplos para justificar sua opinião.

No primeiro exemplo, ele diz que chamar Disposeos controles do Windows Forms é entediante e desnecessário em cenários convencionais. No entanto, ele não menciona que, Disposena verdade, é chamado automaticamente por contêineres de controle nesses cenários principais.

No segundo exemplo, ele afirma que um desenvolvedor pode assumir incorretamente que a instância de IAsyncResult.WaitHandledeve ser descartada de forma agressiva sem perceber que a propriedade inicializa preguiçosamente o identificador de espera, resultando em uma penalidade de desempenho desnecessária. Mas, o problema com este exemplo é que o IAsyncResultpróprio não adere às próprias diretrizes publicadas da Microsoft para lidar com IDisposableobjetos. Ou seja, se uma classe contém uma referência a um IDisposabletipo, a própria classe deve implementar IDisposable. Se IAsyncResultessa regra Disposefosse seguida, seu próprio método poderia tomar a decisão sobre qual de seus membros constituintes precisa ser eliminado.

Portanto, a menos que alguém tenha um argumento mais convincente, continuarei no campo "sempre ligue para o descarte", com o entendimento de que haverá alguns casos marginais que surgem principalmente de más escolhas de design.

Brian Gideon
fonte
3

É realmente muito simples. Sei que foi respondido, mas tentarei novamente, mas tentarei mantê-lo o mais simples possível.

Geralmente, um destruidor nunca deve ser usado. É executado apenas .net quer que ele seja executado. Ele só será executado após um ciclo de coleta de lixo. Ele pode nunca ser executado durante o ciclo de vida de seu aplicativo. Por esse motivo, você nunca deve colocar nenhum código em um destruidor que 'deve' ser executado. Você também não pode confiar na existência de quaisquer objetos existentes na classe quando ela for executada (eles podem já ter sido limpos, pois a ordem em que os destruidores são executados não é garantida).

IDisposible deve ser usado sempre que você tiver um objeto que cria recursos que precisam de limpeza (ou seja, identificadores de arquivo e gráficos). Na verdade, muitos argumentam que qualquer coisa que você colocar em um destruidor deve ser colocada como descartável devido aos motivos listados acima.

A maioria das classes chamará dispose quando o finalizador for executado, mas isso é simplesmente uma proteção e nunca deve ser invocado. Você deve descartar explicitamente qualquer coisa que implemente IDisposable quando terminar de usá-lo. Se você implementar IDisposable, deverá chamar dispose no finalizer. Consulte http://msdn.microsoft.com/en-us/library/system.idisposable.aspx para obter um exemplo.

DaEagle
fonte
Não, o coletor de lixo nunca chama Dispose (). Ele apenas chama o finalizador.
Marc Gravell
Corrigido isso. As classes devem chamar dispose em seu finalizador, mas não é necessário.
DaEagle