Você precisa descartar objetos e defini-los como nulos?

310

Você precisa descartar objetos e configurá-los como nulos ou o coletor de lixo os limpará quando ficarem fora do escopo?

CJ7
fonte
4
Parece haver um consenso de que você não precisa definir o objeto como nulo, mas precisa Dispose ()?
CJ7
3
como Jeff disse: codinghorror.com/blog/2009/01/…
tanathos
9
Meu conselho é sempre descartar se um objeto implementa IDisposable. Use um bloco de uso sempre. Não faça suposições, não deixe ao acaso. Você não precisa definir as coisas como nulas. Um objeto acabou de sair do escopo.
Peter
11
@ Pedro: Não use "usando" blocos com proxies cliente WCF: msdn.microsoft.com/en-us/library/aa355056.aspx
nlawalker
9
No entanto, você pode querer definir algumas referências para null dentro do seu Dispose()método! Essa é uma variação sutil dessa questão, mas importante porque o objeto que está sendo descartado não pode saber se está "saindo do escopo" (chamar Dispose()não é garantia). Mais aqui: stackoverflow.com/questions/6757048/…
Kevin P. Rice

Respostas:

239

Os objetos serão limpos quando não estiverem mais sendo usados ​​e quando o coletor de lixo achar conveniente. Às vezes, pode ser necessário definir um objeto para nulltorná-lo fora do escopo (como um campo estático cujo valor você não precisa mais), mas no geral geralmente não há necessidade de definir null.

Em relação à disposição de objetos, concordo com @Andre. Se o objeto for IDisposable, é uma boa idéia descartá-lo quando você não precisar mais dele, especialmente se o objeto usar recursos não gerenciados. Não descartar recursos não gerenciados levará a vazamentos de memória .

Você pode usar a usinginstrução para descartar automaticamente um objeto assim que seu programa sair do escopo da usinginstrução.

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here

Qual é funcionalmente equivalente a:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}
Zach Johnson
fonte
4
Se obj é um tipo de referência, o bloco final é equivalente a:if (obj != null) ((IDisposable)obj).Dispose();
Randy suporta Monica
1
@Tuzo: Obrigado! Editado para refletir isso.
Zach Johnson
2
Uma observação a respeito IDisposable. Deixar de Dispor um objeto geralmente não causa vazamento de memória em nenhuma classe bem projetada. Ao trabalhar com recursos não gerenciados em C #, você deve ter um finalizador que ainda liberará os recursos não gerenciados. Isso significa que, em vez de desalocar os recursos quando isso deve ser feito, será adiado para quando o coletor de lixo finalizar o objeto gerenciado. Ainda pode causar muitos outros problemas (como bloqueios não lançados). Você deve descartar um IDisposablepensamento!
Aidiakapi
@RandyLevy Você tem uma referência para isso? Obrigado
Básico
Mas minha pergunta é Dispose () precisa implementar alguma lógica? Isso tem que fazer alguma coisa? Ou internamente, quando Dispose () é chamado de sinais GC, o que é bom de se fazer? Eu verifiquei o código fonte do TextWriter, por exemplo, e Dispose não tem implementação.
Mihail Georgescu
137

Os objetos nunca ficam fora do escopo em C #, como em C ++. Eles são tratados pelo Garbage Collector automaticamente quando não são mais usados. Essa é uma abordagem mais complicada que o C ++, onde o escopo de uma variável é inteiramente determinístico. O coletor de lixo CLR passa ativamente por todos os objetos que foram criados e funciona se eles estiverem sendo usados.

Um objeto pode ficar "fora do escopo" em uma função, mas se seu valor for retornado, o GC verificará se a função de chamada mantém ou não o valor de retorno.

Definir referências de objetos nullcomo desnecessário, pois a coleta de lixo funciona, trabalhando para quais objetos estão sendo referenciados por outros objetos.

Na prática, você não precisa se preocupar com a destruição, apenas funciona e é ótimo :)

Disposedeve ser chamado em todos os objetos implementados IDisposablequando você terminar de trabalhar com eles. Normalmente você usaria um usingbloco com esses objetos da seguinte maneira:

using (var ms = new MemoryStream()) {
  //...
}

EDIT No escopo variável. Craig perguntou se o escopo da variável tem algum efeito no tempo de vida do objeto. Para explicar adequadamente esse aspecto do CLR, precisarei explicar alguns conceitos de C ++ e C #.

Escopo da variável real

