O coletor de lixo chamará IDisposable.Dispose para mim?
-
09-06-2019 - |
Pergunta
A rede Padrão IDisposable implica que se você escrever um finalizador e implementar IDisposable, seu finalizador precisará chamar explicitamente Dispose.Isso é lógico e é o que sempre fiz nas raras situações em que um finalizador é necessário.
No entanto, o que acontece se eu fizer isso:
class Foo : IDisposable
{
public void Dispose(){ CloseSomeHandle(); }
}
e não implemente um finalizador, nem nada.A estrutura chamará o método Dispose para mim?
Sim, eu sei que isso parece idiota, e toda a lógica implica que não, mas sempre tive duas coisas na minha cabeça que me deixaram inseguro.
Alguém, há alguns anos, me disse uma vez que isso de fato aconteceria, e essa pessoa tinha um histórico muito sólido de "conhecer o que estava fazendo".
O compilador/estrutura faz outras coisas 'mágicas' dependendo de quais interfaces você implementa (por exemplo:foreach, métodos de extensão, serialização baseada em atributos, etc.), então faz sentido que isso também possa ser 'mágico'.
Embora eu tenha lido muitas coisas sobre isso e haja muitas coisas implícitas, nunca consegui encontrar um definitivo Sim ou Não, responda a esta pergunta.
Solução
O .Net Garbage Collector chama o método Object.Finalize de um objeto na coleta de lixo.Por padrão isso faz nada e deve ser substituído se você quiser liberar recursos adicionais.
Dispose NÃO é chamado automaticamente e deve ser explicitamente chamado se os recursos devem ser liberados, como dentro de um bloco 'usando' ou 'tentar finalmente'
ver http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx Para maiores informações
Outras dicas
Quero enfatizar o ponto de vista de Brian em seu comentário, porque é importante.
Finalizadores não são destruidores determinísticos como em C++.Como outros apontaram, não há garantia de quando será chamado e, de fato, se você tiver memória suficiente, se será sempre ser chamado.
Mas o lado ruim dos finalizadores é que, como disse Brian, eles fazem com que seu objeto sobreviva a uma coleta de lixo.Isso pode ser ruim.Por que?
Como você pode ou não saber, o GC é dividido em gerações - Gen 0, 1 e 2, além do Large Object Heap.Divisão é um termo vago - você obtém um bloco de memória, mas há indicações de onde os objetos da Geração 0 começam e terminam.
O processo de pensamento é que você provavelmente usará muitos objetos que terão vida curta.Portanto, esses devem ser fáceis e rápidos para o GC chegar aos objetos da Geração 0.Portanto, quando há pressão de memória, a primeira coisa que faz é uma coleção da Geração 0.
Agora, se isso não resolver a pressão suficiente, ele volta e faz uma varredura Gen 1 (refazendo a Gen 0) e, se ainda não for suficiente, faz uma varredura Gen 2 (refazendo a Gen 1 e a Gen 0).Portanto, a limpeza de objetos de longa duração pode demorar um pouco e ser bastante cara (já que seus threads podem ficar suspensos durante a operação).
Isso significa que se você fizer algo assim:
~MyClass() { }
Seu objeto, não importa o que aconteça, viverá até a Geração 2.Isso ocorre porque o GC não tem como chamar o finalizador durante a coleta de lixo.Assim, os objetos que precisam ser finalizados são movidos para uma fila especial para serem limpos por um thread diferente (o thread finalizador - que, se você matar, fará com que todos os tipos de coisas ruins aconteçam).Isso significa que seus objetos permanecem por mais tempo e potencialmente forçam mais coletas de lixo.
Então, tudo isso é apenas para deixar claro que você deseja usar IDisposable para limpar recursos sempre que possível e tentar seriamente encontrar maneiras de contornar o uso do finalizador.É do interesse do seu aplicativo.
Já há muita discussão boa aqui e estou um pouco atrasado para a festa, mas gostaria de acrescentar alguns pontos.
- O coletor de lixo nunca executará diretamente um método Dispose para você.
- O CG vai execute finalizadores quando desejar.
- Um padrão comum usado para objetos que possuem um finalizador é fazer com que ele chame um método que é definido por convenção como Dispose(bool disposing) passando false para indicar que a chamada foi feita devido à finalização, em vez de uma chamada Dispose explícita.
- Isso ocorre porque não é seguro fazer suposições sobre outros objetos gerenciados ao finalizar um objeto (eles podem já ter sido finalizados).
class SomeObject : IDisposable {
IntPtr _SomeNativeHandle;
FileStream _SomeFileStream;
// Something useful here
~ SomeObject() {
Dispose(false);
}
public void Dispose() {
Dispose(true);
}
protected virtual void Dispose(bool disposing) {
if(disposing) {
GC.SuppressFinalize(this);
//Because the object was explicitly disposed, there will be no need to
//run the finalizer. Suppressing it reduces pressure on the GC
//The managed reference to an IDisposable is disposed only if the
_SomeFileStream.Dispose();
}
//Regardless, clean up the native handle ourselves. Because it is simple a member
// of the current instance, the GC can't have done anything to it,
// and this is the onlyplace to safely clean up
if(IntPtr.Zero != _SomeNativeHandle) {
NativeMethods.CloseHandle(_SomeNativeHandle);
_SomeNativeHandle = IntPtr.Zero;
}
}
}
Essa é a versão simples, mas há muitas nuances que podem atrapalhar esse padrão.
- O contrato para IDisposable.Dispose indica que deve ser seguro chamar várias vezes (chamar Dispose em um objeto que já foi descartado não deve fazer nada)
- Pode ser muito complicado gerenciar adequadamente uma hierarquia de herança de objetos descartáveis, especialmente se diferentes camadas introduzirem novos recursos descartáveis e não gerenciados.No padrão acima, Dispose(bool) é virtual para permitir que seja substituído para que possa ser gerenciado, mas acho que é propenso a erros.
Na minha opinião, é muito melhor evitar completamente qualquer tipo que contenha diretamente referências descartáveis e recursos nativos que possam exigir finalização.SafeHandles fornece uma maneira muito limpa de fazer isso, encapsulando recursos nativos em descartáveis que fornecem internamente sua própria finalização (juntamente com uma série de outros benefícios, como a remoção da janela durante P/Invoke, onde um identificador nativo pode ser perdido devido a uma exceção assíncrona) .
A simples definição de um SafeHandle torna isso trivial:
private class SomeSafeHandle
: SafeHandleZeroOrMinusOneIsInvalid {
public SomeSafeHandle()
: base(true)
{ }
protected override bool ReleaseHandle()
{ return NativeMethods.CloseHandle(handle); }
}
Permite simplificar o tipo que contém para:
class SomeObject : IDisposable {
SomeSafeHandle _SomeSafeHandle;
FileStream _SomeFileStream;
// Something useful here
public virtual void Dispose() {
_SomeSafeHandle.Dispose();
_SomeFileStream.Dispose();
}
}
Eu não acho.Você tem controle sobre quando Dispose é chamado, o que significa que você poderia, em teoria, escrever um código de descarte que faça suposições sobre (por exemplo) a existência de outros objetos.Você não tem controle sobre quando o finalizador é chamado, portanto, seria duvidoso fazer com que o finalizador chamasse Dispose automaticamente em seu nome.
EDITAR:Fui embora e testei, só para ter certeza:
class Program
{
static void Main(string[] args)
{
Fred f = new Fred();
f = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Fred's gone, and he's not coming back...");
Console.ReadLine();
}
}
class Fred : IDisposable
{
~Fred()
{
Console.WriteLine("Being finalized");
}
void IDisposable.Dispose()
{
Console.WriteLine("Being Disposed");
}
}
Não no caso que você descreve, mas o GC chamará o Finalizador para você, se você tiver um.
NO ENTANTO.Na próxima coleta de lixo, ao invés de ser coletado, o objeto irá para a finalização que, tudo é coletado, então é chamado o finalizador.A próxima coleção depois disso será liberada.
Dependendo da pressão de memória do seu aplicativo, você pode não ter um gc para a geração desse objeto por um tempo.Portanto, no caso de, digamos, um fluxo de arquivo ou uma conexão de banco de dados, pode ser necessário esperar um pouco para que o recurso não gerenciado seja liberado na chamada do finalizador, causando alguns problemas.
Não, não é chamado.
Mas isso facilita não esquecer de descartar seus objetos.Basta usar o using
palavra-chave.
Fiz o seguinte teste para isso:
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo();
foo = null;
Console.WriteLine("foo is null");
GC.Collect();
Console.WriteLine("GC Called");
Console.ReadLine();
}
}
class Foo : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed!");
}
O GC irá não ligue para descartar.Isto poderia ligue para o seu finalizador, mas mesmo isso não é garantido em todas as circunstâncias.
Veja isso artigo para uma discussão sobre a melhor maneira de lidar com isso.
A documentação sobre IDisponível fornece uma explicação bastante clara e detalhada do comportamento, bem como um exemplo de código.O GC NÃO chamará o Dispose()
método na interface, mas chamará o finalizador do seu objeto.
O padrão IDisposable foi criado principalmente para ser chamado pelo desenvolvedor, se você possui um objeto que implementa IDispose o desenvolvedor deve implementar o using
palavra-chave em torno do contexto do objeto ou chame o método Dispose diretamente.
A segurança contra falhas do padrão é implementar o finalizador chamando o método Dispose().Se você não fizer isso, poderá criar alguns vazamentos de memória, ou seja:Se você criar algum wrapper COM e nunca chamar o System.Runtime.Interop.Marshall.ReleaseComObject(comObject) (que seria colocado no método Dispose).
Não há mágica no clr para chamar métodos Dispose automaticamente, a não ser rastrear objetos que contêm finalizadores e armazená-los na tabela Finalizer pelo GC e chamá-los quando alguma heurística de limpeza entrar em ação pelo GC.