Comportamento do coletor de lixo para destruidor

9

Eu tenho uma classe simples que é definida como abaixo.

public class Person
{
    public Person()
    {

    }

    public override string ToString()
    {
        return "I Still Exist!";
    }

    ~Person()
    {
        p = this;

    }
    public static Person p;
}

No método principal

    public static void Main(string[] args)
    {
        var x = new Person();
        x = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Person.p == null);

    }

O coletor de lixo deve ser a principal referência para Person.p e quando exatamente o destruidor será chamado?

Parimal Raj
fonte
Primeiro: um destruidor em C # deve ser um finalizador . Segundo: definir sua instância singleton para a instância que está sendo finalizada parece uma péssima idéia . Terceiro: o que é Person1? Eu vejo apenas Person. Por último: consulte docs.microsoft.com/dotnet/csharp/programming-guide/… para saber como os finalizadores funcionam.
HimBromBeere
@HimBromBeere Person1é Person, na verdade , corrigido o erro de digitação.
Parimal Raj
@HimBromBeere Esta era realmente uma pergunta de entrevista, agora, pelo que entendi, o CG.Collect deveria ter invocado o destruidor, mas não o fez.
Parimal Raj
2
(1) Se você referenciar novamente o objeto que está sendo finalizado dentro do finializador, NÃO SERÁ COLECIONADO LIXO até que essa referência não seja mais acessível a partir de uma raiz (portanto, isso atrasará sua coleta de lixo). (2) O momento em que um finalizador é chamado não é previsível.
Matthew Watson
@HimBromBeere e quando eu coloco ponto de interrupção na Console.WriteLine Person.p está chegando como nulo, independentemente da GC.Collectchamada
Parimal Raj

Respostas:

13

O que você está perdendo aqui é que o compilador está estendendo a vida útil da sua xvariável até o final do método em que está definida - isso é apenas algo que o compilador faz - mas apenas o faz para uma compilação DEBUG.

Se você alterar o código para que a variável seja definida em um método separado, ela funcionará conforme o esperado.

A saída do seguinte código é:

False
True

E o código:

using System;

namespace ConsoleApp1
{
    class Finalizable
    {
        ~Finalizable()
        {
            _extendMyLifetime = this;
        }

        public static bool LifetimeExtended => _extendMyLifetime != null;

        static Finalizable _extendMyLifetime;
    }

    class Program
    {
        public static void Main()
        {
            test();

            Console.WriteLine(Finalizable.LifetimeExtended); // False.

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine(Finalizable.LifetimeExtended); // True.
        }

        static void test()
        {
            new Finalizable();
        }
    }
}

Então, basicamente, seu entendimento estava correto, mas você não sabia que o compilador furtivo manteria sua variável ativa até depois que você ligou GC.Collect()- mesmo que você a defina explicitamente como nula!

Como observei acima, isso só acontece para uma compilação DEBUG - presumivelmente para que você possa inspecionar os valores para variáveis ​​locais enquanto depura até o final do método (mas isso é apenas um palpite!).

O código original funciona como esperado para uma compilação de lançamento - portanto, o código a seguir gera false, trueuma compilação RELEASE e false, falseuma compilação DEBUG:

using System;

namespace ConsoleApp1
{
    class Finalizable
    {
        ~Finalizable()
        {
            _extendMyLifetime = this;
        }

        public static bool LifetimeExtended => _extendMyLifetime != null;

        static Finalizable _extendMyLifetime;
    }

    class Program
    {
        public static void Main()
        {
            new Finalizable();

            Console.WriteLine(Finalizable.LifetimeExtended); // False.

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine(Finalizable.LifetimeExtended); // True iff RELEASE build.
        }
    }
}

Como um adendo: observe que, se você fizer algo no finalizador de uma classe que faça com que uma referência ao objeto que está sendo finalizado seja alcançável a partir de uma raiz do programa, esse objeto NÃO será coletado como lixo, a menos e até que esse objeto não esteja mais referenciado.

Em outras palavras, você pode dar a um objeto uma "suspensão da execução" através do finalizador. Geralmente, isso é considerado um projeto ruim!

Por exemplo, no código acima, onde fazemos _extendMyLifetime = thisno finalizador, estamos criando uma nova referência ao objeto, para que ele não seja coletado como lixo até que _extendMyLifetime(e qualquer outra referência) não faça mais referência a ele.

Matthew Watson
fonte