Training
Certifications
Books
Special Offers
Community




 
Microsoft® Visual C#™ .NET Step by Step
Author John Sharp, Jon Jagger
Pages 656
Disk 1 Companion CD(s)
Level All Levels
Published 01/23/2002
ISBN 9780735612891
ISBN-10 0-7356-1289-7
Price(USD) $39.99
To see this book's discounted price, select a reseller below.
 

More Information

About the Book
Table of Contents
Sample Chapter
Index
Related Series
Related Books
About the Author

Support: Book & CD

Rate this book
Barnes Noble Amazon Quantum Books

 


Chapter 13: Using Garbage Collection and Resource Management



Chapter 13 Using Garbage Collection and Resource Management

In this chapter, you will learn how to:

  • Write code that runs when an object is finalized by using a destructor.
  • Manage system resources by using garbage collection.
  • Release a resource at a known point in time by writing your own disposal method.
  • Release a resource at a known point in time in an exception-safe manner by writing a try/finally statement.
  • Release a resource at a known point in time in an exception-safe manner by writing a using statement.

Garbage Collection

One 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 Objects

The distinction between values and objects was covered in detail in Chapter 8. Here's a brief recap:

  • A value is an instance of a value type (an enumuration or a struct) whereas an object is an instance of a reference type (an array, a class, or a delegate):
  • int i = 42;                  // i is a value
    TextBox box = new TextBox(); // box refers to an object

  • The lifetime of a value is tied to the scope in which it is declared whereas the lifetime of an object is not tied to the scope in which it is created, as demonstrated by the following code:
  • {
    int i = 42;
    TextBox message = new TextBox();
    } // i ends its life here,
    // but the object referred to by TextBox lives on

  • Values typically have very short lifetimes whereas objects typically have long lifetimes.

The Life and Times of an Object

You 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 Destructors

The 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
{
public Tally()
{
instanceCount++;
}
~Tally()
{
instanceCount--;
}
public static int InstanceCount()
{
return instanceCount;
}

private static int instanceCount = 0;
}

There are some very important destructor restrictions:

  • You cannot declare a destructor in a struct. A struct is a value type.
  • struct Tally
    {
    ~Tally() { ... } // compile-time error
    }

  • You cannot declare an access modifier (such as public) for a destructor. This is because you never call the destructor yourself (the garbage collector does, as you'll see shortly).
  • public ~Tally() { ... } // compile-time error

  • You never declare a destructor with parameters. Again, this is because you never call the destructor yourself.
  • ~Tally(int parameter) { ... } // compile-time error

    The compiler automatically translates a destructor into an override of the Object.Finalize method. In other words, the compiler translates the following destructor:

    class Tally
    {
    ~Tally() { ... }
    }
    Into this:
    class Tally
    {
    protected override void Finalize()
    {
    try { ... }
    finally { base.Finalize(); }
    }
    }

    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:

  • You'd forget to destroy the object. This would mean that the object's destructor (if it had one) would not be run and its memory would not be deallocated back to the heap. You'd quite easily run out of memory.
  • You'd try and destroy an active object. Remember, objects are held by reference. If a class held a reference to a destroyed object, it would be a dangling reference. The dangling reference would end up referring either to unused memory or to a completely different object. Either way, the outcome of using such a dangling reference would be undefined. All bets would be off.
  • You'd try and destroy the same object more than once.

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:

  • Each object is destroyed regardless of whether your application has explicitly destroyed all the objects it created. For example, when a program ends, all the current objects will be destroyed.
  • Each object is destroyed only once.
  • Each object is destroyed only when no other references refer to the object. This type of object is called an unreachable object.

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:

  1. It builds a graph of all reachable objects. It does this by repeatedly following reference fields inside objects. The garbage collector builds this graph very carefully and makes sure that circular references do not cause an infinite recursion. Any object not in this graph must be unreachable.
  2. It checks to see if any unreachable objects require finalization. In other words, it checks if any of the unreachable objects has a destructor. Any unreachable object that requires finalization is placed in a special queue called the freachable queue (pronounced F-reachable).
  3. The remaining unreachable objects (those that don't require finalization) are deallocated. The garbage collector performs this deallocation by moving the reachable objects down the heap, thus defragmenting the heap and freeing memory at the top of the heap. When the garbage collector moves a reachable object, it also updates any references to the object.
  4. The garbage collector now allows other threads to resume.
  5. The unreachable objects that require finalization (now in the freachable queue) are finalized in a separate thread.

Recommendations

Writing 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 Management

Sometimes 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 Pattern

An 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);
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
reader.Close();

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 Disposal

One 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);
try
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
finally
{
reader.Close();
}

