Padrão de descarte

Observação

Este conteúdo é reimpresso com permissão da Pearson Education, Inc. de Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition. Essa edição foi publicada em 2008 e, desde então, o livro foi totalmente revisado na terceira edição. Algumas das informações nesta página podem estar desatualizadas.

Todos os programas adquirem um ou mais recursos do sistema, como memória, identificadores do sistema ou conexões de banco de dados, durante a execução. Os desenvolvedores precisam ter cuidado ao usar esses recursos do sistema, que devem ser liberados depois de serem adquiridos e usados.

O CLR dá suporte ao gerenciamento automático de memória. A memória gerenciada (memória alocada usando o operador C# new) não precisa ser liberada explicitamente. Ela é liberada automaticamente pelo GC (coletor de lixo). Isso libera os desenvolvedores da tarefa tediosa e difícil de liberar memória e tem sido uma das principais razões da produtividade sem precedentes proporcionada pelo .NET Framework.

Infelizmente, a memória gerenciada é apenas um dos muitos tipos de recursos do sistema. Outros recursos ainda precisam ser liberados explicitamente e são chamados de recursos não gerenciados. O GC não foi projetado especificamente para gerenciar esses recursos não gerenciados, o que significa que a responsabilidade de gerenciá-los está nas mãos dos desenvolvedores.

O CLR fornece alguma ajuda na liberação de recursos não gerenciados. System.Object declara um método virtual Finalize (também chamado de finalizador), que é chamado pelo GC antes que a memória do objeto seja recuperada por ele e pode ser substituído para liberar recursos não gerenciados. Os tipos que substituem o finalizador são chamados de tipos finalizáveis.

Embora os finalizadores sejam eficazes em alguns cenários de limpeza, eles têm duas desvantagens significativas:

  • O finalizador é chamado quando o GC detecta que um objeto se qualifica para coleta. Isso acontece em um período indeterminado após o recurso deixar de ser necessário. O atraso entre quando o desenvolvedor poderia ou gostaria de liberar o recurso e o momento em que o recurso realmente é liberado pelo finalizador pode ser inaceitável em programas que adquirem muitos recursos escassos (recursos que podem ser esgotados facilmente) ou em casos em que é caro manter os recursos em uso (por exemplo, grandes buffers de memória não gerenciada).

  • Quando o CLR precisa chamar um finalizador, ele precisa adiar a coleta da memória do objeto até a próxima rodada de coleta de lixo (os finalizadores são executados entre coletas). Isso significa que a memória do objeto (e todos os objetos aos quais ele se refere) não será liberada por um período maior.

Portanto, depender exclusivamente de finalizadores pode não ser apropriado em muitos cenários em que é importante recuperar recursos não gerenciados o mais rápido possível, ao lidar com recursos escassos ou em cenários de alto desempenho em que a sobrecarga adicional de finalização do GC é inaceitável.

O Framework a interface System.IDisposable, que deve ser implementada para fornecer ao desenvolvedor uma maneira manual de liberar recursos não gerenciados assim que eles não forem necessários. Ele também fornece o método GC.SuppressFinalize, que pode informar ao GC que um objeto foi descartado manualmente e não precisa mais ser finalizado; nesse caso, a memória do objeto pode ser recuperada antes. Tipos que implementam a interface IDisposable são chamados de tipos descartáveis.

O Padrão de Descarte tem a finalidade de padronizar o uso e a implementação de finalizadores e da interface IDisposable.

O objetivo principal do padrão é reduzir a complexidade da implementação dos métodos Finalize e Dispose. A complexidade decorre do fato de que os métodos compartilham alguns, mas não todos os caminhos de código (as diferenças são descritas posteriormente no capítulo). Além disso, há motivos históricos para alguns elementos do padrão, relacionados à evolução do suporte da linguagem ao gerenciamento de recursos determinístico.

✓ IMPLEMENTE o Padrão de Descarte Básico em tipos que contêm instâncias de tipos descartáveis. Consulte a seção Padrão de Descarte Básico para obter detalhes sobre o padrão básico.

Se um tipo for responsável pelo tempo de vida de outros objetos descartáveis, os desenvolvedores também precisarão de uma maneira de descartá-los. Usar o método Dispose do contêiner é uma maneira conveniente de tornar isso possível.

✓ IMPLEMENTE o Padrão de Descarte Básico e forneça um finalizador em tipos que contêm recursos que precisam ser liberados explicitamente e que não têm finalizadores.

Por exemplo, o padrão deve ser implementado em tipos que armazenam buffers de memória não gerenciados. A seção Tipos Finalizáveis aborda diretrizes relacionadas à implementação de finalizadores.

✓ CONSIDERE implementar o Padrão de Descarte Básico em classes que, por si só, não contêm recursos não gerenciados nem objetos descartáveis, mas provavelmente têm subtipos que contêm.

Um ótimo exemplo disso é a classe System.IO.Stream. Embora essa seja uma classe base abstrata que não contém recursos, a maioria de suas subclasses contêm e, por isso, ela implementa esse padrão.

Padrão de Descarte Básico

A implementação básica do padrão envolve implementar a interface System.IDisposable e declarar o método Dispose(bool), que implementa toda a lógica de limpeza de recursos a ser compartilhada entre o método Dispose e o finalizador opcional.

O seguinte exemplo mostra uma implementação simples do padrão básico:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

O parâmetro booliano disposing indica se o método foi invocado da implementação IDisposable.Dispose ou do finalizador. A implementação Dispose(bool) deve verificar o parâmetro antes de acessar outros objetos de referência (por exemplo, o campo de recurso no exemplo anterior). Esses objetos só devem ser acessados quando o método é chamado da implementação IDisposable.Dispose (quando o parâmetro disposing é igual a true). Se o método for invocado do finalizador (se disposing for false), outros objetos não deverão ser acessados. Isso ocorre porque os objetos são finalizados em uma ordem imprevisível e, portanto, eles ou qualquer uma de suas dependências podem já ter sido finalizados.

Além disso, esta seção se aplica a classes com uma base que ainda não implementa o Padrão de Descarte. Se você estiver herdando de uma classe que já implementa o padrão, basta substituir o método Dispose(bool) para fornecer lógica de limpeza de recursos adicional.

✓ DECLARE um método protected virtual void Dispose(bool disposing) para centralizar toda a lógica relacionada à liberação de recursos não gerenciados.

Toda a limpeza de recursos deve ocorrer nesse método. O método é chamado do finalizador e do método IDisposable.Dispose. O parâmetro será false se for invocado de dentro de um finalizador. Ele deve ser usado para garantir que qualquer código em execução durante a finalização não esteja acessando outros objetos finalizáveis. Detalhes sobre a implementação de finalizadores são descritos na próxima seção.

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

✓ IMPLEMENTE a interface IDisposable chamando Dispose(true) seguido por GC.SuppressFinalize(this).

A chamada para SuppressFinalize só deverá ocorrer se Dispose(true) for executada com êxito.

public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
}

