Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]-1

.pdf
Скачиваний:
23
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

262CHAPTER 8 UNDERSTANDING OBJECT LIFETIME

...

} // end of method Program::Main

Note If you attempt to “use” an object that does not implement IDisposable, you will receive a compiler error.

While this syntax does remove the need to manually wrap disposable objects within try/finally logic, the C# using keyword unfortunately now has a double meaning (specifying namespaces and invoking a Dispose() method). Nevertheless, when you are working with .NET types that support the IDisposable interface, this syntactical construct will ensure that the object “being used” will automatically have its Dispose() method called once the using block has exited. Also, be aware that it is possible to declare multiple objects of the same type within a using

scope. As you would expect, the compiler will inject code to call Dispose() on each declared object:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Dispose *****\n");

// Use a comma-delimited list to declare multiple objects to dispose. using(MyResourceWrapper rw = new MyResourceWrapper(),

rw2 = new MyResourceWrapper())

{

// Use rw and rw2 objects.

}

}

Source Code The SimpleDispose project is included under the Chapter 8 subdirectory.

Building Finalizable and Disposable Types

At this point, we have seen two different approaches to construct a class that cleans up internal unmanaged resources. On the one hand, we could override System.Object.Finalize(). Using this technique, we have the peace of mind that comes with knowing the object cleans itself up when garbage collected (whenever that may be) without the need for user interaction. On the other hand, we could implement IDisposable to provide a way for the object user to clean up the object as soon as it is finished. However, if the caller forgets to call Dispose(), the unmanaged resources may be held in memory indefinitely.

As you might suspect, it is possible to blend both techniques into a single class definition. By doing so, you gain the best of both models. If the object user does remember to call Dispose(), you can inform the garbage collector to bypass the finalization process by calling GC.SuppressFinalize(). If the object user forgets to call Dispose(), the object will eventually be finalized and have a chance to free up the internal resources. The good news is that the object’s internal unmanaged resources will be freed one way or another.

Here is the next iteration of MyResourceWrapper, which is now finalizable and disposable, defined in a C# Console Application named FinalizableDisposableClass:

CHAPTER 8 UNDERSTANDING OBJECT LIFETIME

263

// A sophisticated resource wrapper.

public class MyResourceWrapper : IDisposable

{

//The garbage collector will call this method if the

//object user forgets to call Dispose().

~ MyResourceWrapper()

{

//Clean up any internal unmanaged resources.

//Do **not** call Dispose() on any managed objects.

}

//The object user will call this method to clean up

//resources ASAP.

public void Dispose()

{

//Clean up unmanaged resources here.

//Call Dispose() on other contained disposable objects.

//No need to finalize if user called Dispose(),

//so suppress finalization.

GC.SuppressFinalize(this);

}

}

Notice that this Dispose() method has been updated to call GC.SuppressFinalize(), which informs the CLR that it is no longer necessary to call the destructor when this object is garbage collected, given that the unmanaged resources have already been freed via the Dispose() logic.

A Formalized Disposal Pattern

The current implementation of MyResourceWrapper does work fairly well; however, we are left with a few minor drawbacks. First, the Finalize() and Dispose() methods each have to clean up the same unmanaged resources. This could result in duplicate code, which can easily become a nightmare to maintain. Ideally, you would define a private helper function that is called by either method.

Next, you would like to make sure that the Finalize() method does not attempt to dispose of any managed objects, while the Dispose() method should do so. Finally, you would also like to make sure that the object user can safely call Dispose() multiple times without error. Currently, our Dispose() method has no such safeguards.

To address these design issues, Microsoft has defined a formal, prim-and-proper disposal pattern that strikes a balance between robustness, maintainability, and performance. Here is the final (and annotated) version of MyResourceWrapper, which makes use of this official pattern:

public class MyResourceWrapper : IDisposable

{

//Used to determine if Dispose()

//has already been called. private bool disposed = false;

public void Dispose()

{

//Call our helper method.

//Specifying "true" signifies that

//the object user triggered the cleanup.

CleanUp(true);

264 CHAPTER 8 UNDERSTANDING OBJECT LIFETIME

// Now suppress finalization.

GC.SuppressFinalize(this);

}

private void CleanUp(bool disposing)

{

// Be sure we have not already been disposed! if (!this.disposed)

{

//If disposing equals true, dispose all

//managed resources.

if (disposing)

{

// Dispose managed resources.

}

// Clean up unmanaged resources here.

}

disposed = true;

}

~MyResourceWrapper()

{

//Call our helper method.

//Specifying "false" signifies that

//the GC triggered the cleanup.

CleanUp(false);

}

}