Using a finally block like this works, but it has several drawbacks that make it a less than ideal solution:

  • It quickly gets unwieldy if you have to dispose of more than one resource (you end up with nested try and finally blocks).
  • In some cases, you might have to modify the code (for example, reorder the declaration of the resource reference, remember to initialize the reference to null, and remember to check that the reference isn't null in the finally block).
  • It fails to create an abstraction of the solution. This means the solution is hard to understand and must be repeated every time.
  • The reference to the resource remains in scope after the finally block. This means that you can accidentally use the resource after it has been released.

The using statement is designed to solve all these problems.

The using Statement

The syntax for a using statement is as follows:

using ( type variable = initialization ) embeddedStatement

Such a using statement is precisely equivalent to the following translation:

{
type variable = initialization;
try
{
embeddedStatement
}
finally
{
if (variable != null)
{
((IDisposable)variable).Dispose();
}
}
}

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
{
interface IDisposable
{
void Dispose();
}
}

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:

  • The class containing the disposal method (for example, Close in TextReader) implements the IDisposable interface (as TextReader does).
  • The class implements Dispose (as it must) to call the disposal method (for example, TextReader.Dispose calls TextReader.Close).

For example:

abstract class TextReader : IDisposable
{

public virtual void Dispose()
{
// calls Close
}
public virtual void Close()
{

}
}

Here is the best way to make sure that your code always calls Close on a TextReader:

using (TextReader reader = new StreamReader(filename))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}

This solves all of the problems that existed in the manual try and finally solution. You now have a solution that:

  • Scales well if you need to dispose of multiple resources.
  • Doesn't distort the logic of the program code.
  • Nicely abstracts away the problem, thereby avoiding repetition.
  • Is robust. You can't use the variable (the one declared inside the using statement, in this case, reader) after the using statement has ended because it's not in scope anymore — you'll get a compile-time error.

Adapting to IDisposable

It'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
{
public AutoClosing(TextReader target)
{
this.target = target;
}
public TextReader GetTextReader()
{
return target;
}
public void Dispose()
{
target.Close();
}
private readonly TextReader target;
}

Notice that:

  • AutoClosing does not extend the TextReader class. This avoids the need to replicate all the TextReader constructors inside the AutoClosing struct. (The extension would also be valid if TextReader were a sealed class.)
  • AutoClosing is not designed to be a base class and does not extend a class. You want the memory de-allocated as soon as it goes out of scope. It is best implemented as a struct rather than a class.

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)))
{
TextReader reader = safe.GetTextReader();
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}

Notice how the reader variable is still in the scope of the using statement.

Calling a Disposal Method from a Destructor

One 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
{

~Example()
{
Dispose();
}
public virtual void Dispose()
{
if (!disposed)
{
try {
// release scarce resource here
}
finally {
disposed = true;
GC.SuppressFinalize(this);
}
}
}
public void SomeBehavior()
{
checkIfDisposed();

}

private void checkIfDisposed()
{
if (disposed)
{
throw new ObjectDisposedException("Example");
}
}
private Resource scarce;
private bool disposed = false;
}

Notice that:

  • The class implements IDisposable.
  • The Dispose method is public and can be called at any time.
  • The Dispose method can be safely called multiple times.
  • The destructor calls Dispose.
  • The Dispose method calls GC.SuppressFinalize to stop the garbage collector from needlessly calling the destructor. This is in fact optional; it might turn out that calling GC.SuppressFinalize takes longer than letting the garbage collector call the destructor.
  • All the regular methods of the class check to see if the object has already been disposed. If it has, they throw an exception.

Making Code Exception-Safe

