Tuesday, December 02, 2003

Delphi, GC, and .NET

Chris Anderson points to this cool article by Allen Bauer about class helpers in Delphi 8 for .NET.

I read the article with much joy because I have been interested in this IDisposable thing since the beginning. Back then I was convinced that there was a lot of room for improvement in it. Slowly, I became convinced that the IDisposable pattern was as good as it could be.

Now I see Delphi .NET design seems to clash with some of the conceptions I have acquired about GC in .NET. Of course I don't remember a lot about my Turbo Pascal 5.5 days, and I am not a Delphi programmer, but I think my thoughts still apply. Here is my deconstruction:

- Objects without finalizers take one GC cycle to be freed, while objects with finalizers take more.

- Actually in .NET all objects have a Finalize method. It is only that System.Object has a No-Op Finalizer and any class that doesn't override Object.Finalize is considered not to be finalizable. On the other hand the automatically injected Free procedure in Delphi .NET seems to be a No-Op on objects that dosen't have a real Finalizer.

- In accordance with Microsoft guidelines only very few classes should have finalizers. We are advised against writing finalizers for objects that don't directly wrap unmanaged resources. Most classes with finalizers should also implement IDisposable to provide a mean of explicit control. This allows users to avoid the performance penalty of finalizers (for that Dispose calls GC.SuppressFinalize). In addition, objects that aggregate resource managers should also implement IDisposable even when they shouldn't have finalizers themselves.

- Since all Delphi objects seem to have a Free method, I think it makes a lot of sense for it to automatically implement IDisposabe for all objects having finalizers.

- While in the Dispose method you try to dismantle your object graph (you clean up your main object's unmanaged and managed resources, and you even cascade Dispose calls as necessary), in the Finalize method you should avoid touching other objects (you don't even know if they are still alive since you cannot make assumptions about collection order). That is what the IDisposable design pattern is about. That is supposed to be the role of de Disposing parameter: to make Dispose and Finalize slightly different methods.

- But Allen seems to be in favor of allways manually dismantling an object graph before feeding the parts to the Garbage Collector. By dismantling I mean calling foo. Free and cutting all owned references. I had the idea that the GC was clever enough to take care of the entire graph very efficiently and call only the registered finalizers.

- But in my recollection GC will wait on an object finalizer before freeing dependant objects. So pruning the graph early makes some sense.

- For what I read it seems Borland took the IDispose design pattern upside down and maybe solved the problem. They just let you write one Destroy method, then they take care of injecting the disposed flag.

- Since objects are always meant to be freed through a call to Free (even inside container object destructors), they check for the existence of a destructor, then the check for the disposed flag, and they do the right thing.

- This leaves the question of wheather they call SupressFinalize on the object when Free is called preventively.

- I also wonder if the Delphi compiler is intelligent enough not to add a real finalizer when all you need is a disposer. That is the right thing to do when the object aggregates resource managers but has no unmanaged resources to deal with directly. Maybe there should be a way to give the compiler a clue.

- In my understanding Free seems to be static. You can call it even on a nil instance. It will make the check for you.

- This reminds me of the idea some of us had in the DOTNET list about including a "delete" operator in C#. We all had different ideas about how to differentiate the disposing from the finalizing time, but I think none came with the idea of just injecting a "disposed" flag to test if it was the first time the call is made. I think we were afraid of the interactions of such a simple thing with the posiblity of objects being revived in their Finalizer (which I think it is used in pools for reusable objects).

- What if GC.ReRegisterForFinalize and GC.SupressFinalize worked with the "disposed" flag? SupressFinalize turns it off, and ReRegisterForFinalize turns it on. They could also make the "disposed" flag (or should we call it "finalized") accesible through something like bool GC.IsFinalized(object obj). IsFinalized should first check for null reference (in which case it always returns True) and then for the "disposed" flag.

- So "delete foo" coud do something like:

if !GC.IsFinalized(foo)
{
GC.SupressFinalize(foo);
foo.~foo(); //off course Finalize is not public but we have access to it.
foo = null;
}

That looks a lot like SafeDelete!

- All in all, I think Delphi design makes sense because it allows Borland to preserve compatibility and language consistency, but maybe if I were them I would be discouraging Delphi programmers of those practices for future development. For instance, if developers continue to write Destroy procedures for simple memory or references clean up, Delphi.NET applications will suffer a performance penalty because they will all be translated to finalizers. Or am I wrong?

- I wonder how much magic would it take for Microsoft to do something along the lines of what Borland did. I think they could even improve on it.

- Even when I know I forgot something critical (I always do) I go to sleep with the naive feeling that my delete operator for C# is a good thing. I love to think about those things, I always loved it. You can search for diego and dispose in the DOTNET list archives and you will see!

No comments:

Moving to MSDN

I haven't decided yet, but it is very likely that I will stop blogging here for some time. For some background, I have moved to the sate...