Notice that MyResourceWrapper now defines a private helper method named CleanUp(). When specifying true as an argument, we are signifying that the object user has initiated the cleanup, therefore we should clean up all managed and unmanaged resources. However, when the garbage collector initiates the cleanup, we specify false when calling CleanUp() to ensure that internal disposable objects are not disposed (as we can’t assume they are still in memory!). Last but not least, our bool member variable (disposed) is set to true before exiting CleanUp() to ensure that Dispose() can be called numerous times without error.

To test our final iteration of MyResourceWrapper, add a call to Console.Beep() within the scope of your finalizer:

~MyResourceWrapper()

{

Console.Beep();

//Call our helper method.

//Specifying "false" signifies that

//the GC triggered the cleanup. CleanUp(false);

}

Next, update Main() as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Dispose() / Destructor Combo Platter *****");

// Call Dispose() manually, this will not call the finalizer.

MyResourceWrapper rw = new MyResourceWrapper(); rw.Dispose();

CHAPTER 8 UNDERSTANDING OBJECT LIFETIME

265

//Don't call Dispose(), this will trigger the finalizer

//and cause a beep.

MyResourceWrapper rw2 = new MyResourceWrapper();

}

Notice that we are explicitly calling Dispose() on the rw object, therefore the destructor call is suppressed. However, we have “forgotten” to call Dispose() on the rw2 object, and therefore when the application terminates, we hear a single beep. If you were to comment out the call to Dispose() on the rw object, you would hear two beeps.

Source Code The FinalizableDisposableClass project is included under the Chapter 8 subdirectory.

That wraps up our investigation of how the CLR is managing your objects via garbage collection. While there are additional (fairly esoteric) details regarding the collection process I have not examined here (such as weak references and object resurrection), you are certainly in a perfect position for further exploration on your own terms.

Summary

The point of this chapter was to demystify the garbage collection process. As you have seen, the garbage collector will only run when it is unable to acquire the necessary memory from the managed heap (or when a given AppDomain unloads from memory). When a collection does occur, you can rest assured that Microsoft’s collection algorithm has been optimized by the use of object generations, secondary threads for the purpose of object finalization, and a managed heap dedicated to host large objects.

This chapter also illustrated how to programmatically interact with the garbage collector using the System.GC class type. As mentioned, the only time when you will really need to do so is when you are building finalizable or disposable class types that operate upon unmanaged resources.

Recall that finalizable types are classes that have overridden the virtual System.Object. Finalize() method to clean up unmanaged resources at the time of garbage collection. Disposable objects, on the other hand, are classes (or structures) that implement the IDisposable interface, which should be called by the object user when it is finished using said object. Finally, you learned about an official “disposal” pattern that blends both approaches.

P A R T 3

Advanced C# Programming

Constructs

C H A P T E R 9

Working with Interfaces

This chapter builds on your current understanding of object-oriented development by examining the topic of interface-based programming. Here you learn how to define and implement interfaces, and come to understand the benefits of building types that support “multiple behaviors.” Along the way, a number of related topics are also discussed, such as obtaining interface references, explicit interface implementation, and the construction of interface hierarchies.

The remainder of this chapter is spent examining a number of standard interfaces defined within the .NET base class libraries. As you will see, your custom types are free to implement these predefined interfaces to support a number of advanced behaviors such as object cloning, object enumeration, and object sorting. We wrap up the chapter by examining how interface types can be used to establish a callback mechanism, allowing two objects in memory to communicate in a bidirectional manner.

Understanding Interface Types

To begin this chapter, allow me to provide a formal definition of the interface type. An interface is nothing more than a named set of abstract members. Recall from Chapter 6 that abstract methods are pure protocol in that they do not provide a default implementation. The specific members defined by an interface depend on the exact behavior it is modeling. Yes, it’s true. An interface expresses a behavior that a given class or structure may choose to implement. Furthermore, as you will see in this chapter, a class (or structure) can support as many interfaces as necessary, thereby supporting (in essence) multiple behaviors.

As you might guess, the .NET base class libraries ship with hundreds of predefined interface types that are implemented by various classes and structures. For example, as you will see in Chapter 22, ADO.NET ships with multiple data providers that allow you to communicate with a particular database management system. Thus, unlike COM-based ADO, under ADO.NET we have numerous connection objects we may choose between (SqlConnection, OracleConnection, OdbcConnection, etc.).

Regardless of the fact that each connection object has a unique name, is defined within a different namespace, and (in some cases) is bundled within a different assembly, all connection objects implement a common interface named IDbConnection:

