O Garbage Collector chamará IDisposable.Dispose para mim?

134

O .NET IDisposable Pattern implica que, se você escrever um finalizador e implementar IDisposable, seu finalizador precisará chamar explicitamente Dispose. Isso é lógico e é o que eu sempre fiz nas raras situações em que um finalizador é garantido.

No entanto, o que acontece se eu apenas fizer isso:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

e não implemente um finalizador, ou qualquer coisa. A estrutura chamará o método Dispose para mim?

Sim, percebo que isso parece idiota, e toda a lógica implica que não, mas sempre tive duas coisas na parte de trás da minha cabeça que me deixaram insegura.

  1. Alguém, alguns anos atrás, me disse uma vez que, de fato, faria isso, e essa pessoa tinha um histórico muito sólido de "conhecer suas coisas".

  2. 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.), por isso faz sentido que também possa ser 'mágico'.

Embora tenha lido muitas coisas sobre o assunto e muitas coisas estejam implícitas, nunca consegui encontrar uma resposta definitiva Sim ou Não a esta pergunta.

Orion Edwards
fonte

Respostas:

121

O .Net Garbage Collector chama o método Object.Finalize de um objeto na coleta de lixo. Por padrão, isso não faz nada e deve ser substituído se você deseja liberar recursos adicionais.

Dispose NÃO é chamado automaticamente e deve ser explicitamente chamado para que os recursos sejam liberados, como dentro de um bloco 'using' ou 'try finally'

consulte http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx para obter mais informações

Xian
fonte
35
Na verdade, não acredito que o GC chame Object.Finalize, se não for substituído. O objeto está determinado a não ter efetivamente um finalizador e a finalização é suprimida - o que o torna mais eficiente, pois o objeto não precisa estar nas filas de finalização / alcançável.
Jon Skeet
7
Conforme o MSDN: msdn.microsoft.com/en-us/library/…, na verdade, você não pode "substituir" o método Object.Finalize no C #, o compilador gera um erro: Não substitua o objeto.Finalize. Em vez disso, forneça um destruidor. ; ou seja, você deve implementar um destruidor que atue efetivamente como o Finalizador. [acabou de ser adicionado aqui por
questões
1
O GC não faz nada com um objeto que não substitui um Finalizador. Ele não é colocado na fila de finalização - e nenhum Finalizador é chamado.
Dave Black
1
@dotnetguy - embora a especificação original do C # mencione um "destruidor", na verdade é chamado de Finalizador - e sua mecânica é totalmente diferente da maneira como um verdadeiro "destruidor" funciona para idiomas não gerenciados.
Dave Black
67

Quero enfatizar o argumento de Brian em seu comentário, porque é importante.

Os finalizadores não são destruidores determinísticos, como no C ++. Como outros já apontaram, não há garantia de quando será chamado e, de fato, se você tiver memória suficiente, se algum dia será chamado.

Mas o ruim dos finalizadores é que, como Brian disse, isso faz com que seu objeto sobreviva a uma coleta de lixo. Isso pode ser ruim. Por quê?

Como você pode ou não saber, o GC é dividido em gerações - Gen 0, 1 e 2, mais o Large Object Heap. Dividir é um termo flexível - você obtém um bloco de memória, mas há indicadores de onde os objetos Gen 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 - Gen 0. Portanto, quando há pressão de memória, a primeira coisa que faz é uma coleção Gen 0.

Agora, se isso não resolver a pressão suficiente, ele voltará e fará uma varredura da Geração 1 (refazendo a Geração 0) e, se ainda não for suficiente, fará uma varredura da Geração 2 (refazendo a Geração 1 e Geração 0). Portanto, a limpeza de objetos de longa duração pode demorar um pouco e ser bastante cara (já que seus threads podem ser suspensos durante a operação).

Isso significa que se você fizer algo assim:

~MyClass() { }

Seu objeto, não importa o quê, viverá para a Geração 2. Isso ocorre porque o GC não tem como chamar o finalizador durante a coleta de lixo. Portanto, os objetos que precisam ser finalizados são movidos para uma fila especial a ser limpa por um thread diferente (o thread do finalizador - que, se você mata, faz com que todos os tipos de coisas ruins aconteçam). Isso significa que seus objetos ficam mais tempo e potencialmente forçam mais coletas de lixo.

Portanto, tudo isso é apenas para esclarecer o ponto em que você deseja usar o IDisposable para limpar os recursos sempre que possível e tentar seriamente encontrar maneiras de usar o finalizador. É no melhor interesse do seu aplicativo.

