Comportement du récupérateur de place pour Destructor

9

J'ai une classe simple définie comme ci-dessous.

public class Person
{
    public Person()
    {

    }

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

    ~Person()
    {
        p = this;

    }
    public static Person p;
}

Dans la méthode principale

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

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

    }

Le garbage collector est-il censé servir de référence principale pour Person.p et quand exactement le destructeur sera-t-il appelé?

Parimal Raj
la source
Premièrement: un destructeur en C # est censé être un finaliseur . Deuxièmement: définir votre instance singleton sur l'instance en cours de finalisation semble être une très très mauvaise idée . Troisièmement: qu'est-ce que c'est Person1? Je vois seulement Person. Dernier: voir docs.microsoft.com/dotnet/csharp/programming-guide/… pour le fonctionnement des finaliseurs.
HimBromBeere
@HimBromBeere Person1est en fait Person, a corrigé la faute de frappe.
Parimal Raj
@HimBromBeere C'était en fait une question d'entrevue, maintenant selon ma compréhension, CG.Collect aurait dû invoquer destructor mais il ne l'a pas fait.
Parimal Raj
2
(1) Si vous re-référencez l'objet en cours de finalisation à l'intérieur de son finaliseur, alors IL NE SERA PAS COLLECTÉ jusqu'à ce que cette référence ne soit plus accessible depuis une racine (cela a donc pour effet de retarder sa collecte des ordures). (2) Le moment où un finaliseur est appelé n'est pas prévisible.
Matthew Watson
@HimBromBeere et quand je mets le point d'arrêt à Console.WriteLine Person.p arrive comme nul, indépendamment de l' GC.Collectappel
Parimal Raj

Réponses:

13

La chose qui vous manque ici est que le compilateur prolonge la durée de vie de votre xvariable jusqu'à la fin de la méthode dans laquelle elle est définie - c'est juste quelque chose que le compilateur fait - mais il ne le fait que pour une construction DEBUG.

Si vous modifiez le code afin que la variable soit définie dans une méthode distincte, cela fonctionnera comme prévu.

La sortie du code suivant est:

False
True

Et le code:

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

Donc, fondamentalement, votre compréhension était correcte, mais vous ne saviez pas que le compilateur sournois allait garder votre variable en vie jusqu'à ce que vous l'appeliez GC.Collect()- même si vous la définissez explicitement sur null!

Comme je l'ai noté ci-dessus, cela ne se produit que pour une build DEBUG - probablement pour que vous puissiez inspecter les valeurs des variables locales pendant le débogage jusqu'à la fin de la méthode (mais c'est juste une supposition!).

Le code d'origine FONCTIONNE comme prévu pour une build de version - donc le code suivant sort false, truepour une build RELEASE et false, falsepour une build 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.
        }
    }
}

En tant qu'additif: notez que si vous faites quelque chose dans le finaliseur pour une classe qui fait qu'une référence à l'objet en cours de finalisation est accessible à partir d'une racine de programme, alors cet objet ne sera PAS récupéré à moins que et jusqu'à ce que cet objet ne soit plus référencé.

En d'autres termes, vous pouvez donner à un objet un "sursis d'exécution" via le finaliseur. Ceci est généralement considéré comme une mauvaise conception!

Par exemple, dans le code ci-dessus, où nous le faisons _extendMyLifetime = thisdans le finaliseur, nous créons une nouvelle référence à l'objet, il ne sera donc plus récupéré tant que _extendMyLifetime(et toute autre référence) ne le référencera plus.

Matthew Watson
la source