//The IDbConnection interface defines a common

//set of members supported by all connection objects. public interface IDbConnection : IDisposable

{

// Methods

IDbTransaction BeginTransaction();

IDbTransaction BeginTransaction(IsolationLevel il); void ChangeDatabase(string databaseName);

void Close();

269

 

270 CHAPTER 9 WORKING WITH INTERFACES

IDbCommand CreateCommand(); void Open();

// Properties

string ConnectionString { get; set;} int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; }

}

Note By convention, .NET interface types are prefixed with a capital letter “I.” When you are creating your own custom interfaces, it is considered a best practice to do the same.

Don’t concern yourself with the details of what these members actually do at this point. Simply understand that the IDbConnection interface defines a set of members that are common to all ADO.NET connection objects. Given this, you are guaranteed that each and every connection object supports members such as Open(), Close(), CreateCommand(), and so forth. Furthermore, given that interface members are always abstract, each connection object is free to implement these methods in its own unique manner.

Another example: the System.Windows.Forms namespace defines a class named Control, which is a base class to a number of Windows Forms UI widgets (DataGridView, Label, StatusBar, TreeView, etc.). The Control class implements an interface named IDropTarget, which defines basic drag-and- drop functionality:

public interface IDropTarget

{

// Methods

void OnDragDrop(DragEventArgs e); void OnDragEnter(DragEventArgs e); void OnDragLeave(EventArgs e); void OnDragOver(DragEventArgs e);

}

Based on this interface, we can now correctly assume that any class that extends System. Windows.Forms.Control supports four subroutines named OnDragDrop(), OnDragEnter(), OnDragLeave(), and OnDragOver().

As you work through the remainder of this text, you will be exposed to dozens of interfaces that ship with the .NET base class libraries. As you will see, these interfaces can be implemented on your own custom classes and structures to define types that integrate tightly within the framework.

Contrasting Interface Types to Abstract Base Classes

Given your work in Chapter 6, the interface type may seem very similar to an abstract base class. Recall that when a class is marked as abstract, it may define any number of abstract members to provide a polymorphic interface to all derived types. However, even when a class type does define a set of abstract members, it is also free to define any number of constructors, field data, nonabstract members (with implementation), and so on. Interfaces, on the other hand, only contain abstract members.

The polymorphic interface established by an abstract parent class suffers from one major limitation in that only derived types support the members defined by the abstract parent. However, in larger software systems, it is very common to develop multiple class hierarchies that have no common parent beyond System.Object. Given that abstract members in an abstract base class only

CHAPTER 9 WORKING WITH INTERFACES

271

apply to derived types, we have no way to configure types in different hierarchies to support the same polymorphic interface. By way of an illustrative example, assume you have defined the following abstract class:

abstract class CloneableType

{

//Only derived types can support this

//"polymorphic interface." Classes in other

//heirarchies have no access to this abstract

//member.

public abstract object Clone();

}

Given this definition, only members that extend CloneableType are able to support the Clone() method. If you create a new collection of classes that do not extend this base class, you are unable to gain this polymorphic interface. As you would guess, interface types come to the rescue. Once an interface has been defined, it can be implemented by any type, in any hierarchy, within any namespaces or any assembly (written in any .NET programming language). Given this, interfaces are highly polymorphic. Consider the standard .NET interface named ICloneable defined in the System namespace. This interface defines a single method named Clone():

public interface ICloneable

{

object Clone();

}

If you were to examine the .NET Framework 3.5 SDK documentation, you would find that a large number of seemingly unrelated types (System.Array, System.Data.SqlClient.SqlConnection,

System.OperatingSystem, System.String, etc.) all implement this interface. Although these types have no common parent (other than System.Object), we can treat them polymorphically via the ICloneable interface type.

For example, if we had a method named CloneMe() that took an ICloneable interface parameter, we could pass this method any object that implements said interface. Consider the following simple Program class defined within a Console Application named ICloneableExample:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** A First Look at Interfaces *****\n");

//All of these types support the ICloneable interface. string myStr = "Hello";

OperatingSystem unixOS = new OperatingSystem(PlatformID.Unix, new Version()); System.Data.SqlClient.SqlConnection sqlCnn =

new System.Data.SqlClient.SqlConnection();

//Therefore, they can all be passed into a method taking ICloneable.

CloneMe(myStr);

CloneMe(unixOS);

CloneMe(sqlCnn);

Console.ReadLine();

}

private static void CloneMe(ICloneable c)

{

// Clone whatever we get and print out the name.