0

I'm playing around with finalizers in c# and found this example which shows that the finalizer can be called even if the constructor didn't succeed.

  1. How correct is this behavior?
  2. Is there a description of this mechanism somewhere in the documentation?
  3. Why is this happening?
internal class Program
{
    public static void Main(string[] args)
    {
        Do();

        GC.Collect();
        Console.ReadLine();
    }

    public static void Do()
    {
        var objects = new BigObject[1000];
        try
        {
            for (var i = 0; i < objects.Length; i++)
            {
                var obj = new BigObject();
                objects[i] = obj;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

public class BigObject
{
    public BigObject()
    {
        Console.WriteLine(GetHashCode());
    }

    private Array _array = new int[1_000_000_000];

    ~BigObject()
    {
        Console.WriteLine("~" + GetHashCode());
    }
}

Output:

58225482
54267293
18643596
33574638
33736294
35191196
48285313
31914638
18796293
34948909
46104728
12289376
43495525
55915408
33476626
System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at ConsoleApp11.BigObject..ctor() in C:\Users\t.lemeshko\source\repos\ConsoleApp11\Program.cs:line 41
   at ConsoleApp11.Program.Do() in C:\Users\t.lemeshko\source\repos\ConsoleApp11\Program.cs:line 22
~55915408
~43495525
~12289376
~7880838
~46104728
~34948909
~18796293
~31914638
~48285313
~35191196
~33736294
~33574638
~18643596
~54267293
~58225482
~33476626

So whe can see there is an object with hash 7880838 has been destroyed, but not cinstructed

Timur Lemeshko
  • 2,747
  • 5
  • 27
  • 39
  • See also https://stackoverflow.com/questions/188693/is-the-destructor-called-if-the-constructor-throws-an-exception and https://stackoverflow.com/questions/40692553/exception-throw-from-constructor-and-what-happen-to-instance-and-memory-allocati – Charlieface Jun 08 '23 at 10:50
  • thnks. i've seen it, and no, there is no exact answer for my question – Timur Lemeshko Jun 08 '23 at 11:12

2 Answers2

1

From the documentation

Finalizers (historically referred to as destructors) are used to perform any necessary final clean-up when a class instance is being collected by the garbage collector

Note that object creation is a multi step process, simplified this involve allocating memory for the object, and then initializing it. In this case it is the object initialization that fails when it tries to allocate the array. Note that the array is separate from the BigObject. Your BigObject is actually really small, it just contains a single reference.

Since the GC has succeeded in allocating memory for the object it needs to clean it up, regardless if the initialization succeeded or not. Otherwise any exception in a constructor would result in a memory leak. And when the object is collected its finalizer need to run according to the quoted rule.

If the failure occurred in the actual allocation step for your object I would not expect the finalizer to run. Since the there was no allocation there is no need for any cleanup, and therefore no finalizer. This could happen if your BigObject actually was big, i.e. containing millions of fields instead of just one.

Also not that the finalizer is intended for disposing unmanaged resources, you cannot safely access any other managed object within a finalizer, since they may already have been collected. You need to be careful overall when writing finalizers, since mistakes can easily result in memory leaks or other bugs.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Arguably the [spec is unclear](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/basic-concepts#79-automatic-memory-management): *"When the object is created, memory is allocated for it, the constructor is run, and the object is considered live."* probably should say *"When the object is created, memory is allocated for it, the object is considered live, and the constructor is run."* – Charlieface Jun 08 '23 at 10:53
  • @Charlieface Agreed. I find it not that uncommon that the finer details are a bit fuzzy in the specification. – JonasH Jun 08 '23 at 11:01
  • ECMA-335 (for .NET) on the other hand says "called when an instance of the class is no longer reachable" which is not very clear, but I suppose by definition if the constructor has run even partially then it *was* at one point reachable and is not anymore. – Charlieface Jun 08 '23 at 11:05
1

According to this article the fields are initialized before calling constructor. In this case the exception is thrown before constructor code execution, still an object is created, hence the object finalizer is called after.

twerk kid
  • 76
  • 3