Nos dois idiomas, a variável só pode ser usada no mesmo escopo que foi definido - classe, função ou um bloco de instrução entre chaves. A diferença sutil, no entanto, é que, em C #, as variáveis ​​não podem ser redefinidas em um bloco aninhado.

Em C ++, isso é perfeitamente legal:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8

Em C #, no entanto, você recebe um erro do compilador:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}

Isso faz sentido se você observar o MSIL gerado - todas as variáveis ​​usadas pela função são definidas no início da função. Dê uma olhada nesta função:

public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}

Abaixo está a IL gerada. Observe que o iVal2, que é definido dentro do bloco if, é realmente definido no nível da função. Efetivamente, isso significa que o C # só tem escopo de nível de classe e função no que diz respeito à vida útil variável.

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope

Escopo C ++ e vida útil do objeto

Sempre que uma variável C ++, alocada na pilha, sai do escopo, ela é destruída. Lembre-se de que, em C ++, você pode criar objetos na pilha ou na pilha. Quando você os cria na pilha, uma vez que a execução sai do escopo, eles são retirados da pilha e destruídos.

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives

Quando objetos C ++ são criados no heap, eles devem ser explicitamente destruídos, caso contrário, é um vazamento de memória. Porém, não existe esse problema com as variáveis ​​da pilha.

Vida útil do objeto C #

No CLR, os objetos (ou seja, tipos de referência) são sempre criados no heap gerenciado. Isso é reforçado ainda mais pela sintaxe de criação de objeto. Considere esse trecho de código.

MyClass stackObj;

Em C ++, isso criaria uma instância na MyClasspilha e chamaria seu construtor padrão. Em C #, criaria uma referência à classe MyClassque não aponta para nada. A única maneira de criar uma instância de uma classe é usando o newoperador:

MyClass stackObj = new MyClass();

De certa forma, os objetos C # são muito parecidos com os objetos criados usando a newsintaxe em C ++ - eles são criados no heap, mas, diferentemente dos objetos C ++, eles são gerenciados pelo tempo de execução, portanto você não precisa se preocupar em destruí-los.

Como os objetos estão sempre na pilha, o fato de as referências a objetos (ou seja, ponteiros) ficarem fora do escopo torna-se discutível. Existem mais fatores envolvidos para determinar se um objeto deve ser coletado do que simplesmente a presença de referências ao objeto.

Referências de objeto em C #

Jon Skeet comparou as referências de objeto em Java a pedaços de string anexados ao balão, que é o objeto. A mesma analogia se aplica às referências de objeto C #. Eles simplesmente apontam para um local da pilha que contém o objeto. Assim, defini-lo como nulo não tem efeito imediato no tempo de vida do objeto, o balão continua a existir, até que o GC o "abra".

Continuando pela analogia do balão, parece lógico que, uma vez que o balão não tenha cordas, ele poderá ser destruído. De fato, é exatamente assim que os objetos contados de referência funcionam em linguagens não gerenciadas. Exceto que essa abordagem não funciona muito bem para referências circulares. Imagine dois balões que são presos por uma corda, mas nenhum deles tem uma corda para mais nada. Sob regras simples de contagem, ambas continuam existindo, mesmo que todo o grupo de balões seja "órfão".

Objetos .NET são muito parecidos com balões de hélio sob o teto. Quando o teto é aberto (o GC é executado) - os balões não utilizados flutuam para longe, mesmo que haja grupos de balões presos juntos.

O .NET GC usa uma combinação de GC geracional e marca e varredura. A abordagem geracional envolve o tempo de execução que favorece a inspeção de objetos alocados mais recentemente, pois é mais provável que eles não sejam utilizados e a marcação e a varredura envolvem o tempo de execução percorrendo todo o gráfico de objetos e resolvendo se há grupos de objetos que não são utilizados. Isso lida adequadamente com o problema de dependência circular.

Além disso, o .NET GC é executado em outro thread (o chamado thread do finalizador), pois tem muito o que fazer, e isso no thread principal interromperia o programa.

