Definindo um objeto como null vs Dispose ()


108

Estou fascinado pela maneira como o CLR e o GC funcionam (estou trabalhando para expandir meu conhecimento sobre isso lendo CLR via C #, os livros / postagens de Jon Skeet e muito mais).

Enfim, qual é a diferença entre dizer:

MyClass myclass = new MyClass();
myclass = null;

Ou fazendo com que MyClass implemente IDisposable e um destruidor e chamando Dispose ()?

Além disso, se eu tiver um bloco de código com uma instrução using (por exemplo, abaixo), se eu percorrer o código e sair do bloco using, o objeto será descartado ou quando ocorrer uma coleta de lixo? O que aconteceria se eu chamar Dispose () no bloco using anyay?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

Classes de fluxo (por exemplo, BinaryWriter) têm um método Finalize? Por que eu iria querer usar isso?

Respostas:


210

É importante separar o descarte da coleta de lixo. São coisas completamente distintas, com um ponto em comum que abordarei em um minuto.

Dispose, coleta de lixo e finalização

Quando você escreve uma usinginstrução, é simplesmente um açúcar sintático para um bloco try / finally, de modo que Disposeé chamado mesmo se o código no corpo da usinginstrução lançar uma exceção. Isso não significa que o objeto seja coletado como lixo no final do bloco.

O descarte é sobre recursos não gerenciados ( recursos que não são de memória). Podem ser identificadores de IU, conexões de rede, identificadores de arquivo, etc. Esses são recursos limitados, então geralmente você deseja liberá-los assim que puder. Você deve implementar IDisposablesempre que seu tipo "possuir" um recurso não gerenciado, seja diretamente (geralmente por meio de um IntPtr) ou indiretamente (por exemplo Stream, por meio de um , a SqlConnectionetc.).

A coleta de lixo em si é apenas sobre memória - com um pequeno toque. O coletor de lixo é capaz de localizar objetos que não podem mais ser referenciados e liberá-los. Porém, ele não procura por lixo o tempo todo - apenas quando detecta que precisa (por exemplo, se uma "geração" do heap ficar sem memória).

A reviravolta é a finalização . O coletor de lixo mantém uma lista de objetos que não são mais alcançáveis, mas que têm um finalizador (escrito como ~Foo()em C #, um tanto confuso - eles não são nada como destruidores C ++). Ele executa os finalizadores nesses objetos, apenas no caso de eles precisarem fazer uma limpeza extra antes que sua memória seja liberada.

Os finalizadores quase sempre são usados ​​para limpar recursos no caso em que o usuário do tipo se esqueceu de descartá-los de maneira ordenada. Portanto, se você abrir um FileStreammas esquecer de chamar Disposeou Close, o finalizador eventualmente liberará o identificador de arquivo subjacente para você. Em um programa bem escrito, os finalizadores quase nunca deveriam disparar na minha opinião.

Definir uma variável para null

Um pequeno ponto sobre como definir uma variável para null- isso quase nunca é necessário para a coleta de lixo. Às vezes, você pode querer fazer isso se for uma variável de membro, embora em minha experiência seja raro que "parte" de um objeto não seja mais necessária. Quando é uma variável local, o JIT geralmente é inteligente o suficiente (no modo de liberação) para saber quando você não usará uma referência novamente. Por exemplo:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

A única vez em que pode valer a pena definir uma variável local nullé quando você está em um loop, e alguns ramos do loop precisam usar a variável, mas você sabe que atingiu um ponto em que não o faz. Por exemplo:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

Implementando IDisponíveis / finalizadores

Portanto, seus próprios tipos devem implementar finalizadores? Quase certamente não. Se você apenas indiretamente mantém recursos não gerenciados (por exemplo, você tem um FileStreamcomo uma variável de membro), então adicionar seu próprio finalizador não ajudará: o fluxo quase certamente será elegível para coleta de lixo quando seu objeto for, então você pode apenas confiar em FileStreamter um finalizador (se necessário - pode se referir a outra coisa, etc). Se você deseja manter um recurso não gerenciado "quase" diretamente, SafeHandleé seu amigo - leva um pouco de tempo para começar, mas significa que você quase nunca precisará escrever um finalizador novamente . Você normalmente só precisa de um finalizador se tiver um controle realmente direto sobre um recurso (an IntPtr) e deve procurar mover paraSafeHandleassim que puder. (Existem dois links lá - leia ambos, de preferência.)

Joe Duffy tem um longo conjunto de diretrizes sobre finalizadores e IDisposable (co-escrito com muitas pessoas inteligentes) que vale a pena ler. Vale a pena estar ciente de que se você selar suas classes, torna a vida muito mais fácil: o padrão de sobrescrever Disposepara chamar um novo Dispose(bool)método virtual etc. só é relevante quando sua classe é projetada para herança.

Isso tem sido um pouco confuso, mas por favor, peça esclarecimentos onde você gostaria de alguns :)


Re "O único momento em que pode valer a pena definir uma variável local como nula" - talvez também alguns dos cenários de "captura" mais espinhosos (várias capturas da mesma variável) - mas pode não valer a pena complicar a postagem! +1 ...
Marc Gravell

@Marc: Isso é verdade - eu nem tinha pensado em variáveis ​​capturadas. Hmm. Sim, acho que vou deixar isso pra lá;)
Jon Skeet

