Partilhar via


Padrão de descarte

Nota

Este conteúdo é reimpresso com permissão da Pearson Education, Inc., a partir 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 revisto 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 têm que ter cuidado ao usar esses recursos do sistema, porque eles devem ser liberados depois de terem sido adquiridos e usados.

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

Infelizmente, a memória gerenciada é apenas um dos muitos tipos de recursos do sistema. Recursos diferentes da memória gerenciada ainda precisam ser liberados explicitamente e são chamados de recursos não gerenciados. O GC especificamente não foi projetado para gerenciar esses recursos não gerenciados, o que significa que a responsabilidade pelo gerenciamento de recursos não gerenciados 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 Finalize virtual (também chamado de finalizador) que é chamado pelo GC antes que a memória do objeto seja recuperada pelo GC e possa 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 deteta que um objeto é elegível para coleção. Isso acontece em algum período de tempo indeterminado depois que o recurso não é mais 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 facilmente esgotados) ou em casos em que os recursos são caros para manter em uso (por exemplo, grandes buffers de memória não gerenciados).

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

Portanto, confiar exclusivamente em finalizadores pode não ser apropriado em muitos cenários, quando é 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 de GC adicionada de finalização é inaceitável.

O Framework fornece a interface que deve ser implementada System.IDisposable 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 GC.SuppressFinalize método que pode dizer ao GC que um objeto foi descartado manualmente e não precisa mais ser finalizado, caso em que a memória do objeto pode ser recuperada anteriormente. Os tipos que implementam a IDisposable interface são chamados de tipos descartáveis.

O Dispose Pattern destina-se a padronizar o uso e a implementação de finalizadores e da IDisposable interface.

A principal motivação para o padrão é reduzir a complexidade da implementação do e dos FinalizeDispose métodos. 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 mais adiante no capítulo). Além disso, existem razões históricas para alguns elementos do padrão relacionados com a evolução do suporte linguístico para a gestão determinística de recursos.

✓ DO implementar o Padrão de Descarte Básico em tipos que contenham 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 é responsável pela vida útil de outros objetos descartáveis, os desenvolvedores também precisam de uma maneira de descartá-los. Usar o método do Dispose contêiner é uma maneira conveniente de tornar isso possível.

✓ IMPLEMENTAR o Padrão de Descarte Básico e fornecer um finalizador em tipos que possuem 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 discute diretrizes relacionadas à implementação de finalizadores.

✓ CONSIDERE a implementação do Padrão de Descarte Básico em classes que não possuem recursos não gerenciados ou objetos descartáveis, mas provavelmente terão subtipos que o façam.

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

Padrão de descarte básico

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

O exemplo a seguir 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 disposing Boolean indica se o método foi invocado a IDisposable.Dispose partir da implementação ou do finalizador. A Dispose(bool) implementação deve verificar o parâmetro antes de acessar outros objetos de referência (por exemplo, o campo de recurso no exemplo anterior). Tais objetos só devem ser acessados quando o método é chamado a IDisposable.Dispose partir da implementação (quando o disposing parâmetro é igual a true). Se o método for invocado a partir do finalizador (disposing is false), outros objetos não deverão ser acessados. A razão é que os objetos são finalizados em uma ordem imprevisível e, portanto, eles, ou qualquer uma de suas dependências, já podem ter sido finalizados.

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

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

Toda a limpeza de recursos deve ocorrer neste método. O método é chamado a partir do finalizador e do IDisposable.Dispose método. 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. Os detalhes da implementação dos finalizadores são descritos na próxima seção.

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

✓ DO implementar a IDisposable interface simplesmente chamando Dispose(true) seguido de GC.SuppressFinalize(this).

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

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

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

O Dispose(bool) método é aquele 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 declarar quaisquer sobrecargas do Dispose método que não Dispose() sejam 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. Alguns idiomas podem optar por implementar automaticamente esse padrão em determinados tipos.

✓ DO permite que o Dispose(bool) método 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 lançar uma exceção de dentro Dispose(bool) , exceto em situações críticas em que o processo de contenção tenha sido corrompido (vazamentos, estado compartilhado inconsistente, etc.).

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

Se Dispose puder gerar uma exceção, a lógica de limpeza de bloqueio adicional não será executada. Para contornar isso, o usuário precisaria envolver todas as chamadas para Dispose (dentro do bloco final!) em um bloco try, o que leva a manipuladores de limpeza muito complexos. Se estiver executando um Dispose(bool disposing) método, nunca lance uma exceção se o descarte for falso. Isso encerrará o processo se for executado dentro de um contexto de finalizador.

✓ JOGUE 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 método Close(), além do Dispose(), se fechar é terminologia padrão na área.

Ao fazer isso, é importante que você torne a Close implementação idêntica e Dispose considere implementar o IDisposable.Dispose método 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 Dispose(bool) código de finalização no método.

Os finalizadores são notoriamente difíceis de implementar corretamente, principalmente porque você não pode fazer certas suposições (normalmente válidas) sobre o estado do sistema durante sua execução. As orientações que se seguem devem ser cuidadosamente tidas em consideração.

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

Se a classe base já for finalizável e implementar o Basic Dispose Pattern, você não deverá substituir Finalize novamente. Em vez disso, você deve apenas substituir o Dispose(bool) método para fornecer lógica de limpeza de recursos adicionais.

O código a seguir mostra um exemplo de um 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 cuidadosamente qualquer caso em que você acha 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 encapsular recursos não gerenciados sempre que possível, caso em que um finalizador se torna desnecessário porque o wrapper é responsável por sua própria limpeza de recursos.

X NÃO torna os tipos de valor finalizáveis.

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

✓ FAÇA um tipo finalizável se o tipo for responsável por liberar um recurso não gerenciado que não tenha seu próprio finalizador.

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

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

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

✓ DO implementar o Padrão de Descarte Básico em todos os tipos finalizáveis.

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

X NÃO acesse nenhum objeto finalizável no caminho do código do finalizador, porque 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 forma confiável no finalizador de A, ou vice-versa. Os finalizadores são chamados em uma ordem aleatória (sem uma garantia de pedido fraca para finalização crítica).

Além disso, esteja ciente de que os objetos armazenados em variáveis estáticas serão coletados em determinados pontos durante o descarregamento de um domínio de 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) pode não ser seguro se Environment.HasShutdownStarted retornar true.

✓ FAÇA o seu Finalize método protegido.

Os desenvolvedores de C#, C++ e VB.NET não precisam se preocupar com isso, porque os compiladores ajudam a aplicar essa diretriz.

X NÃO DEIXE AS EXCEÇÕES ESCAPAREM DA LÓGICA DO FINALIZADOR, EXCETO para falhas críticas do sistema.

Se uma exceção for lançada de um finalizador, o CLR encerrará todo o processo (a partir do .NET Framework versão 2.0), impedindo que outros finalizadores sejam executados e os recursos sejam liberados de maneira controlada.

✓ CONSIDERE a criação e o uso de um objeto crítico finalizável (um tipo com uma hierarquia de tipo que contém CriticalFinalizerObject) para situações em que um finalizador absolutamente deve ser executado, mesmo diante de descarregamentos forçados de domínio de aplicativo e abortamentos de thread.

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

Reimpresso com permissão da Pearson Education, Inc., de Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition por Krzysztof Cwalina e Brad Abrams, publicado em 22 de outubro de 2008 por Addison-Wesley Professional como parte da Microsoft Windows Development Series.

Consulte também