C# Destructor not working as expected

2019-06-21 20:39发布

问题:

Please see the code below. I expect it to print either 10 because I have explicitly invoked the garbage collector. But I always get either a 0 or 20 as output. Why is that?

void Main()
{
    Panda[] forest_panda = new Panda[10];
    for(int i=0; i<forest_panda.GetLength(0);i++)
    {
        forest_panda[i]=new Panda("P1");
    }

    for(int i=0; i<forest_panda.GetLength(0);i++)
    {
        forest_panda[i]=new Panda("P1");
    }

    System.GC.Collect();

    Console.WriteLine("Total Pandas created is {0}",Panda.population);          
}

class Panda
{
    public static int population=0;
    public string name;

    public Panda(string name)
    {
        this.name = name;
        population = population + 1;
    }

    ~Panda()
    {
        population = population - 1;
    }   
}

Please note that the class for Main is automatically created by LINQPad (the editor that comes with the "C# 4.0 in a Nutshell" book). I am new to C#.

回答1:

You have not run an explict garbage collection. From the docs of GC.Collect():

Use this method to attempt to reclaim all memory that is inaccessible. However, the Collect method does not guarantee that all inaccessible memory is reclaimed.

All objects, regardless of how long they have been in memory, are considered for collection; however, objects that are referenced in managed code are not collected. Use this method to force the system to attempt to reclaim the maximum amount of available memory.

The garabage collector is highly optimized and "decides" all by himself when he actually does the garbage collection and then call the finalizers. Additionally it is all done asynchronously. That is also why Finalizers are called non-deterministic cleanup. You never now when cleanup happens.

You have two options now. You can either call GC.WaitForPendingFinalizers() wich will halt the current thread until the all finalizable objects have been finalized. Or call this new overload: System.GC.Collect(int generation, System.GCCollectionMode mode) with GCCollectionMode.Forced It was introduced in .NET 3.5.

Just keep in mind that usually it is not necessary and more importantly: a bad idea to call the garbage collector manually. Also implementing the finalizer is only needed in rare occasions. Calling the garbage collector will slow down the runtime. Implementing finalizers will slow down the runtime additionally. The garabge collector puts all objects that implement the finalizer into the finalization queue when they are ready to be garabge collected. Processing this queue is expensive. To make things worse, when the finalizer is run, it is not guaranteed that the members you are trying to access there are still alive. It is very well possible they have already been grabage collected. That's why you should use the finalizer only when you have unmanaged resources that need to be cleaned up.

All this is definately not needed in your example. What you acutally want is IDisposable for deterministic cleanup.



回答2:

There are a couple of things to notice here:

First of all the GC behaves differently between release and debug builds. Generally, in release mode objects can be reclaimed sooner than in debug mode.

As Tim points out calling GC.Collect doesn't call finalizers. If you want to wait for finalizers to run call GC.WaitForPendingFinalizers as well.

Finalizers are run by a dedicated thread, so you're actually modifying state from two different threads without any synchronization. While this may not be a problem in this particular case, doing so is not a good idea. But before you go and add synchronization to your finalizer, please keep in mind that a deadlocked finalizer means that no more finalizers will run and thus the memory for those objects will not be reclaimed.



回答3:

Try adding System.GC.WaitForPendingFinalizers after you garbage collect.
http://www.developer.com/net/csharp/article.php/3343191/C-Tip-Forcing-Garbage-Collection-in-NET.htm



回答4:

You create twenty objects, so the value would then be 20. Explicitly calling System.GC.Collect() does not actually guarantee calling the destructor. Therefore if it was called all 20 objects may have been destructed or none may have been.

This explains what is actually happening.

It isn't good practice to create a destructor or call GC.Collect explicitly.

If An object needs to do cleanup, it should implement IDisposable



回答5:

In .NET, object lifetimes are non-deterministic and do not behave as you'd expect from C++ constructor/destructors. In fact, .NET objects do not technically have destructors. The finalizer differs in that it is expected to clean up unmanaged resources used by the object during it's lifetime.

To have a deterministic way of freeing resources used by your object, you implement the IDisposable interface. IDisposable isn't perfect though as it still requires the calling code to correctly dispose of the object when it's done, and it's hard to handle accidental multiple calls to Dispose. However syntax in C# makes this generally very easy.

class Panda : IDisposable
{
    public static int population = 0;
    public string _name;

    public Panda( string name )
    {
        if( name == null )
            throw new ArgumentNullException( name );

        _name = name;
        population++;
    }

    protected virtual void Dispose( bool disposing )
    {
        if( disposing && name != null )
        {
            population--;
            name = null;
        }
    }

    public void Dispose()
    {
        Dispose( true );
        GC.SuppressFinalize( this );
    }

    ~Panda(){ Dispose( false ); }
}

Then to use the class:

using( var panda = new Panda( "Cute & Cuddly" ) )
{
    // Do something with the panda

} // panda.Dispose() called automatically


回答6:

Using destructors (a.k.a. finalizers) is not really a good way of doing things in C#. There is no guarantee that the finalizer will ever run, even if you explicitly invoke the garbage collector. You shouldn't try to force garbage collection either, because it will probably have a negative perfomance impact on your application overall.

Instead, if you need to explicitly free resources owned by an object, you should implement the IDisposable interface, and place your cleanup logic inside the Dispose() method. Conversely, when you use an object that implements IDisposable, you should always take care to call its Dispose() method when you are finished with it. C# provides the "using" statement for this purpose.

Many classes that do I/O (such as Streams) implement IDisposable. Here is an example of using a FileStream to read a text file. Note the "using" statement to ensure the FileStream is disposed when we are finished with it:

using (FileStream fs = File.OpenRead("C:\\temp\\myfile.txt"))
{
    // Read a text file 1024 bytes at a time and write it to the console
    byte[] b = new byte[1024];
    while (fs.Read(b, 0, b.Length) > 0)
    {
        Console.WriteLine(Encoding.UTF8.GetString(b));
    }
} // Dispose() is called automatically here

The above code is equivalent to this:

FileStream fs = File.OpenRead("C:\\temp\\myfile.txt"))
try
{
    // Read a text file 1024 bytes at a time and write it to the console
    byte[] b = new byte[1024];
    while (fs.Read(b, 0, b.Length) > 0)
    {
        Console.WriteLine(Encoding.UTF8.GetString(b));
    }
}
finally
{
    fs.Dispose();
}


回答7:

The Disposing Pattern would be the best to use. Here's the full implementation of your work.

Remember, that you have to call Dispose by your own like done in the code below.

    public static void Main()
    {
        Panda[] forest_panda = new Panda[10];
        for (int i = 0; i < forest_panda.GetLength(0); i++)
            forest_panda[i] = new Panda("P1");

        // Dispose the pandas by your own
        foreach (var panda in forest_panda)
            panda.Dispose();


        for (int i = 0; i < forest_panda.GetLength(0); i++)
            forest_panda[i] = new Panda("P1");

        // Dispose the pandas by your own
        foreach (var panda in forest_panda)
            panda.Dispose();

        Console.WriteLine("Total Pandas created is {0}", Panda.population);
    }

    class Panda : IDisposable
    {
        public static int population = 0;
        public string name;

        public Panda(string name)
        {
            this.name = name;
            population = population + 1;
        }

        ~Panda()
        {
            Dispose(false);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                population = population - 1;
            }
        }
    }