Igor Zevaka
fonte
1
@ Igor: Ao sair do escopo, quero dizer que a referência do objeto está fora de contexto e não pode ser referida no escopo atual. Certamente isso ainda acontece em c #.
CJ7
@ Craig Johnston, não confunda o escopo da variável usado pelo compilador com a duração da variável, que é determinada pelo tempo de execução - eles são diferentes. Uma variável local pode não estar "ativa", embora ainda esteja no escopo.
Randy apoia Monica
1
@ Craig Johnston: Consulte blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx : "não há garantia de que uma variável local permaneça ativa até o final de um escopo, se não estiver O tempo de execução é livre para analisar o código que possui e determinar o que não há usos adicionais de uma variável além de um certo ponto e, portanto, não manter essa variável ativa além desse ponto (ou seja, não tratá-la como uma raiz para os fins do GC) ".
Randy suporta Monica
1
@Tuzo: Verdadeiro. É para isso que serve o GC.KeepAlive.
Steven Sudit 01/06/10
1
@ Craig Johnston: Não e sim. Não, porque o tempo de execução do .NET gerencia isso para você e faz um bom trabalho. Sim, porque o trabalho do programador não é escrever código que (apenas) compila, mas escrever código que é executado . Às vezes, ajuda a saber o que o tempo de execução está fazendo nos bastidores (por exemplo, solução de problemas). Alguém poderia argumentar que é o tipo de conhecimento que ajuda a separar bons programadores de ótimos programadores.
Randy apoia Monica
18

Como outros já disseram, você definitivamente deseja ligar Disposese a classe implementar IDisposable. Eu tomo uma posição bastante rígida sobre isso. Alguns afirmam poder que chamar Disposeem DataSet, por exemplo, é inútil porque eles desmontaram e viu que ele não fez nada de significativo. Mas acho que existem muitas falácias nesse argumento.

Leia isso para um debate interessante de pessoas respeitadas sobre o assunto. Então leia meu raciocínio aqui porque acho que Jeffery Richter está no campo errado.

Agora, se você deve ou não definir uma referência null. A resposta é não. Deixe-me ilustrar meu argumento com o seguinte código.

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}

Então, quando você acha que o objeto referenciado por aé elegível para coleta? Se você disse que após a ligação a = null, está errado. Se você disse que após a conclusão do Mainmétodo, também está errado. A resposta correta é que ele é elegível para coleta em algum momento durante a chamada para DoSomething. Isso está certo. É elegível antes que a referência seja configurada nulle talvez até antes da conclusão da chamada DoSomething. Isso ocorre porque o compilador JIT pode reconhecer quando as referências a objetos não são mais desreferenciadas, mesmo que ainda estejam enraizadas.

Brian Gideon
fonte
3
E se ahouver um campo de membro privado em uma classe? Se anão estiver definido como nulo, o GC não tem como saber se aserá usado novamente em algum método, certo? Portanto a, não será coletado até que toda a classe que contém seja coletada. Não?
22411 Kevin P. Rice
4
@ Kevin: Correto. Se afosse um membro da classe e a classe que aainda estivesse enraizada e em uso, ela também permaneceria por aí. Esse é um cenário em que configurá-lo nullpode ser benéfico.
27711 Brian Gideon
1
Seu argumento está relacionado a um motivo pelo qual Disposeé importante - não é possível invocar Dispose(ou qualquer outro método não-inlinável) em um objeto sem uma referência a ele; chamar Disposedepois que uma é feita usando um objeto garantirá que uma referência com raiz continuará a existir durante toda a última ação executada nele. O abandono de todas as referências a um objeto sem chamar Disposepode ironicamente fazer com que os recursos do objeto sejam ocasionalmente liberados muito cedo .
Supercat
Este exemplo e explicação não parecem definitivos na sugestão rígida de nunca definir referências como nulas. Quero dizer, exceto pelo comentário de Kevin, uma referência definida como nula depois de descartada parece bastante benigna , então qual é o mal? Estou esquecendo de algo?
dathompson 25/01
13

Você nunca precisa definir objetos como nulos em C #. O compilador e o tempo de execução cuidarão de descobrir quando eles não estão mais no escopo.

Sim, você deve descartar objetos que implementem IDisposable.

EMP
fonte
2
Se você tiver uma referência de longa duração (ou até estática) a um objeto grande, wantanule-a assim que terminar com ela, para que ela possa ser recuperada gratuitamente.
Steven Sudit 01/06
12
Se você já "terminou", não deve ser estático. Se não é estático, mas "de longa duração", ele ainda deve ficar fora do escopo logo após o término. A necessidade de definir referências como null indica um problema com a estrutura do código.
EMP
Você pode ter um item estático com o qual terminar. Considere: Um recurso estático que é lido do disco em um formato amigável e analisado em um formato adequado para o uso do programa. Você pode acabar com uma cópia privada dos dados brutos que não servem a nenhum outro propósito. (Exemplo do mundo real: A análise é uma rotina de duas passagens e, portanto, não pode simplesmente processar os dados como ele é lido.)
Loren Pechtel
1
Em seguida, ele não deve armazenar dados brutos em um campo estático se for usado apenas temporariamente. Claro, você pode fazer isso, simplesmente não é uma boa prática exatamente por esse motivo: você precisa gerenciar sua vida útil manualmente.
EMP
2
Você evita isso armazenando os dados brutos em uma variável local no método que os processa. O método retorna os dados processados, que você mantém, mas o local dos dados brutos fica fora do escopo quando o método sai e é automaticamente GCed.
EMP
11

