A quick refresher on finalization: When an object with a finalizer is created, a reference to it is placed in the finalization queue. When the object is no longer referenced by the application, the GC moves it to the f-reachable queue. This wakes up the finalizer thread, which in turn removes the object from the queue and runs its finalizer. At the next GC, the object's memory is reclaimed.
With that in mind, consider a graph of objects with finalizers, such as a StreamWriter and a FileStream. The FileStream is a thin wrapper around a Win32 file handle and the related APIs. The StreamWriter is a buffering convenience wrapper around a stream. Both require finalization to work properly - the StreamWriter needs a finalizer to flush its internal buffer and close the underlying stream, and the FileStream needs a finalizer to close the Win32 file handle. Both should also implement IDisposable or provide other means for deterministic finalization (e.g. a Close method).
So what happens if the user doesn't use deterministic finalization and relies on a finalizer instead?
FileStream fs = new FileStream("1.txt", FileAccess.ReadWrite);
StreamWriter sw = new StreamWriter(fs);
//No sw.Close() here
Scenario #1: If the StreamWriter's finalizer is called first, it will flush the buffer and close the FileStream. Since the stream was deterministically closed, it will not be finalized.
Scenario #2: If the FileStream's finalizer is called first, it will close the Win32 file handle. When the StreamWriter's finalizer is called, it will attempt to flush its internal buffer into a closed stream!
Since finalization order is undefined (and there's no way for it to be defined, because it depends on the GC's traversal order which is also undefined), we can never tell if we're going to land in scenario #1 (yay) or scenario #2 (ouch). Therefore, StreamWriter does not have a finalizer.
Bonus question: What happens if you forget to deterministically close a StreamWriter? (You can try it at home. Nothing horrible will happen, but you will lose the buffered data that wasn't yet flushed to the underlying stream.)
This also brings up the subject of resurrection, a little-known "feature" that boils down to making an object accessible from within a finalizer. For example, if we were to implement object pooling, we would want to ensure that even if the user didn't explicitly return the instance to the pool, it will still be returned to the pool by the finalizer:
//Implementation omitted for clarity
The first problem with this approach is that the finalizer for this specific instance won't ever be called again. We get one shot at the finalization queue - no one is going to add our instance back automatically, so we have to take care of it ourselves:
Another problem we are certainly going to have revolves around other finalizable objects we might be referencing. Since finalization order is undefined, it is entirely possible that the finalizers for our contained (referenced) objects have already run, rendering their state inconsistent. Any attempt to use them will yield undefined results - few objects are willing to work properly after their finalizer has already run, or are even aware of the possibility this might happen!
Effectively, the only "safe" way of resurrecting such referenced objects that are outside our control is discarding and reinitializing them from scratch.