X NÃO torne o método sem parâmetros Dispose virtual.

O método Dispose(bool) é o que deve ser substituído por subclasses.

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X NÃO declare sobrecargas do método Dispose diferentes de Dispose() e Dispose(bool).

Dispose deve ser considerada uma palavra reservada para ajudar a codificar esse padrão e evitar confusão entre implementadores, usuários e compiladores. Algumas linguagens podem optar por implementar automaticamente esse padrão em determinados tipos.

✓ PERMITA que o método Dispose(bool) seja chamado mais de uma vez. O método pode optar por não fazer nada após a primeira chamada.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X EVITE gerar uma exceção de dentro de Dispose(bool), exceto em situações críticas em que o processo de contenção foi corrompido (vazamentos, estado compartilhado inconsistente etc.).

Os usuários esperam que uma chamada para Dispose não gere exceções.

Se Dispose puder gerar uma exceção, a lógica adicional de limpeza do bloco finally não será executada. Para contornar isso, o usuário precisaria encapsular cada chamada para Dispose (dentro do bloco finally!) em um bloco try, o que leva a manipuladores de limpeza muito complexos. Se estiver executando um método Dispose(bool disposing), nunca gere uma exceção se descartar for false. Isso encerrará o processo se ele estiver em execução em um contexto de finalizador.

✓ GERE um ObjectDisposedException de qualquer membro que não possa ser usado após o objeto ter sido descartado.

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

✓ CONSIDERE fornecer o método Close(), além do Dispose(), se fechar for a terminologia padrão na área.

Ao fazer isso, é importante tornar a implementação Close idêntica a Dispose e considerar implementar o método IDisposable.Dispose explicitamente.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Tipos finalizáveis