Concordo com a resposta comum aqui de que sim, você deve descartar e não, geralmente não deve definir a variável como nula ... mas eu gostaria de ressaltar que descartar NÃO é principalmente sobre gerenciamento de memória. Sim, ele pode ajudar (e às vezes ajuda) no gerenciamento de memória, mas seu objetivo principal é fornecer a liberação determinística de recursos escassos.

Por exemplo, se você abrir uma porta de hardware (serial, por exemplo), um soquete TCP / IP, um arquivo (no modo de acesso exclusivo) ou mesmo uma conexão com o banco de dados, você impediu qualquer outro código de usar esses itens até que eles sejam liberados. Descarte geralmente libera esses itens (junto com GDI e outros identificadores de "sistema operacional" etc.), dos quais existem milhares de disponíveis, mas ainda são limitados no geral). Se você não chamar dipose no objeto proprietário e liberar explicitamente esses recursos, tente abrir o mesmo recurso novamente no futuro (ou outro programa o faça); a tentativa de abertura falhará porque seu objeto não exposto e não coletado ainda tem o item aberto . Obviamente, quando o GC coletar o item (se o padrão Dispose tiver sido implementado corretamente), o recurso será liberado ... mas você não sabe quando será, então você não não sei quando é seguro reabrir esse recurso. Esse é o principal problema do Dispose. Obviamente, liberar esses identificadores também libera memória também, e nunca liberá-los pode nunca liberar essa memória ... portanto, toda a conversa sobre vazamentos de memória ou atrasos na limpeza da memória.

Eu vi exemplos do mundo real disso causando problemas. Por exemplo, eu vi aplicativos Web do ASP.Net que eventualmente não conseguem se conectar ao banco de dados (embora por curtos períodos de tempo ou até que o processo do servidor da Web seja reiniciado) porque o pool de conexões do servidor sql está cheio ... , tantas conexões foram criadas e não liberadas explicitamente em tão pouco tempo que nenhuma nova conexão pode ser criada e muitas das conexões no pool, embora não ativas, ainda são referenciadas por objetos não digitados e não coletados e, portanto, podem ' Não seja reutilizado. O descarte correto das conexões com o banco de dados, sempre que necessário, garante que esse problema não ocorra (pelo menos não, a menos que você tenha acesso simultâneo muito alto).

Yort
fonte
11

Se o objeto for implementado IDisposable, sim, você deve descartá-lo. O objeto pode estar preso a recursos nativos (identificadores de arquivo, objetos do SO) que podem não ser liberados imediatamente caso contrário. Isso pode levar à falta de recursos, problemas de bloqueio de arquivos e outros erros sutis que poderiam ser evitados.

Consulte também Implementando um método Dispose no MSDN.

Chris Schmich
fonte
Mas o coletor de lixo não chamará Dispose ()? Se sim, por que você precisaria chamá-lo?
CJ7
A menos que você chame explicitamente, não há garantia de que Disposeserá chamado. Além disso, se o seu objeto estiver segurando um recurso escasso ou estiver bloqueando algum recurso (por exemplo, um arquivo), será necessário liberá-lo o mais rápido possível. Aguardar o GC fazer isso é subótimo.
Chris Schmich 28/05
12
O GC nunca chamará Dispose (). O GC pode chamar um finalizador que, por convenção, deve limpar os recursos.
Adrianm 28/05
@adrianm: Não mightligue, mas willligue.
Leppie
2
@ leppie: os finalizadores não são determinísticos e podem não ser chamados (por exemplo, quando o domínio do aplicativo é descarregado). Se você precisar de finalização determinística, deverá implementar o que eu acho que é chamado de manipulador crítico. O CLR tem tratamento especial desses objetos para garantir que eles são finalizados (ex-lo pré-jits o código de finalização para lidar com pouca memória)
adrianm
9