In 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

  1. Start Microsoft Visual Studio .NET.
  2. Open the UsingStatement project in the \Visual C# Step by Step\Chapter 13\UsingStatement folder.
  3. The UsingStatement project opens.

  4. On the Debug menu, click Start Without Debugging.
  5. The Windows form appears.

  6. Click Open File on the form.
  7. The Open dialog box opens.

  8. In the Open dialog box, navigate to the \Visual C# Step by Step\Chapter 13\UsingStatement folder and select the Form1.cs source file. This is the source file for the application itself.
  9. Click Open.
  10. The contents of the file are loaded into the Windows form.

    Click to view graphic
    Click to view graphic

  11. Close the form.
  12. You return to Visual Studio .NET.

  13. Open the Form1.cs source file in the Code pane, and then locate the openFileDialog_FileOk method.
  14. This method should look exactly like this:

    private void openFileDialog_FileOk(object sender, 
    System.ComponentModel.CancelEventArgs e)
    {
    string fullPathname = openFileDialog.FileName;
        FileInfo src = new FileInfo(fullPathname);
        filename.Text = src.Name;
        source.Text = "";
    TextReader reader = new StreamReader(fullPathname);
    string line;
    while ((line = reader.ReadLine()) != null)
    {
    source.Text += line + "\n";
    }
    reader.Close();
    }

    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.

  15. Rewrite the openFileDialog_FileOk method exactly as follows:
  16. private void openFileDialog_FileOk(object sender, 
    System.ComponentModel.CancelEventArgs e)
    {
    string fullPathname = openFileDialog.FileName;
        FileInfo src = new FileInfo(fullPathname);
        filename.Text = src.Name;
        source.Text = "";
    TextReader reader = new StreamReader(fullPathname);
    try
    {
    string line;
    while ((line = reader.ReadLine()) != null)
    {
    source.Text += line + "\n";
    }
    }
    finally
    {
    reader.Close();
    }
    }

  17. Rebuild and re-run the application to verify that it still works.
  18. Rewrite the openFileDialog_FileOk method exactly as follows:
  19. private void openFileDialog_FileOk(object sender, 
    System.ComponentModel.CancelEventArgs e)
    {
    string fullPathname = openFileDialog.FileName;
        FileInfo src = new FileInfo(fullPathname);
        filename.Text = src.Name;
        source.Text = "";
    using (TextReader reader = new StreamReader(fullPathname))
    {
    string line;
    while ((line = reader.ReadLine()) != null)
    {
    source.Text += line + "\n";
    }
    }
    }

  20. Rebuild and re-run the application to verify that it still works.
  21. Confirm that the reader variable is in the scope of the using statement.
  22. To do this, try making a call to reader.Close after the using statement, like this:

    private void openFileDialog_FileOk(object sender, 
    System.ComponentModel.CancelEventArgs e)
    {
    string fullPathname = openFileDialog.FileName;
        FileInfo src = new FileInfo(fullPathname);
        filename.Text = src.Name;
        source.Text = "";
    using (TextReader reader = new StreamReader(fullPathname))
    {
    string line;
    while ((line = reader.ReadLine()) != null)
    {
    source.Text += line + "\n";
    }
    }
    reader.Close();
    }

    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

  • Keep Visual Studio .NET running and turn to Chapter 14.

If you want to exit Visual Studio .NET now

  • On the File menu, click Exit.
  • If you see a Save dialog box, click Yes.

Chapter 13 Quick Reference

ToDo this
Write a destructorWrite a method whose name is the same as the name of the class and is prefixed with a tilde (~). The method must not have an access modifier (such as public) and cannot have any parameters. For example:
class Example 
{
~Example()
{

}
}
Call a destructorForget it. You can't call a destructor. Only the garbage collector can.
Force garbage collectionCall System.GC.Collect();
Release a resource at a known point in timeWrite a disposal method (a method that disposes of a resource) and call it explicitly from the program. For example:
class TextReader 
{

public virtual void Close()
{

}
}
class Example
{
void Use()
{
TextReader reader = ...
// use reader
reader.Close();
}
}
Release a resource at a known point in time in an exception-safe mannerRelease the resource with a using statement. For example:
class TextReader : IDisposable 
{

public virtual void Dispose()
{
// calls Close
}
public virtual void Close()
{

}
}
class Example
{
void Use()
{
using (TextReader reader = ...)
{
// use reader
}
}
}



Last Updated: January 10, 2002
Top of Page