|
Chapter 13: Using Garbage Collection and Resource Management
Chapter 13 Using Garbage Collection and Resource ManagementIn this chapter, you will learn how to:
Garbage CollectionOne of the great strengths of the Microsoft Visual C# language is that it makes a fundamental distinction between values and objects. Values and objects are different.
Comparing Values and ObjectsThe distinction between values and objects was covered in detail in Chapter 8. Here's a brief recap:
int i = 42; // i is a value
{
The Life and Times of an ObjectYou create an object like this:
new TextBox(); From your point of view, this is an atomic operation, but underneath, object creation is really a two-phase process. First you have to allocate some raw memory from the heap. You do this using the new keyword. You have no control over this phase of an object's creation. Second you have to convert the raw memory into an object; you have to initialize the object. You do this by using a constructor. In contrast to allocation, you do have control over this phase of an object's creation.
It's common to create an object when initializing a reference variable. For example:
TextBox message = new TextBox(); You can then use the object that the reference refers to by using the dot operator. For example:
message.Text = "People of Earth, your attention please"; Object death is also a two-phase process. The two phases exactly mirror the two phases of creation. First you have to convert the object back into raw memory. You do this by writing a destructor. The destructor is the opposite of the constructor. Second the raw memory has to be given back to the heap; the binary bits that the object lived in have to be deallocated. Once again you have no control over this phase. The process of returning memory back to the heap is known as garbage collection.
Writing DestructorsThe syntax for writing a destructor is a tilde (~) followed by the name of the class. For example, here's a simple class that counts the number of live instances by incrementing a static count in the constructor and decrementing the static count in the destructor:
class Tally There are some very important destructor restrictions:
struct Tally
public ~Tally() { ... } // compile-time error
~Tally(int parameter) { ... } // compile-time errorThe compiler automatically translates a destructor into an override of the Object.Finalize method. In other words, the compiler translates the following destructor:
class Tally The compiler-generated Finalize method contains the destructor body inside a try block, followed by a finally block that calls the base class Finalize. This ensures that a destructor always calls its base class destructor (just like in C++).
It's important to realize that only the compiler can make this translation. You can't override Finalize yourself and you can't call Finalize yourself. In other words, Finalize really is just another name for the destructor.
Why Use the Garbage Collector?The fundamental thing to remember about destructors is that you can never call them. The reason for this is that, in C#, you can never destroy an object yourself. There just isn't any syntax to do it. There are good reasons why the designers of C# decided to forbid you from explicitly writing code to destroy objects. If it was your responsibility to destroy objects, sooner or later:
These problems are unacceptable in a language like C#, which places robustness and security high on its list of design goals. Instead, the garbage collector is responsible for destroying objects for you. Only the garbage collector can destroy objects. The garbage collector guarantees that:
These guarantees are tremendously useful and free you, the programmer, from tedious housekeeping chores that are easy to get wrong. They allow you to concentrate on the logic of the program itself and be more productive. However, as with all design trade-offs, garbage collection comes at a price. You don't know the order in which objects will be destroyed. Objects are not necessarily destroyed in the reverse order that they are created in (as they are in C++). Neither do you know when the garbage collector will decide to destroy objects. An object is not destroyed at the moment that it becomes unreachable. Because destroying objects can be a time-consuming operation, the garbage collector destroys objects only when it is necessary (when the heap memory is exhausted) or when you explicitly ask it to (by calling the System.GC.Collect method). Clearly, this makes C# unsuitable for some time-critical applications.
How Does the Garbage Collector Run?The garbage collector runs in its own thread and only runs when other threads are in a safe state (for example, when they are suspended). This is important because, as the garbage collector runs, it needs to move objects and update object references. The steps that the garbage collector takes when it runs are as follows:
RecommendationsWriting classes that contain destructors adds complexity to your code and to the garbage collection process and makes your program run more slowly. If your program does not contain any destructors, the garbage collector does not need to perform Steps 3 and 5 (in the previous section). Clearly, not doing something is faster than doing it. (There's no code faster than no code.) The first recommendation is, therefore, to try to avoid using destructors except when you really need them (for example, consider a using statement instead; these are covered in the following section).If you write a destructor, you need to write it very carefully. In particular, you need to be aware that, if your destructor calls other objects, those other objects might have already had their destructor called by the garbage collector (remember, the order of finalization is not guaranteed). The second recommendation is therefore to ensure that each destructor reclaims one resource and nothing else. This can require splitting a class into two classes (one of which is the class dedicated to managing the resource).
Resource ManagementSometimes it's inadvisable to release a resource in a destructor; some resources are just too valuable and too scarce to lie around unreleased for arbitrary lengths of time. (Remember, you don't know when the garbage collector will call an object's destructor.) Scarce resources need to be released, and they need to be released as soon as possible. In these situations, your only option is to release the resource yourself. A disposal method is a method that disposes of a resource. If a class has a disposal method, you can call it explicitly and thus control when the resource is released.
The Disposal Method PatternAn example of a class that contains a disposal method is the TextReader class from the System.IO namespace. TextReader contains a virtual method called Close. The StreamReader (which reads characters from a stream) and StringReader (which reads characters from a string) classes both derive from TextReader and both override the Close method. Here's an example that reads lines from a file using the StreamReader class:
TextReader reader = new StreamReader(filename); It's important to call Close when you have finished with reader to release the file handle (and encoding) resources. However, there is a problem with this example; it's not exception-safe. If the call to ReadLine (or WriteLine) throws an exception, the call to Close will not happen; it will be bypassed.
Exception-Safe DisposalOne way to ensure that a disposal method is always called, regardless of whether there is an exception, is to call the disposal method inside a finally block. Here's the previous example using this technique:
TextReader reader = new StreamReader(filename); Using a finally block like this works, but it has several drawbacks that make it a less than ideal solution:
The using statement is designed to solve all these problems.
The using StatementThe syntax for a using statement is as follows:
using ( type variable = initialization ) embeddedStatement Such a using statement is precisely equivalent to the following translation:
{
This equivalence means that the variable you declare in a using statement must be of a type that implements the IDisposable interface. The IDisposable interface lives in the System namespace and contains just one method called Dispose:
namespace System You can use a using statement as a clean, exception-safe, robust way to ensure that a resource is always automatically released. You just need to make sure that:
For example:
abstract class TextReader : IDisposable Here is the best way to make sure that your code always calls Close on a TextReader:
using (TextReader reader = new StreamReader(filename)) This solves all of the problems that existed in the manual try and finally solution. You now have a solution that:
Adapting to IDisposableIt's instructive to consider how you could have used a using statement to ensure that your code always called TextReader.Close if the TextReader class didn't implement the IDisposable interface. You could do it like this:
struct AutoClosing : IDisposable Notice that:
You can rewrite the GetTextReader accessor method as a read-only property. Properties are covered in Chapter 14. You could then use this struct as follows:
using (AutoClosing safe = new AutoClosing(new StreamReader(filename))) Notice how the reader variable is still in the scope of the using statement.
Calling a Disposal Method from a DestructorOne of the drawbacks of the disposal method pattern is that it relies on you (the programmer) to call the disposal method, and programmers tend to occasionally forget to do things like this. The trade-off in deciding whether to use destructors or disposal methods is this: A call to a destructor will happen, you just don't know when, whereas you know exactly when a call to a disposal method happens, you just can't be sure that it will actually happen because you might forget to call it. However, it is possible to ensure that a disposal method is always called. The solution is to call the disposal method from a destructor. This acts as a useful "backup." You might forget to call the disposal method, but at least you can be sure that it will be called, even if it's only when the program shuts down. An example of how to do this is:
class Example : IDisposable Notice that:
Making Code Exception-SafeIn the following exercise, you will rewrite a small piece of code. The code opens a text file, reads its contents one line at a time, writes these lines to a rich text box on a Windows form, and then closes the text file. The problem is that the code is not exception-safe. If an exception arises as the file is read or as the lines are written to the rich text box, the call to close the text file will be bypassed. You will rewrite the code to use a using statement instead, thus ensuring that the code is exception-safe.
Write a using statement
The UsingStatement project opens.
The Windows form appears.
The Open dialog box opens.
The contents of the file are loaded into the Windows form.
You return to Visual Studio .NET.
This method should look exactly like this:
private void openFileDialog_FileOk(object sender, The filename, openFileDialog, and the source identifiers are three private fields of the Form1 class. The problem with this code is that the final statement, the call to reader.Close, is not guaranteed to happen. If you are not used to handling exceptions, it can take a while to get used to this and even longer to spot it as a potential problem.
private void openFileDialog_FileOk(object sender,
private void openFileDialog_FileOk(object sender,
To do this, try making a call to reader.Close after the using statement, like this:
private void openFileDialog_FileOk(object sender, You will find that this does not compile. You will see an error message such as:
The type or namespace name 'reader' could not be found (are you missing a using directive or an assembly reference?) This error message appears because in the previous example, use of the variable reader is only permitted within the body of the using statement.
If you want to continue to the next chapter
If you want to exit Visual Studio .NET now
If you see a Save dialog box, click Yes.
Chapter 13 Quick Reference
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||