Se eles implementarem a interface IDisposable, você deverá descartá-los. O coletor de lixo cuidará do resto.

EDIT: melhor é usar o usingcomando ao trabalhar com itens descartáveis:

using(var con = new SqlConnection("..")){ ...
Andre
fonte
5

Quando um objeto implementa IDisposablevocê deve chamar Dispose(ou Close, em alguns casos, isso chamará Dispose para você).

Você normalmente não precisa definir objetos null, porque o GC saberá que um objeto não será mais usado.

Há uma exceção quando eu defino objetos como null. Quando recupero muitos objetos (do banco de dados) nos quais preciso trabalhar e os armazeno em uma coleção (ou matriz). Quando o "trabalho" é concluído, defino o objeto como null, porque o GC não sabe que terminei de trabalhar com ele.

Exemplo:

using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called
GvS
fonte
4

Normalmente, não há necessidade de definir campos como nulos. Eu sempre recomendo a eliminação de recursos não gerenciados.

Por experiência, eu também aconselho você a fazer o seguinte:

  • Cancele a inscrição nos eventos se você não precisar mais deles.
  • Defina qualquer campo que contenha um delegado ou uma expressão como nulo se não for mais necessário.

Eu me deparei com alguns problemas muito difíceis de encontrar que foram o resultado direto de não seguir os conselhos acima.

Um bom lugar para fazer isso é em Dispose (), mas mais cedo é geralmente melhor.

Em geral, se existir uma referência a um objeto, o coletor de lixo (GC) poderá demorar mais algumas gerações para descobrir que um objeto não está mais em uso. O tempo todo o objeto permanece na memória.

Isso pode não ser um problema até você descobrir que seu aplicativo está usando muito mais memória do que você esperaria. Quando isso acontecer, conecte um perfilador de memória para ver quais objetos não estão sendo limpos. Definir campos que referenciam outros objetos como nulos e limpar coleções à disposição pode realmente ajudar o GC a descobrir quais objetos ele pode remover da memória. O GC recuperará a memória usada mais rapidamente, tornando seu aplicativo muito menos com fome e mais rápido.

Marnix van Valen
fonte
1
O que você quer dizer com 'eventos e delegados' - o que deve ser 'limpo' com eles?
CJ7
@ Craig - eu editei minha resposta. Espero que isso esclareça um pouco.
Marnix van Valen
3

Sempre chame o descarte. Não vale a pena o risco. Grandes aplicativos corporativos gerenciados devem ser tratados com respeito. Nenhuma suposição pode ser feita; caso contrário, ela voltará para morder você.

Não ouça leppie.

Muitos objetos não implementam o IDisposable, então você não precisa se preocupar com eles. Se realmente saírem do escopo, serão liberados automaticamente. Também nunca me deparei com a situação em que tive que definir algo como nulo.

Uma coisa que pode acontecer é que muitos objetos podem ser mantidos abertos. Isso pode aumentar bastante o uso de memória do seu aplicativo. Às vezes, é difícil descobrir se isso é realmente um vazamento de memória ou se seu aplicativo está apenas fazendo um monte de coisas.

As ferramentas de perfil de memória podem ajudar com coisas assim, mas pode ser complicado.

Além disso, sempre cancele a inscrição de eventos desnecessários. Também tenha cuidado com a ligação e os controles do WPF. Não é uma situação usual, mas me deparei com uma situação em que eu tinha um controle WPF que estava sendo vinculado a um objeto subjacente. O objeto subjacente era grande e consumia uma grande quantidade de memória. O controle WPF estava sendo substituído por uma nova instância, e a antiga ainda estava por aí por algum motivo. Isso causou um grande vazamento de memória.

No site posterior, o código foi mal escrito, mas o ponto é que você deseja garantir que as coisas que não são usadas fiquem fora do escopo. Aquele demorou muito tempo para encontrar com um criador de perfil de memória, pois é difícil saber quais itens na memória são válidos e o que não deveria estar lá.

Peter
fonte
2

Eu tenho que responder também. O JIT gera tabelas junto com o código a partir da análise estática do uso de variáveis. Essas entradas da tabela são as "Raízes do GC" no quadro de pilha atual. À medida que o ponteiro da instrução avança, essas entradas da tabela se tornam inválidas e prontas para a coleta de lixo. Portanto: se for uma variável com escopo definido, você não precisará defini-la como nula - o GC coletará o objeto. Se for um membro ou uma variável estática, você deverá configurá-lo como nulo

Hui
fonte