Freigeben über


The long-awaited return of DF

Back from the dead

Well, not precisely dead, but I certainly began feeling that way - shipping a product is hard work, and it is incredibly easy to get "heads down." Here on the VCQA team, we're very focused on stabilization. A lot of testruns. Harsh bug bars. Only the highest quality fixes. Beta 2 is out the door, and it's time to tighten up for the endgame. As we're fond of saying: the product has 2005 on it. By my count, we've got about 6 months to ship a product.

About that DF thing. While composing what was to be my final post on this whole DF topic, I ran into a brick wall. I couldn't seem to explain how to tie it together. It didn't fit well enough with the Dispose(bool) pattern. There were too many caveats - too many special cases that the user had to handle, both from the authoring end and the consuming end.

The new, new DF is more straightforward. When I heard about it, I was glad. Glad as a user - as a tester, when I hear "redesign," it sounds an awful lot like "here's a bunch of extra testing work, and some of those cool tests you wrote can go in the wastebasket." Even though the redesign impacted my already heavy workload (my dentist: "Do you know you're grinding your teeth at night?"), I was happy to see we were doing what I felt was right.

The executive overview is this : we implement the Dispose(bool) pattern for you, and hook it up in the best way possible to your base. (If you have one.)

Here, I'm only going to delve into the first part; that "best way possible" stuff requires a complex decision tree that I don't have the energy to iterate over right now.

We still have the same general design: ~T is basically your destructor, which we implement with Dispose(void), and !T is basically your finalizer, implemented through Finalize(void). What we've added to the mix is Dispose(bool).

Here's how I would write what the compiler does for you:

Dispose(bool disposing) {
if(disposing) {
GC::SuppressFinalize(this);
//call your ~T code
} else { /* call your !T code */ }

//contingent upon what your base class looks like we may call
__super::Dispose(disposing);
}

The major difference between our Dispose(bool) and the one suggested by the CLR Dispose pattern is the else. This is mostly because the user writes Dispose(bool) in the CLR version - in the C++ version, the compiler authors it, so it is left up to the user how to handle it. (The best thing to do, usually, is to invoke the finalizer from the destructor, unless you *really* know what you're doing.)

Typically, you release Disposable managed items from your destructor and unmanaged items for your finalizer. I was confused by this at first. For some reason, I thought it would be the reverse. But eventually, I understood why.

An object allocated on the GC heap has a few potential invocation states for cleanup: user-invoked, intentional finalization, and unintentional finalization. Intentional finalization is easiest, so we'll tackle it first. Sometimes, the cleanup of an object isn't a priority. If you're just dealing with managed heap space, for example, the GC is great at that. It prioritizes, finds the right times to go about cleaning up, and is quite efficient at it.

As to user-invoked cleanup; when dealing with a scarce or untracked resource (e.g. hunks of unmanaged heap space, or database connections), you typically want to free up those resources as soon as you know you're done with them. This is why you want objects with Disposers: it's good engineering practice to let go of scarce resources ASAP. That's why you want to provide them to your users. However, not all of the people using your type may get this (or care) and - this is important - there's no way in the CLR to get them to care. You can't force it. Heck, you yourself are bound to forget now and then.

So we need to handle unintentional finalization, to make sure we don't leak. In your Finalizer, you clean up those resources that wouldn't be cleaned up on their own. And those are the only resources you should clean up in a Finalizer. You should read that again, it's an important distinction in my mind.

Finalization is NOT deterministic. When your finalizer is invoked by the CLR, you can't be certain that any finalizable objects you have references to have not already been finalized. Trying to Dispose something that's already been finalized is dangerous.

With all these strong admonitions and specifics, a good example is due - look for that in an upcoming post.

Comments

  • Anonymous
    June 09, 2005
    The comment has been removed
  • Anonymous
    June 10, 2005
    Interesting Finds
  • Anonymous
    June 15, 2005
    I didn't make this compeltely clear, but I have to echo your sentiments - were I to have designed this, I would have made it work exactly like the C# version.

    As it is, what I typically do (and I believe we recommend) is explicitly call the finalizer from the destructor code. I think the logic is that we leave that decision up to the user - it's a little harder, but it provides more flexibility. (Just like C++ always is. :) )

    I've got about 3/4 of an example ready to post. I just need to stop working on these bugs for an hour to finish it up...
  • Anonymous
    August 08, 2005
    Andy Rich did spot a bug in my code and pointed to his related The long-awaited return of DF blog entry:...
  • Anonymous
    August 08, 2005
    Just like String::Split, I dislike those GetFiles methods that put more load on the managed...
  • Anonymous
    August 08, 2005
    Just like String::Split, I dislike those GetFiles methods that put more load on the managed...
  • Anonymous
    August 08, 2005
    Andy Rich did spot a bug in my code and pointed to his related The long-awaited return of DF blog entry:...
  • Anonymous
    March 13, 2008
    Managed C Destructors and Finalizers
  • Anonymous
    May 06, 2008
    Managed C Destructors and Finalizers