Tipos finalizáveis são tipos que estendem o Padrão de Descarte Básico substituindo o finalizador e fornecendo o caminho do código de finalização no método Dispose(bool).

Os finalizadores são notoriamente difíceis de implementar da maneira correta, principalmente porque você não pode fazer determinadas suposições (normalmente válidas) sobre o estado do sistema durante a execução deles. As diretrizes a seguir devem ser consideradas atentamente.

Observe que algumas das diretrizes se aplicam não apenas ao método Finalize, mas a qualquer código chamado de um finalizador. No caso do Padrão de Descarte Básico definido anteriormente, isso significa a lógica que é executada dentro de Dispose(bool disposing) quando o parâmetro disposing é false.

Se a classe base já for finalizável e implementar o Padrão de Descarte Básico, você não deverá substituir Finalize novamente. Em vez disso, substitua o método Dispose(bool) para fornecer lógica adicional de limpeza de recursos.

O seguinte código mostra um exemplo de tipo finalizável:

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

X EVITE tornar os tipos finalizáveis.

Considere atentamente qualquer caso em que você acreditar que um finalizador é necessário. Há um custo real associado a instâncias com finalizadores, tanto do ponto de vista do desempenho quanto da complexidade do código. Prefira usar wrappers de recursos, como SafeHandle, para encapsular recursos não gerenciados sempre que possível; nesse caso, um finalizador se torna desnecessário porque o wrapper é responsável pela própria limpeza de recursos.

X NÃO torne tipos de valor finalizáveis.

Somente tipos de referência realmente são finalizáveis pelo CLR e, portanto, qualquer tentativa de colocar um finalizador em um tipo de valor será ignorada. Os compiladores de C# e C++ impõem essa regra.

✓ TORNE um tipo finalizável se ele for responsável por liberar um recurso não gerenciado que não tenha o próprio finalizador.

Ao implementar o finalizador, basta chamar Dispose(false) e colocar toda a lógica de limpeza de recursos dentro do método Dispose(bool disposing).

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

✓ IMPLEMENTE o Padrão de Descarte Básico em cada tipo finalizável.

Isso proporciona aos usuários do tipo um meio de executar explicitamente a limpeza determinística desses mesmos recursos pelos quais o finalizador é responsável.

X NÃO acesse nenhum objeto finalizável no caminho do código do finalizador, pois há um risco significativo de que eles já tenham sido finalizados.

Por exemplo, um objeto finalizável A que tem uma referência a outro objeto finalizável B não pode usar B de modo confiável no finalizador A, ou vice-versa. Os finalizadores são chamados em ordem aleatória (além de uma garantia de ordenação fraca para finalização crítica).

Além disso, observe que objetos armazenados em variáveis estáticas serão coletados em determinados pontos durante o descarregamento de um domínio do aplicativo ou ao sair do processo. Acessar uma variável estática que se refere a um objeto finalizável (ou chamar um método estático que pode usar valores armazenados em variáveis estáticas) poderá não ser seguro se Environment.HasShutdownStarted retornar true.

✓ TORNE o método Finalize protegido.

Desenvolvedores de C#, C++ e VB.NET não precisam se preocupar com isso, pois os compiladores ajudam a impor essa diretriz.

X NÃO permita que exceções escapem da lógica do finalizador, exceto para falhas críticas do sistema.

Se uma exceção for gerada de um finalizador, o CLR desligará todo o processo (desde o .NET Framework versão 2.0), impedindo que outros finalizadores sejam executados e que os recursos sejam liberados de maneira controlada.

✓ CONSIDERE criar e usar um objeto finalizável crítico (um tipo com uma hierarquia de tipos que contém CriticalFinalizerObject) para situações em que um finalizador absolutamente precisa ser executado, mesmo em face de descarregamentos forçados do domínio do aplicativo e anulações de thread.

Portions © 2005, 2009 Microsoft Corporation. Todos os direitos reservados.

Reimpresso com permissão da Pearson Education, Inc. das Diretrizes de Design do Framework: convenções, linguagens e padrões para bibliotecas do .NET reutilizável, 2ª edição por Krzysztof Cwalina e Brad Abrams, publicado em 22 de outubro de 2008 por Addison-Wesley Professional como parte da série de desenvolvimento do Microsoft Windows.

Confira também