Cory Foy
fonte
8
Concordo que você deseja usar o IDisposable sempre que possível, mas também deve ter um finalizador que chame um método de descarte. Você pode chamar GC.SuppressFinalize () em IDispose.Dispose após chamar seu método de descarte para garantir que seu objeto não seja colocado na fila do finalizador.
JColeson 19/05
2
As gerações são numeradas de 0 a 2, não de 1 a 3, mas sua postagem é boa. Eu acrescentaria, entretanto, que quaisquer objetos referenciados por seu objeto, ou quaisquer objetos referenciados por eles, etc. também serão protegidos contra a coleta de lixo (embora não contra a finalização) para outra geração. Portanto, objetos com finalizadores não devem conter referências a nada que não seja necessário para a finalização.
precisa
3
Em relação ao "Seu objeto, não importa o quê, viverá para a Geração 2." Esta é uma informação MUITO fundamental! Isso economizou muito tempo na depuração de um sistema, onde havia muitos objetos Gen2 de curta duração "preparados" para finalização, mas nunca finalizados causaram OutOfMemoryException devido ao uso intenso de heap. Removendo o finalizador (mesmo vazio) e movendo (contornando) o código para outro lugar, o problema desapareceu e o GC conseguiu lidar com a carga.
apontador de
@CoryFoy "Seu objeto, não importa o quê, viverá para a Geração 2" Existe alguma documentação para isso?
Ashish Negi 26/03
33

Já há muita discussão boa aqui, e estou um pouco atrasado para a festa, mas queria acrescentar alguns pontos.

  • O coletor de lixo nunca executará diretamente um método Dispose para você.
  • O GC irá executar finalizadores quando se sente como ele.
  • Um padrão comum usado para objetos que têm um finalizador é chamar um método que é definido por convenção como Dispose (disposição booleana) 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 já podem 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 te enganar nesse 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 camadas diferentes introduzirem novos recursos descartáveis ​​e não gerenciados. No padrão acima, Dispose (bool) é virtual para permitir que ele 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. O SafeHandles fornece uma maneira muito clara de fazer isso, encapsulando recursos nativos em descartáveis ​​que fornecem internamente sua própria finalização (junto com vários outros benefícios, como remover a janela durante P / Invoke, em que um identificador nativo pode ser perdido devido a uma exceção assíncrona) .

Simplesmente definir 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();
 }
}
Andrew
fonte
1
De onde vem a classe SafeHandleZeroOrMinusOneIsInvalid? É um tipo .net incorporado?
Orion Edwards
+1 para // 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.// As únicas classes não lacradas que deveriam ter finalizadores são aquelas cujo objetivo se concentra em finalização.
supercat
1
@OrionEdwards yes see msdn.microsoft.com/en-us/library/…
Martin Capodici
1
Em relação à chamada para GC.SuppressFinalizeneste exemplo. Nesse contexto, SuppressFinalize só deve ser chamado se for Dispose(true)executado com êxito. Se houver Dispose(true)falha em algum momento após a finalização ser suprimida, mas antes de todos os recursos (principalmente os não gerenciados) serem limpos, você ainda desejará que a finalização ocorra para fazer o máximo de limpeza possível. Melhor mover a GC.SuppressFinalizechamada para o Dispose()método após a chamada para Dispose(true). Consulte as Diretrizes de design da estrutura e este post .
BitMask777
6

Acho que não. Você tem controle sobre quando Dispose é chamado, o que significa que, em teoria, você poderia escrever um código de descarte que faz suposições sobre (por exemplo) a existência de outros objetos. Você não tem controle sobre quando o finalizador é chamado, portanto, seria duvidoso que o finalizador chamasse Dispose automaticamente em seu nome.


EDIT: Eu fui e testei, apenas 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");
    }
}
Matt Bishop
fonte
Fazer suposições sobre os objetos disponíveis para você durante o descarte pode ser perigoso e complicado, especialmente durante a finalização.
21926 Scott Dorman
3

Não no caso descrito, mas o GC chamará o Finalizador para você, se você tiver um.

CONTUDO. A próxima coleta de lixo, em vez de ser coletada, o objeto entra na fila de finalização, tudo é coletado e, em seguida, é finalizado. 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, digamos, um fluxo de arquivos ou uma conexão db, talvez seja necessário aguardar um pouco para que o recurso não gerenciado seja liberado na chamada do finalizador por um tempo, causando alguns problemas.

Brian Leahy
fonte
1

Não, não é chamado.

Mas isso facilita a não esquecer de descartar seus objetos. Basta usar a usingpalavra - chave.

Eu 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!");
    }
penyaskito
fonte
1
Este foi um exemplo de como se você NÃO usar a palavra-chave <code> using </code>, ela não será chamada ... e esse trecho tem 9 anos, feliz aniversário!
penyaskito 12/09
1

O GC não chamará de descarte. Ele pode chamar seu finalizador, mas mesmo isso não é garantido em todas as circunstâncias.

Consulte este artigo para uma discussão sobre a melhor maneira de lidar com isso.

Rob Walker
fonte
0

A documentação em IDisposable fornece uma explicação bastante clara e detalhada do comportamento, bem como um código de exemplo. O GC NÃO chamará o Dispose()método na interface, mas chamará o finalizador para o seu objeto.

Joseph Daigle
fonte
0

O padrão IDisposable foi criado principalmente para ser chamado pelo desenvolvedor, se você tiver um objeto que implementa o IDispose, o desenvolvedor deve implementar a usingpalavra-chave em torno do contexto do objeto ou chamar o método Dispose diretamente.

O fail safe para o padrão é implementar o finalizador chamando o método Dispose (). Se você não fizer isso, poderá criar vazamentos de memória, por exemplo: 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, além de rastrear objetos que contêm finalizadores e armazená-los na tabela Finalizador pelo GC e chamá-los quando algumas heurísticas de limpeza são iniciadas pelo GC.

Erick Sgarbi
fonte