I come from a C++ background and I've been working with C# for about a year. Like many others I'm flummoxed as to why deterministic resource management is not built-in to the language. Instead of deterministic destructors we have the dispose pattern. People start to wonder whether spreading the IDisposable cancer through their code is worth the effort.
In my C++-biased brain it seems like using reference-counted smart pointers with deterministic destructors is a major step up from a garbage collector that requires you to implement IDisposable and call dispose to clean up your non-memory resources. Admittedly, I'm not very smart... so I'm asking this purely from a desire to better understand why things are the way they are.
What if C# were modified such that:
Objects are reference counted. When an object's reference count goes to zero, a resource cleanup method is called deterministically on the object, then the object is marked for garbage collection. Garbage collection occurs at some non-deterministic time in the future at which point memory is reclaimed. In this scenario you don't have to implement IDisposable or remember to call Dispose. You just implement the resource cleanup function if you have non-memory resources to release.
- Why is that a bad idea?
- Would that defeat the purpose of the garbage collector?
- Would it be feasible to implement such a thing?
EDIT: From the comments so far, this is a bad idea because
- GC is faster without reference counting
- problem of dealing with cycles in the object graph
I think number one is valid, but number two is easy to deal with using weak references.
So does the speed optimization outweigh the cons that you:
- may not free a non-memory resource in a timely manner
- might free a non-memory resource too soon
If your resource cleanup mechanism is deterministic and built-in to the language you can eliminate those possibilities.
Brad Abrams posted an e-mail from Brian Harry written during development of the .Net framework. It details many of the reasons reference counting was not used, even when one of the early priorities was to keep semantic equivalence with VB6, which uses reference counting. It looks into possibilities such as having some types ref counted and not others (
IRefCounted
!), or having specific instances ref counted, and why none of these solutions were deemed acceptable.Reference counting was tried in C#. I believe, the folks that released Rotor (a reference implementation of CLR for which the source was made available) did reference counting-based GC just to see how it would compare to the generational one. The result was surprising -- the "stock" GC was so much faster, it was not even funny. I don't remember exactly where I heard this, I think it was one of the Hanselmuntes podcasts. If you want to see C++ get basically crushed in performance comparison with C# -- google Raymond Chen's chinese dictionary app. He did a C++ version, and then Rico Mariani did a C# one. I think it took Raymond 6 iterations to finally beat the C# version, but by that time he had to drop all the nice object orientednes of C++, and get down to the win32 API level. The entire thing turned into a performance hack. C# program, at the same time, was optimized only once, and in the end still looked like a decent OO project
Deterministic non-memory resource management is part of the language, however it is not done with destructors.
Your opinion is common among people coming from a C++ background, attempting to use the RAII design pattern. In C++ the only way you can guarrantee that some code will run in the end of a scope, even if an exeption is thrown, is to allocate an object on the stack and put the clean-up code in the destructor.
In other languages (C#, Java, Python, Ruby, Erlang, ...) you can use try-finally (or try-catch-finally) instead to ensure that the clean-up code will always run.
I C#, you can also use the using construct:
Thus, for a C++-programmer, it might help to think about "running clean-up code" and "freeing memory" as two separate things. Put your clean-up code in a finally block and leave to the GC to take care of the memory.
There's a lot of issues in play here. First of all you need to distinguish between freeing managed memory and clean-up of other resources. The former can be really fast whereas the later may be very slow. In .NET the two are separated, which allows for faster clean-up of managed memory. This also implies, that you should only implement Dispose/Finalizer when you have something beyond managed memory to clean up.
The .NET employs a mark and sweep technique where it traverses the heap looking for roots to objects. Rooted instances survive the garbage collection. Everything else can be cleaned by just reclaiming the memory. The GC has to compact memory every now and then, but apart from that reclaiming memory is a simple pointer operation even when reclaiming multiple instances. Compare this with multiple calls to destructors in C++.