Garbage collection is a key responsibility of the runtime. It is important to note, however, that the garbage collection relates to memory utilization. It is not about the cleaning up of file handles, database connection strings, ports, or other limited resources. FinalizersFinalizers allow programmers to write code that will clean up a class's resources. However, unlike constructors that are called explicitly using the new operator, finalizers cannot be called explicitly from within the code. There is no new equivalent such as a delete operator. Rather, the garbage collector is responsible for calling a finalizer on an object instance. Therefore, developers cannot determine at compile time exactly when the finalizer will execute. All they know is that the finalizer will run sometime between when an object was last used and before the application shuts down. (Finalizers will execute barring process termination prior to the natural closure of the process. For instance, events such as the computer being turned off or a forced termination of the process will prevent the finalizer from running.) The finalizer declaration is identical to the destructor syntax of C#'s predecessornamely, C++. As shown in Listing 9.20, the finalizer declaration is prefixed with a tilde before the name of the class. Listing 9.20. Defining a Finalizer
Finalizers do not allow any parameters to be passed, and as a result, finalizers cannot be overloaded. Furthermore, finalizers cannot be called explicitly. Only the garbage collector can invoke a finalizer. Therefore, access modifiers on finalizers are meaningless, and as such, they are not supported. Finalizers in base classes will be invoked automatically as part of an object finalization call. Because the garbage collector handles all memory management, finalizers are not responsible for de-allocating memory. Rather, they are responsible for freeing up resources such as database connections and file handles, resources that require an explicit activity that the garbage collector doesn't know about.
Deterministic Finalization with the using StatementThe problem with finalizers on their own is that they don't support deterministic finalization (the ability to know when a finalizer will run). Rather, finalizers serve the important role of a backup mechanism for cleaning up resources if a developer using a class neglects to call the requisite cleanup code explicitly. For example, consider the TemporaryFileStream that not only includes a finalizer but also a Close() method. The class uses a file resource that could potentially consume a significant amount of disk space. The developer using TemporaryFileStream can explicitly call Close() in order to restore the disk space. Providing a method for deterministic finalization is important because it eliminates a dependency on the indeterminate timing behavior of the finalizer. Even if the developer fails to call Close() explicitly, the finalizer will take care of the call. The finalizer will run later than if it was called explicitly, but it will be called. Because of the importance of deterministic finalization, the base class library includes a specific interface for the pattern and C# integrates the pattern into the language. The IDisposable interface defines the details of the pattern with a single method called Dispose(), which developers call on a resource class to "dispose" of the consumed resources. Listing 9.21 demonstrates the IDisposable interface and some code for calling it. Listing 9.21. Resource Cleanup with IDisposable
The steps for both implementing and calling the IDisposable interface are relatively simple. However, there are a couple of points you should not forget. First, there is a chance that an exception will occur between the time TemporaryFileStream is instantiated and Dispose() is called. If this happens, Dispose() will not be invoked and the resource cleanup will have to rely on the finalizer. To avoid this, callers need to implement a try/finallyblock. Instead of coding such a block explicitly, C# provides a using statement expressly for the purpose. The resulting code appears in Listing 9.22. Listing 9.22. Invoking the using Statement
The resulting CIL code is identical to the code that would be created if there was an explicit try/finally block, where fileStream.Dispose() is called in the finally block. The using statement, however, provides a syntax shortcut for the try/finally block. Within a using statement, you can instantiate more than one variable by separating each variable with a comma. The key is that all variables are of the same type and that they implement IDisposable. To enforce the use of the same type, the data type is specified only once rather than before each variable declaration.
Garbage Collection and FinalizationThe IDisposable pattern contains one additional important call. Back in Listing 9.21, the Close() method included a call to System.GC.SuppressFinalize() (captured again in Listing 9.23). Its purpose was to remove the TemporaryFileStream class instance from the finalization (f-reachable) queue. Listing 9.23. Suppressing Finalization
The f-reachable queue is a list of all objects that are ready for garbage collection and that also have finalization implementations. The runtime cannot garbage collect objects with finalizers until after their finalization methods have been called. However, garbage collection itself does not call the finalization method. Rather, references to finalization objects are added to the f-reachable queue, thereby ironically delaying garbage collection. This is because the f-reachable queue is a list of "references," and as such, the objects are not garbage until after their finalization methods are called and the object references are removed from the f-reachable queue.
Resource Utilization and Finalization GuidelinesWhen defining classes that manage resources, you should consider the following.
|