What You’ll Learn in This Hour
• Understanding the IDisposable
interface
• Declaring and using finalizers
In Hour 1, “The .NET Framework and C#,” you learned that one of the benefits provided by the .NET Framework is automatic memory management. This helps you create applications that are more stable by preventing many common programming errors and enables you to focus your time on the business logic your application requires. Even with automatic memory management, it is still important to understand how the garbage collector interacts with your program and the types you create.
You briefly learned about value and reference types in Hour 3, “Understanding C# Types.” The simple definition for a value type presented was that a value type is completely self-contained and copied “by value.” A reference type, on the other hand, contains a reference to the actual data. Because variables of a value type directly contain their data, it is not possible for operations on one to affect the other. It is possible for two variables of a reference type to refer to the same object, allowing operations on one variable to affect the other.
In this hour, you learn some of the fundamentals of how memory is organized in the common language runtime (CLR), how the garbage collector works, and how the .NET Framework provides mechanisms for deterministic finalization.
To better understand how types work, you need to have a basic understanding of the two mechanisms the CLR uses to store data in memory: the stack and the heap. Figure 23.1 shows a conceptual view of stack and heap memory.
The simplest way to think of stack memory is that it is organized like a stack of plates in a restaurant. The last plate placed on the stack is the first one removed. This is also known as a last-in first-out (LIFO) queue. Stack memory is used by the .NET Framework to store local variables (except for the local variables used in iterator blocks or those captured by a lambda or anonymous method) and temporary values. You can think of stack memory as providing cheap garbage collection because the lifetime of variables placed on the stack is well known.
Heap memory, however, is more like a wall of empty slots. Each slot can indicate if it is already full or if it is no longer used and ready to be recycled. When a slot has been filled, its contents can be replaced only with something that is the same type as it originally contained. A slot can be reused (and have its type changed) only when it has been recycled.
A type defined by a class is a reference type. When you create an instance of a reference type using the new
operator, the runtime actually performs several actions on your behalf. The two primary actions that occur follow:
1. Memory is allocated and initialized to its default value.
2. The constructor for the class is executed to initialize the allocated memory.
At this point, your object is initialized and ready to be used. Because the runtime allocated the memory on your behalf, it is reasonable to expect that it also deallocates that memory at some undetermined future point in time. The responsibility for deallocating memory falls to the garbage collector.
The simple view of how the garbage collector works is that when it runs, the following actions occur:
1. Every instance of a reference type is assumed to be “garbage.”
2. The garbage collector determines which instances are still accessible. These types are considered “live” and are marked to indicate they are still reachable.
3. The memory used by the unmarked reference types is deallocated.
4. The managed heap is then compacted (by moving memory) to reduce fragmentation and consolidate the used memory space. As the “live” objects are moved, the mark is cleared in anticipation of the next garbage collection cycle.
Every object instance has a lifetime, which is the length of execution time an object is pointed to by a valid reference. As long as an object has at least one valid reference, it cannot be destroyed. By creating more references to an object, you can potentially extend its lifetime.
Because memory deallocation occurs at an unspecified point in time, the .NET Framework provides a mechanism, through the IDisposable
interface, that enables you to provide explicit cleanup behavior before the memory is reclaimed. This interface provides a way to perform deterministic resource deallocation. It also provides a consistent pattern that types needing to control resource allocation can utilize.
Listing 23.1 shows the definition of the IDisposable
interface, which provides a single public method named Dispose
.
public interface IDisposable
{
void Dispose();
}
Types that implement this interface, commonly called disposable types, give the code using that type a way to indicate that the type is eligible for garbage collection. If the type has any unmanaged resources it maintains, calling Dispose
should immediately release those unmanaged resources; however, calling the Dispose
method does not actually cause the instance to be garbage collected.
Even if a type implements IDisposable
, there is no way for the .NET Framework to ensure that you have actually called the Dispose
method. This problem is compounded when you consider the implications of an exception occurring. If an exception occurs, there is a possibility that, depending on how the calling code is written, the disposable type will never get the opportunity to actually perform its cleanup.
From Hour 11, “Handling Errors Using Exceptions,” you learned that you could place the calling code in a try-finally block where the call to the Dispose
method is placed in the finally
handler. Although this is the correct implementation, it can be easy to get incorrect (particularly when you need to nest multiple protected regions) and can lead to code that is hard to read.
To alleviate these problems, C# provides the using
statement. You saw examples of the using
statement in Hour 14, “Using Files and Streams.” The using
statement provides a clean and simple way to indicate the intended lifetime of an object and ensures the Dispose
method is called when that lifetime ends.
The syntax for the using
statement is as follows:
using (resource-acquisition) embedded-statement
When the compiler encounters a using
statement, it actually generates code similar to what is shown in Listing 23.2.
{
DisposableObject x = new DisposableObject();
try
{
// use the object.
}
finally
{
if (x != null)
{
((IDisposable)x).Dispose();
}
}
}
There are a few subtle, but important, things to note in this expansion. The first is the outermost enclosing braces. This defines a scope that contains the expansion but, more specifically, helps ensure that the variable defined in the using
statement is accessible only from within that defined scope. The second is that the local variable declared for the resource acquisition is read-only and that it is a compile-time error to modify this variable from within the using
block statement. Finally, because the compiler explicitly casts the object to the IDisposable
interface (to ensure that it is calling the correct Dispose
method), the using
statement can be used only with types that implement the IDisposable
interface.
To implement the dispose pattern, you provide an implementation of the IDisposable
interface. However, implementing the IDisposable
interface is actually only part of the pattern. The complete dispose pattern, in the context of the Contact
class, is shown in Listing 23.3.
Tip: When Should You Implement the Dispose Pattern?
Typically, you should only implement the dispose pattern if
• You control unmanaged resources directly.
• You control other disposable resources directly.
public class Contact : IDisposable
{
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
// Release only managed resources here.
}
// Release only unmanaged resource here.
this.disposed = true;
}
}
}
The public Dispose
method should always be implemented as shown. The order of the two calls is important and shouldn’t be changed. This order ensures that GC.SuppressFinalize
is called only if the Dispose
operation completes successfully. When Dispose
calls Dispose(true)
, the call might fail, but later on the garbage collector can call Dispose(false)
. In reality, these are two different calls that can execute different portions of the code, so even though Dispose(true)
fails, Dispose(false)
might not.
All your resource cleanup should be contained in the Dispose(bool disposing)
method. If necessary, you should protect the cleanup by testing the disposing
parameter. This should happen for both managed and unmanaged resources. The Dispose(bool disposing)
runs in two distinct scenarios:
• If disposing
is true
, the method has been called directly or indirectly by a user’s code. Managed and unmanaged resources can be disposed.
• If disposing
is false
, the method has been called by the runtime from inside the finalizer, and you should not reference other objects. Only unmanaged resources can be disposed.
The benefit of using the dispose pattern is that it provides a way for users of the type to explicitly indicate that they are done using that instance and that its resources should be released.
The dispose pattern is not the only way the .NET Framework enables you to perform explicit resource cleanup before the object’s memory is reclaimed. The other way is using a finalizer, which is a special method called automatically after an object becomes inaccessible. It is important to realize that the finalizer method will not be called until the garbage collector realizes the object is unreachable.
A finalizer method looks like the default constructor for a class except the method name is prefixed with the tilde (~
) character. Listing 23.4 shows how you would implement a finalizer for the Contact
class.
public class Contact : IDisposable
{
private bool disposed;
public Contact()
{
}
~Contact()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
// Release only managed resources here.
}
// Release only unmanaged resource here.
this.disposed = true;
}
}
}
There are several important rules about declaring and using finalizers:
• The exact time and order of when a finalizer executes is undefined.
• The finalizer method runs on the GC thread, not the application’s main thread.
• Finalizers apply only to reference types.
• You cannot specify an access modifier for a finalizer method.
• You cannot provide parameters to a finalizer method.
• You cannot overload or override a finalizer method.
• You cannot call a finalizer method directly in your own code; only the garbage collector can call a finalizer method.
Although finalizers are typically used when you want to provide some assurance that resources will be released even if the calling code does not call Dispose
, it is not necessary to provide a finalizer just because you implement the dispose pattern. However, if you do implement a finalizer, be sure to also implement the dispose pattern.
Finalizers are actually difficult to write correctly because many of the normal assumptions you can make about the state of the runtime environment are not true during object finalization.
Making your class finalizable means that it cannot be garbage collected until after the finalizer has run, which means your class survives at least one extra garbage collection cycle. In addition, if not written carefully, finalizer methods have the possibility of creating a new reference to the instance being finalized, in which case the instance is alive again.
The .NET Framework does an excellent job at handling memory allocation and deallocation on your behalf, enabling you to focus on the business logic required by your application. It is, however, beneficial to have at least a basic understanding of how the .NET Framework manages memory and, more important, what mechanisms it provides to enable you to influence that management.
In this hour, you learned about the different ways the .NET Framework manages memory, through the managed heap and the stack. You saw how the using
statement enables you to ensure a disposable object has its Dispose
method called and how to implement the IDisposable
interface in your own classes through the dispose pattern.
Q. What are the two mechanisms the CLR uses to store data in memory?
A. The CLR uses stack and heap memory to store data.
Q. What is object lifetime?
A. Every object instance has a lifetime, which is the length of execution time an object is pointed to by a valid reference.
Q. What is one purpose of the IDisposable
interface?
A. The IDisposable
interface is intended to provide a way to perform deterministic resource deallocation. It also provides a consistent pattern that types which need to control resource allocation can utilize.
Q. Can the using
statement be used with types that do not implement IDisposable
?
A. No, the IDisposable
interface is required because the compiler-generated expansion of the using
statement casts the object to the interface to ensure that the correct implementation of the Dispose
method is called.
Q. When should you implement the dispose pattern?
A. Typically you should only implement the dispose pattern if
• You control unmanaged resources directly.
• You control other disposable resources directly.
Q. If you implement IDisposable
, should you also implement a finalizer?
A. No, just because you implement the IDisposable
interface does not mean that you should also implement a finalizer. However, if you do implement a finalizer, you should also implement the IDisposable
interface.
1. What are the two primary actions that occur on your behalf when using the new
operator?
2. Does calling Dispose
immediately cause the disposable object to be garbage collected?
3. What is the implication on garbage collection of implementing a finalizer?
1. The two primary actions that occur are
a. Memory is allocated.
b. The constructor for the class is executed to initialize the allocated memory.
2. No, calling Dispose
does not cause the object to be immediately garbage collected. It can, however, cause the object to immediately release any unmanaged resources it maintains.
3. Making your class finalizable means that it cannot be garbage collected until after the finalizer has run, which means your class survives at least one extra garbage collection cycle. In addition, if not written carefully, finalizer methods have the possibility of creating a new reference to the instance being finalized, in which case the instance is alive again.
There are no exercises for this hour.