Comportamiento del recolector de basura para destructor

9

Tengo una clase simple que se define a continuación.

public class Person
{
    public Person()
    {

    }

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

    ~Person()
    {
        p = this;

    }
    public static Person p;
}

En el método principal

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

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

    }

¿Se supone que el recolector de basura es la referencia principal para Person.p y cuándo exactamente se llamará al destructor?

Parimal Raj
fuente
Primero: un destructor en C # es un finalizador . Segundo: configurar su instancia singleton en la instancia que se está finalizando parece una muy, muy mala idea . Tercero: ¿qué es Person1? Solo veo Person. Por último: consulte docs.microsoft.com/dotnet/csharp/programming-guide/… para ver cómo funcionan los finalizadores.
HimBromBeere
@HimBromBeere Person1es en realidad Person, reparó el error tipográfico.
Parimal Raj
@HimBromBeere Esta fue en realidad una pregunta de entrevista, ahora, según tengo entendido, CG.Collect debería haber invocado al destructor, pero no fue así.
Parimal Raj
2
(1) Si vuelve a hacer referencia al objeto que se está finalizando dentro de su finalizador, NO SE RECOGERÁ BASURA hasta que ya no se pueda acceder a esa referencia desde una raíz (por lo que esto tiene el efecto de retrasar su recolección de basura). (2) El momento en el que se llama a un finalizador no es predecible.
Matthew Watson el
@HimBromBeere y cuando pongo el punto de interrupción en Console.WriteLine Person.p aparece como nulo, independientemente de la GC.Collectllamada
Parimal Raj

Respuestas:

13

Lo que falta aquí es que el compilador está extendiendo la vida útil de su xvariable hasta el final del método en el que se define, eso es algo que hace el compilador, pero solo lo hace para una compilación DEPURACIÓN.

Si cambia el código para que la variable se defina en un método separado, funcionará como espera.

La salida del siguiente código es:

False
True

Y el 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();
        }
    }
}

Así que, básicamente, tu comprensión fue correcta, pero no sabías que el furtivo compilador mantendría viva tu variable hasta después de que llamaste GC.Collect(), ¡incluso si la configuraste explícitamente como nula!

Como señalé anteriormente, esto solo sucede para una compilación DEBUG, presumiblemente para que pueda inspeccionar los valores de las variables locales mientras se depura hasta el final del método (¡pero eso es solo una suposición!).

El código original funciona como se esperaba para una compilación de lanzamiento, por lo que el siguiente código genera false, trueuna compilación RELEASE y false, falseuna compilación 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 anexo: tenga en cuenta que si hace algo en el finalizador para una clase que hace que una referencia al objeto que se está finalizando sea accesible desde la raíz de un programa, entonces ese objeto NO se recolectará basura a menos que y hasta que ese objeto ya no esté referenciado

En otras palabras, puede darle a un objeto una "suspensión de ejecución" a través del finalizador. Sin embargo, esto generalmente se considera un mal diseño.

Por ejemplo, en el código anterior, donde lo hacemos _extendMyLifetime = thisen el finalizador, estamos creando una nueva referencia al objeto, por lo que ahora no se recolectará basura hasta que _extendMyLifetime(y cualquier otra referencia) ya no haga referencia a él.

Matthew Watson
fuente