você poderia dizer o que acontecerá quando você definir "foo = null" em seu trecho de código acima? Pelo que eu sei, essa linha limpa apenas o valor de uma variável apontando para o objeto foo no heap gerenciado. então a questão é o que acontecerá ao objeto foo lá? não deveríamos ligar para descartar isso?
odiseh

@odiseh: Se o objeto fosse descartável, então sim - você deveria descartá-lo. Essa seção da resposta tratava apenas da coleta de lixo, que é totalmente separada.
Jon Skeet,

1
Eu estava procurando um esclarecimento sobre algumas questões de IDisposable, então eu pesquisei por "Skeet IDisposable" e encontrei isto. Ótimo! : D
Maciej Wozniak

22

Quando você descarta um objeto, os recursos são liberados. Quando você atribui null a uma variável, está apenas mudando uma referência.

myclass = null;

Depois de executar isso, o objeto ao qual myclass estava se referindo ainda existe e continuará até que o CG comece a limpá-lo. Se Dispose for chamado explicitamente ou se estiver em um bloco de uso, todos os recursos serão liberados o mais rápido possível.


7
Ele pode não ainda existem depois de executar essa linha - que pode ter sido lixo coletado antes que a linha. O JIT é inteligente - fazer frases como essa quase sempre irrelevantes.
Jon Skeet

6
Definir como nulo pode significar que os recursos mantidos pelo objeto nunca são liberados. O GC não descarta, ele apenas finaliza, portanto, se o objeto contiver recursos não gerenciados diretamente e seu finalizador não descartar (ou não tiver um finalizador), esses recursos vazarão. Algo a ter em conta.
LukeH

6

As duas operações não têm muito a ver uma com a outra. Quando você define uma referência como null, ela simplesmente faz isso. Por si só, não afeta a classe que foi referenciada. Sua variável simplesmente não aponta mais para o objeto que costumava apontar, mas o próprio objeto permanece inalterado.

Quando você chama Dispose (), é uma chamada de método no próprio objeto. O que quer que o método Dispose faça, agora é feito no objeto. Mas isso não afeta sua referência ao objeto.

A única área de sobreposição é que, quando não há mais referências a um objeto, ele eventualmente será coletado como lixo. E se a classe implementar a interface IDisposable, então Dispose () será chamado no objeto antes de ser coletado como lixo.

Mas isso não acontecerá imediatamente após definir sua referência como nula, por dois motivos. Primeiro, outras referências podem existir, então ele não será coletado de forma alguma ainda, e segundo, mesmo que essa seja a última referência, então agora está pronto para ser coletado, nada acontecerá até que o coletor de lixo decida excluir o objeto.

Chamar Dispose () em um objeto não "mata" o objeto de forma alguma. É comumente usado para limpar, de forma que o objeto possa ser excluído com segurança posteriormente, mas, no final das contas, não há nada mágico em Dispose, é apenas um método de classe.


Acho que essa resposta complementa ou é um detalhe para a resposta "recursiva".
dance2die
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.