Topics in This Chapter
Creating Objects: Learn how to use a factory design pattern to create objects.
Exception Handling: Effective exception handling requires an understanding of how exceptions are thrown and caught in .NET. Along with an overview of exception handling, this section looks at how to create custom exception objects.
Using System.Object Methods: Familiarity with the System.Object
methods is necessary if you want to create custom collection classes that implement the standard features found in classes contained in the Framework Class Library.
Collection Classes and Interfaces: .NET offers a variety of collection classes for managing multiple objects. This section looks at arrays, hash tables, and stacks, among others. Of particular interest are the 2.0 classes that support generics.
Object Serialization: Objects can be converted (serialized) into a binary stream or an XML formatted stream. An example using the binary serializer is presented.
Object Life Cycle Management: .NET Garbage Collection automatically removes unreferenced objects. This can produce unwanted results unless measures are taken to ensure that objects are destroyed properly.
The purpose of this chapter is to consider what happens to a class when it becomes an object. This metamorphosis raises some interesting questions: What is the best way to create an object? How do you ensure that an object handles errors gracefully? How do you prevent an object from wasting resources (the dreaded memory leak)? What is the best way to work with groups of objects? How do you dispose of an object? Although these questions are unlikely to keep a developer awake at night, their consideration should lead to a keener insight into class design.
In an attempt to answer these questions, a variety of topics are presented. These include how to create objects using established design patterns; how to implement the System.Object
methods on custom classes; how to implement exception handling; how to persist objects using serialization; and how to use collection classes and interfaces to manage groups of objects. The chapter concludes with a look at how to design an object so that it shuts down properly when subject to .NET Garbage Collection.
The subject of creating objects typically receives less attention than that of designing classes; but it is important enough that a formal body of techniques known as creational patterns has been developed to provide object creation models. A popular approach—used throughout the Framework Class Library (FCL)—is to implement the factory creational pattern. As the name implies, the factory is an object whose sole purpose is to create other objects—much like a real-world factory. Its advantage is that it handles all the details of object creation; a client instructs the factory which object to create and is generally unaffected by any implementation changes that may occur.
There are a number of ways to implement the factory pattern. This section presents two logical approaches—illustrated in Figures 4-1 and 4-2.
Figure 4-1 represents the case where one factory is used to produce all of the related products (objects). In Figure 4-2, each product has its own factory, and the client sends the request to the factory that produces the desired object. We'll look at examples of both, beginning with code for the single factory implementation (see Listing 4-1).
Example 4-1. Using a Class Factory to Create Objects— Single Factory
public interface IApparel // Interface representing product { string ShowMe(); bool Knit // Property to indicate if Knit { get; } } public class SportsShirt : IApparel { public string ShowMe() { return("Sports Shirt"); } public bool Knit { get {return true;} } } public class DressShirt : IApparel { public string ShowMe() { return("Dress Shirt"); } public bool Knit { get {return false;} } } // Factory to return instances of apparel classes public class ApparelFactory { public IApparel CreateApparel( string apptype) { switch (apptype) { case "MDRSHIRT": return new DressShirt(); case "MSPSHIRT": return new SportsShirt(); } return null; } }
In this example, the class factory implements a method named CreateApparel
that returns objects. The most important thing to note is that this method is declared with IApparel
as its return type, which enables it to return an instance of any class that implements that interface.
public IApparel CreateApparel( string apptype)
The same effect could be achieved by replacing the interface with an abstract class. This could yield better code reuse in situations where objects share common behavior, but should be weighed against other factors that were discussed in Chapter 3, “Class Design in C#.”
With the factory and product classes defined, all the hard work has been done. It's a simple matter for clients to create objects:
ApparelFactory factory= new ApparelFactory(); IApparel ob1 = factory.CreateApparel("MDRSHIRT"); IApparel ob2 = factory.CreateApparel("MSPSHIRT"); string shirtType = ob1.ShowMe(); // Returns "Dress Shirt"
If the application needs to add any more products, the factory is supplied with the new code, but no changes are required on the client side. It only needs to be aware of how to request all available products.
This solution adds an abstract class (we could use an interface) for the factory and two concrete subclasses that implement it to produce specific apparel objects:
// abstract public abstract class AppFactory { public abstract IApparel CreateApparel(); } // Concrete factory classes public class DressShirtFactory:AppFactory { public override IApparel CreateApparel( ) { return new DressShirt(); } } public class SportShirtFactory : AppFactory { public override IApparel CreateApparel( ) { return new SportsShirt(); } }
We have created the abstract class so that its subclasses can be passed to a new ApparelCollector
class that serves as an intermediary between the clients and the factories. Specifically, the client passes the factory to this class, and it is responsible for calling the appropriate factory.
public class ApparelCollector { public void CollectApparel(AppFactory factory) { IApparel apparel = factory.CreateApparel(); } }
The code to use the new class is analogous to that in the first example:
AppFactory factory = new DressShirtFactory(); IApparel obj2 = new ApparelCollector().CollectApparel(factory);
For a simple example like this, the first approach using one factory is easier to implement. However, there are cases where it's preferable to have multiple factories. The objects may be grouped into families of products (a shirt factory and a dress factory, for example), or you may have a distributed application where it makes sense for different developers to provide their own factory classes.
One of the most important aspects of managing an object is to ensure that its behavior and interaction with the system does not result in a program terminating in error. This means that an application must deal gracefully with any runtime errors that occur, whether they originate from faulty application code, the Framework Class Library, or hardware faults.
.NET provides developers with a technique called structured exception handling (SEH) to deal with error conditions. The basic idea is that when an exception occurs, an exception object is created and passed along with program control to a specially designated section of code. In .NET terms, the exception object is thrown from one section of code to another section that catches it.
Compared to error handling techniques that rely on error codes and setting bit values, SEH offers significant advantages:
The exception is passed to the application as an object whose properties include a description of the exception, the assembly that threw the exception, and a stack trace that shows the sequence of calls leading to the exception.
If an exception is thrown and an application does not catch it, the Common Language Runtime (CLR) terminates the application. This forces the developer to take error handling seriously.
The exception handling and detection code does not have to be located where the errors occur. This means, for example, that exception handling code could be placed in a special class devoted to that purpose.
Exceptions are used exclusively and consistently at both the application and system level. All methods in the .NET Framework throw exceptions when an error occurs.
Before looking at the actual mechanics of implementing exception handling, let's examine the exception itself. As previously mentioned, an exception is a class instance. All .NET exceptions derive from the System.Exception
class. So, an understanding of this class is essential to working with exceptions.
As shown in Figure 4-3, System.Exception
is the base class for two generic subclasses—SystemException
and ApplicationException
—from which all exception objects directly inherit. .NET Framework exceptions (such as IOException
and ArithmeticException
) derive directly from IOException
, whereas custom application exceptions should inherit from ApplicationException
. The sole purpose of these classes is to categorize exceptions, because they do not add any properties or methods to the base System.Exception
class.
The System.Exception
class contains relatively few members. Table 4-1 summarizes the members discussed in this section.
Table 4-1. System.Exception
Class Properties
Property | Type | Description |
---|---|---|
|
| Contains a URL that points to help documentation. |
|
| Is set to |
|
| The text describing the exception. |
| Name of the assembly that generated the exception. | |
|
| Contains the sequence of method names and signatures that were called prior to the exception. It is invaluable for debugging. |
|
| Provides details about the method that threw the exception. The property is an object of type |
|
| This is a protected property used when interoperating with COM code. When an exception is thrown to a COM client, this value is converted to an |
C# uses a try
/catch
/finally
construct to implement exception handling (see Figure 4-4). When an exception occurs, the system searches for a catch
block that can handle the current type of exception. It begins its search in the current method, working down the list of catch
blocks sequentially. If none is found, it then searches the catch
blocks in the calling method associated with the relevant try
block. If the search yields no matching catch
block, an unhandled exception occurs. As discussed later, the application is responsible for defining a policy to deal with this. Let's look at the details of using these three blocks.
The code inside the try
block is referred to as a guarded region because it has associated catch
or finally
blocks to handle possible exceptions or cleanup duties. Each try
block must have at least one accompanying catch
or finally
block.
A catch
block consists of the keyword catch
followed by an expression in parentheses called the exception filter that indicates the type of exception to which it responds. Following this is the code body that implements the response.
The exception filter identifies the exception it handles and also serves as a parameter when an exception is thrown to it. Consider the following statement:
catch (DivideByZeroException ex) { ... }
The filter will be invoked if a System.DivideByZeroException
occurs. The variable ex
references the exception and provides access to its properties, such as ex.Message
and ex.StackTrace
.
When using multiple catch
blocks, ordering is important. They should be listed hierarchically, beginning with the most specific exceptions and ending with the more general ones. In fact, the compiler generates an error if you do not order them correctly.
catch (DivideByZeroException ex) { ... } catch (IndexOutOfRangeException ex) { ... } catch (Exception ex) { ... }
This codes first looks for specific exceptions such as a division by zero or an index out of range. The final exception filter, Exception
, catches any exception derived from System.Exception
. When an exception is caught, the code in the block is executed and all other catch
blocks are skipped. Control then flows to the finally
block—if one exists.
Note that the catch
block may include a throw
statement to pass the exception further up the call stack to the previous caller. The throw
statement has an optional parameter placed in parentheses that can be used to identify the type of exception being thrown. If throw
is used without a parameter, it throws the exception caught by the current block. You typically throw an exception when the calling method is better suited to handle it.
Regarded as the “cleanup” block, the finally
block is executed whether or not an exception occurs and is a convenient place to perform any cleanup operations such as closing files or database connections. This block must be included if there are no catch
blocks; otherwise, it is optional.
Listing 4-2 illustrates the use of the try
/catch
/finally
blocks in dealing with an exception generated by the CLR.
Example 4-2. Handling Exceptions Generated by the CLR
using System; // Class to illustrate results of division by zero public class TestExcep { public static int Calc(int j) { return (100 / j); } } class MyApp { public static void Main() { TestExcep exTest = new TestExcep(); try { // Create divide by zero in called method int dZero = TestExcep.Calc(0); // This statement is not executed Console.WriteLine("Result: {0}",dZero); } catch(DivideByZeroException ex) { Console.WriteLine("{0} {1} ", ex.Message, ex.Source); Console.WriteLine(ex.TargetSite.ToString()); Console.WriteLine(ex.StackTrace); } catch (Exception ex) { Console.WriteLine("General "+ex.Message); } finally { Console.WriteLine("Cleanup occurs here."); } } }
In this example, TestExcep.Calc
throws a division by zero exception when MyApp
calls it with a zero value. Because Calc
has no code to handle the exception, the exception is thrown back automatically to MyApp
at the point where Calc
was called. From there, control passes to the block provided to handle DivideByZeroException
exceptions. For demonstration purposes, the statements in the catch
block display the following information provided by the exception object:
Property | Value Printed |
---|---|
|
|
|
|
|
|
|
|
StackTrace
displays only those methods on the call stack to the level where the exception is first handled—not where it occurs. Although you may be tempted to catch exceptions at the point where they occur in order to view the full call stack, this is discouraged. It may improve diagnostics; however, it takes time and space to throw an exception and its entire call stack. Usually, the lower on a call stack that an exception occurs, the more likely it is that the conditions causing it can be avoided by improved coding logic.
Custom exception classes are useful when you need to describe errors in terms of the class that issues the error. For example, you may want the exception to describe the specific behavior causing the error or to indicate a problem with a parameter that does not meet some required criteria. In general, first look to the most specific system exception available; if that is inadequate, consider creating your own.
In Listing 4-3, a method throws a custom exception if the object it receives does not implement the two required interfaces. The exception, NoDescException
, returns a message describing the error and the name of the object causing the failure.
Example 4-3. Building a Custom Exception Class
// Custom Exception Class
[Serializable]
public class NoDescException : ApplicationException
{ // Three constructors should be implemented
public NoDescException(){}
public NoDescException(string message):base(message){}
public NoDescException(string message, Exception innerEx)
:base(message, innerEx){ }
}
// Interfaces that shape objects are to implement
public interface IShapeFunction
{ double GetArea(); }
public interface IShapeDescription
{ string ShowMe();}
// Circle and Rectangle classes are defined
class Circle : IShapeFunction
{
private double radius;
public Circle (double rad)
{
radius= rad;
}
// Methods to implement both interfaces
public double GetArea()
{ return (3.14*radius*radius); }
}
class Rectangle : IShapeFunction, IShapeDescription
{
private int width, height;
public Rectangle(int w, int h)
{
width= w;
height=h;
}
// Methods to implement both interfaces
public double GetArea()
{ return (height*width); }
public string ShowMe()
{ return("rectangle"); }
}
public class ObjAreas
{
public static void ShowAreas(object ObjShape)
{
// Check to see if interfaces are implemented
if (!(ObjShape is IShapeDescription &&
ObjShape is IShapeFunction) )
{
// Throw custom exception
string objName = ObjShape.ToString();
throw new NoDescException
("Interface not implemented for "+objName);
}
// Continue processing since interfaces exist
IShapeFunction myShape = (IShapeFunction)ObjShape;
IShapeDescription myDesc = (IShapeDescription) ObjShape;
string desc = myDesc.ShowMe();
Console.WriteLine(desc+" Area= "+
myShape.GetArea().ToString());
}
}
To view the custom exception in action, let's create two shape objects and pass them via calls to the static ObjAreas.ShowAreas
method.
Circle myCircle = new Circle(4.0);
Rectangle myRect = new Rectangle(5,2);
try
{
ObjAreas.ShowAreas(myRect);
ObjAreas.ShowAreas(myCircle);
}
catch (NoDescException ex)
{
Console.WriteLine(ex.Message);
}
The ShowAreas
method checks to ensure the object it has received implements the two interfaces. If not, it throws an instance of NoDescException
and control passes to the calling code. In this example, the Circle
object implements only one interface, resulting in an exception.
Pay particular attention to the design of NoDescException
. It is a useful model that illustrates the rules to be followed in implementing a custom exception type:
The class should be derived from ApplicationException
.
By convention, the exception name should end in Exception
. The Exception
base class defines three public constructors that should be included:
A parameterless constructor to serve as the default.
A constructor with one string parameter—usually the message.
A constructor with a string parameter and an Exception
object parameter that is used when an exception occurs while a previous exception is being handled.
Use the base
initializer to call the base class to take care of the actual object creation. If you choose to add fields or properties, add a new constructor to initialize these values.
The Serializable
attribute specifies that the exception can be serialized, which means it can be represented as XML for purposes of storing it or transmitting it. Typically, you can ignore this attribute, because it's only required if an exception object is being thrown from code in one application domain to another. Application domains are discussed Chapter 15, “Code Refinement, Security, and Deployment”; for now, think of them as logical partitions that .NET uses to isolate code segments.
Unhandled exceptions occur when the CLR is unable to find a catch
filter to handle the exception. The default result is that the CLR will handle it with its own methods. Although this provides a warning to a user or developer, it is not a recommended way to deal with it. The solution is in the problem: Take advantage of .NET's unhandled exception event handlers to funnel all of the exceptions to your own custom exception handling class.
The custom class provides a convenient way to establish a policy for dealing with unhandled exceptions. The code can be implemented to recognize whether it is dealing with a debug or release version, and respond accordingly. For example, in debug version, your main concern is to start the debugger; in a release version, you should log the error and provide a meaningful screen that allows the user to end the program.
Unfortunately, there is no single approach that applies to all C# programming needs. Your actual solution depends on whether you are working with a Console, Windows Form, Web Forms, or Web Services application. In this section, we will look at how to implement a Windows Forms solution, which is conceptually the same as for a Console application. Web Forms and Web Services are addressed in the Web Applications chapters, Chapters 16–18.
Event handling was discussed in the previous chapter along with the important role of delegates. We can now use those techniques to register our own callback method that processes any unhandled exceptions thrown in the Windows application.
When an exception occurs in Windows, the application's OnThreadException
method is ultimately called. It displays a dialog box describing the unhandled exception. You can override this by creating and registering your own method that matches the signature of the System.Threading.ThreadExceptionEventHandler
delegate. Listing 4-4 shows one way this can be implemented.
MyUnhandledMethod
is defined to handle the exception and must be registered to receive the callback. The following code registers the method for the ThreadException
event using the ThreadExceptionEventHandler
delegate and runs the application:
static void Main() { Application.ThreadException += new ThreadExceptionEventHandler(UnForgiven.MyUnhandledMethod); Application.Run(new Form1()); }
The implementation code in the method is straightforward. The most interesting aspect is the use of preprocessor directives (#if DEBUG
) to execute code based on whether the application is in release or debug mode.
Example 4-4. Unhandled Exceptions in a Windows Form
// Class to receive callback to process unhandled exceptions using System.Diagnostics; using System.Windows.Forms; using System.Threading; public class UnForgiven { // Class signature matches ThreadExceptionEventHandler public static void MyUnhandledMethod (object sender, ThreadExceptionEventArgs e) { #if DEBUG // Statements for debug mode // Display trace and start the Debugger MessageBox.Show("Debug: "+e.ToString()); #else // Statements for release mode // Provide output to user and log errors MessageBox.Show("Release: "+e.ToString()); #endif } }
For a Console application, the same approach is used except that the delegate and event names are different. You would register the method with the following:
Thread.GetDomain().UnhandledException += new UnhandledExceptionEventHandler( UnForgiven.MyUnhandledMethodAp);
Also, the method's EventArgs
parameter changes to UnhandledExceptionEventArgs
.
The use of exception handling is the key to implementing code that performs in a stable manner when unexpected conditions occur. But successful exception handling requires more than surrounding code with try
and catch
blocks. Thought must be given to which exceptions should be caught and what to do with a caught exception. Included here are some general rules to consider. For a more extensive list, consult online “best practices” information.
Only catch exceptions when you have a specific need to do the following:
Perform a recovery
Perform a cleanup, particularly to release resources
Log information
Provide extra debug information
Use exceptions to handle unexpected events, not as a way to implement logic.
Distinguish between error handling and exception handling. Incorrectly formatted credit card numbers or incorrect passwords, for example, are all occurrences that should be anticipated and handled by the logic of your code. An unavailable database table or hardware failure requires an exception.
Don't catch or throw base exception types.
The base exception type System.Exception
is a catch-all exception that can be used to catch any exception this is not specifically identified and caught. Much like “fools gold,” its intuitive appeal is deceptive. The objective of exception handling is to identify specific exceptions and handle with code appropriate for them; catching each exception prevents the unexpected ones from being identified. As a rule, catch specific exceptions and allow unidentified ones to propagate up the call stack.
Make liberal use of finally
blocks.
The purpose of a finally
block is to perform any housekeeping chores before a method is exited. It is implemented as part of both try
/catch
/finally
and try
/ finally
constructs. Because code should be more liberal in its use of throws than catches, an application should contain more try
/finally
constructs than try
/ catch
constructs.
The inclusion of thorough, intelligent exception handling code in an application is one of the hallmarks of well-written code. Although you'll find code segments in this book where exception handling is ignored or used sparingly, understand that this is done only in the interest of simplifying code examples. In real-world programming, there are no excuses for leaving it out.
When creating a custom class, particularly one that will be available to other developers, it is important to ensure that it observes the proper rules of object etiquette. It should be CLS compliant, provide adequate exception handling, and adhere to OOP principles of encapsulation and inheritance when providing member accessibility. A class should also implement the features that .NET developers are accustomed to when working with Framework classes. These features include a custom implementation of the System.Object
methods that all classes inherit:
ToString(). By default, this method returns the name of the class. It should be overridden to display contents of the object that distinguish it from other instances of the class.
Equals(). This is used to compare instances of a class to determine if they are equal. It's up to the class to define what equality means. This could mean that two objects have matching field values or that two objects reference the same memory location.
MemberwiseClone(). This method returns a copy of an object that contains the object's value fields only—referred to as a shallow copy. A custom class can use this method to return a copy of itself or implement its own clone method to return a deep copy that also includes reference type values.
The remainder of this section provides examples of overriding or using these methods in custom classes. Note that System.Object.Finalize
—a method to perform cleanup duties before an object is claimed by garbage collection—is discussed in Section 4.6, “Object Life Cycle Management.”
This method is most commonly used with primitive types to convert numbers to a string format that can be displayed or printed. When used with objects, the default is to return the fully qualified name of the object: <namespace>.<classname>
. It's common to override this and return a string containing selected values from members of the object. A good example of this is the exception object from the previous section (refer to Figure 4-3) that returns the Message
and StackTrace
values:
ex.ToString() // Output: // Attempted to divide by zero // at TestExcep.Calc(Int32 j) // at MyApp.Main()
The code shown in Listing 4-5 demonstrates how easy it is to implement ToString
in your own class.
The StringBuilder
class is used to create the text string returned by the method. It provides an efficient way of handling strings and is described in Chapter 5, “C# Text Manipulation and File I/O.”
Example 4-5. Overriding ToString()
using System.Text; using System; public class Chair { private double myPrice; private string myVendor, myID; public Upholstery myUpholstery; public Chair(double price, string vendor, string sku) { myPrice = price; myVendor = vendor; myID = sku; } // Override System.Object ToString() public override string ToString() { StringBuilder chairSB = new StringBuilder(); chairSB.AppendFormat("ITEM = Chair"); chairSB.AppendFormat(" VENDOR = {0}", this.myVendor); chairSB.AppendFormat(" PRICE = {0}", this.myPrice.ToString()); return chairSB.ToString(); } public string MyVen { get {return myVendor;} } //... other properties to expose myPrice, myID } public class Upholstery { public string Fabric ; public Upholstery( string fab) { Fabric = fab; } }
The following statements create an instance of the object and set desc
to the more meaningful value returned by ToString()
:
Chair myChair = new Chair(120.0, "Broyhill", "60-1222"); string desc = myChair.ToString()); // Returns ITEM = Chair VENDOR = Broyhill PRICE = 120.0
When comparing two reference types, Equals
returns true only if they point to the same object in memory. To compare objects based on their value (value equality rather than referential equality), you must override the default method. An example of this is the String
class—a reference type that implements Equals
to perform a value comparison based on the characters in the strings.
The code in Listing 4-6 illustrates how to override Equals
in the Chair
class to compare objects using value semantics. It also overrides the GetHashCode
method—always recommended when overriding Equals
.
Example 4-6. Overriding Equals()
public override bool Equals(Object obj) { // Include the following two statements if this class // derives from a class that overrides Equals() //if (!base.Equals(obj)) // return false; // (1) Null objects cannot be compared if (obj == null) return false; // (2) Check object types if (this.GetType() != obj.GetType()) return false; // (3) Cast object so we can access its fields Chair otherObj = (Chair) obj; // (4) Compare reference fields if (!Object.Equals(myUpholstery, otherObj.myUpholstery)) return false; // (5) Compare Value Type members if (!myVendor.Equals(otherObj.myVendor)) return false; if (!myPrice.Equals(otherObj.myPrice)) return false; if (!myID.Equals(otherObj.myID)) return false; return true; } // Override GetHashCode – Required if Equals overridden public override int GetHashCode() { return myID.GetHashCode(); }
This method compares an instance of the current object with the one passed to it. The first step is to ensure that the received object is not null. Next, following the steps in Figure 4-5, the types of the two objects are compared to make sure they match.
The heart of the method consists of comparing the field values of the two objects. To compare reference fields, it uses the static Object.Equals
method, which takes two objects as arguments. It returns true
if the two objects reference the same instance, if both objects are null, or if the object's Equals
comparison returns true
. Value types are compared using the field's Equals
method:
if (!myID.Equals(otherObj.myID)) return false;
Here is an example that demonstrates the new Equals
method. It creates two Chair
objects, sets their fields to the same value, and performs a comparison:
Chair myChair = new Chair(150.0, "Lane", "78-0988"); myChair.myUpholstery = new Upholstery("Silk"); Chair newChair = new Chair(150.0, "Lane", "78-0988"); newChair.myUpholstery= new Upholstery("Silk"); // Next statement returns false. Why? bool eq = ( myChair.Equals(newChair));
Although the two objects have identical field values, the comparison fails—which is probably not what you want. The reason is that the objects point to two different myUpholstery
instances, causing their reference comparison to fail. The solution is to override the Equals
method in the Upholstery
class, so that it performs a value comparison of the Fabric
fields. To do so, place this code inside its Equals
method, in addition to the other overhead code shown in Listing 4-6:
Upholstery otherObj = (Upholstery) obj; if (!Fabric.Equals(otherObj.Fabric)) return false;
The GetHashCode
method generates an Int32
hash code value for any object. This value serves as an identifier that can be used to place any object in a hash table collection. The Framework designers decreed that any two objects that are equal must have the same hash code. As a result, any new Equals
method must be paired with a GetHashCode
method to ensure that identical objects generate the same hash code value.
Ideally, the hash code values generated over the range of objects represent a wide distribution. This example used a simple algorithm that calls the base type's GetHashCode
method to return a value based on the item's ID. This is a good choice because the IDs are unique for each item, the ID is an instance field, and the ID field value is immutable—being set only in the constructor.
After you override the original Equals
method to compare object values, your application may still need to determine if reference variables refer to the same object. System.Object
has a static method called ReferenceEquals
for this purpose. It is used here with our Chair
class:
Chair chair1 = new Chair(120.0, "Broyhill", "66-9888") )
Chair chair2 = new Chair(120.0, "Broyhill", "66-9933") )
Chair chair3 = chair1;
if (Object.ReferenceEquals(chair1, chair3) )
{ MessageBox.Show("Same object");}
else
{ MessageBox.Show("Different objects");}
The method returns true
because chair1
and chair3
reference the same instance.
The usual way to create an object is by using the new
operator to invoke a constructor. An alternative approach is to make a copy or clone of an existing object. The object being cloned may be a class instance, an array, a string, or any reference type; primitives cannot be cloned. For a class to be cloneable, it must implement the ICloneable
interface. This is a simple interface defined as
public interface ICloneable { Object Clone(); }
It consists of a single method, Clone
, that is implemented to create a copy of the object. The cloned object may represent a shallow copy or a deep copy of the object's fields. A shallow copy creates a new instance of the original object type, and then copies the non-static fields from the original object to the new object. For a reference type, only a pointer to the value is copied. Thus, the clone points to the same reference object as the original. A deep copy duplicates everything. It creates a copy of reference objects and provides a reference to them in the copy.
Figure 4-6 depicts a shallow and deep copy for this instance of the Chair
class:
Chair myChair = new Chair(150.0, "Lane", "78-0988"); Upholstery myFabric = new Upholstery("Silk"); myChair.myUpholstery = myFabric;
In both cases, the clone of the myChair
object contains its own copy of the value type fields. However, the shallow copy points to the same instance of myUpholstery
as the original; in the deep copy, it references a duplicate object.
Let's now look at how to implement shallow cloning on a custom object. Deep cloning (not discussed) is specific to each class, and it essentially requires creating an instance of the object to be cloned and copying all field values to the clone. Any reference objects must also be created and assigned values from the original referenced objects.
A shallow copy is sufficient when the object to be copied contains no reference type fields needed by the copy. The easiest way to implement shallow copying is to use the System.Object.MemberwiseClone
method, a protected, non-virtual method that makes a shallow copy of the object it is associated with. We use this to enable the Chair
class to clone itself:
public class Chair: ICloneable { //... other code from Listing 4-5 public Object Clone() { return MemberwiseClone(); // from System.Object }
The only requirements are that the class inherit the ICloneable
interface and implement the Clone
method using MemberwiseClone
. To demonstrate, let's use this code segment to create a shallow copy clone of myChair
by calling its Clone
method:
// Make clone of myChair
Chair chairClone = (Chair)myChair.Clone();
bool isEqual;
// (1) Following evaluates to false
isEqual = Object.ReferenceEquals(myChair,chairClone);
// (2) Following evaluates to true
isEqual = Object.ReferenceEquals(
myChair.myUpholstery,chairClone.myUpholstery);
The results confirm this is a shallow copy: The reference comparison of myChair
and its clone fails because chairClone
is created as a copy of the original object; on the other hand, the comparison of the reference field myUpholstery
succeeds because the original and clone objects point to the same instance of the myUpholstery
class.
In .NET, collection is a general term that applies to the set of classes that represent the classic data structures used to group related types of data. These classes include stacks, queues, hash tables, arrays, and dictionaries. All are contained in one of two namespaces: System.Collections
or System.Collections.Generic
. In .NET versions 1.0 and 1.1, System.Collections
was home to all collection classes. In the .NET 2.0 release, these original classes were revised to support generics—a way to make collections type-safe.
The .NET developers left the original namespace in for backward compatibility and added the System.Collections.Generic
namespace to contain the generic classes. With the exception of the generics features, the classes in the two namespaces offer the same functionality—although there are a few name changes. To avoid confusion, much of this section refers to the System.Collections
namespace. The details of generics are presented at the end of the section.
To best work with container classes such as the ArrayList
and Hashtable
, a developer should be familiar with the interfaces they implement. Interfaces not only provide a uniform way of managing and accessing the contents of a collection, but they are the key to creating a custom collection. We'll begin the section by looking at the most useful interfaces and the behavior they impart to the collection classes. Then, we'll examine selected collection classes.
Interfaces play a major role in the implementation of the concrete collection classes. All collections inherit from the ICollection
interface, and most inherit from the IEnumerable
interface. This means that a developer can use common code semantics to work with the different collections. For example, the foreach
statement is used to traverse the elements of a collection whether it is a Hashtable,
a SortedList
, or a custom collection. The only requirement is that the class implements the IEnumerable
interface.
Table 4-2 summarizes the most important interfaces inherited by the collection classes. IComparer
provides a uniform way of comparing elements for the purpose of sorting; IDictionary
defines the special members required by the Hashtable
and Dictionary
objects; similarly, IList
is the base interface for the ArrayList
collection.
Table 4-2. System.Collections
Interface | Description |
---|---|
| The base interface for the collection classes. It contains properties to provide the number of elements in the collection and whether it is thread-safe. It also contains a method that copies elements of the collection into an array. |
| Exposes a method that is used to compare two objects and plays a vital role in allowing objects in a collection to be sorted. |
| Allows an object to represent its data as a collection of key-and-value pairs. |
| Permits iteration through the contents of an object that implements the |
IEnumerator IEnumerable | Supports a simple iteration through a collection. The iteration only supports reading of data in the collection. |
| Supplies a hash code for an object. It contains one method: |
| The base interface of all lists. It controls whether the elements in the list can be modified, added, or deleted. |
The UML-like diagram in Figure 4-7 shows how these interfaces are related. Recall that interfaces may inherit from multiple interfaces. Here, for example, IDictionary
and IList
inherit from IEnumerable
and ICollection
. Also of special interest is the GetEnumerator
method that is used by interfaces to return the IEnumerator
interface.
Of these interfaces, IDictionary
and IList
are the most important when considering the built-in collections provided by the FCL. For this reason, we'll discuss them later in the context of the collection classes.
This interface provides the minimal information that a collection must implement. All classes in the System.Collections
namespace inherit it (see Table 4-3).
Table 4-3. ICollection
Members
Member | Description |
---|---|
| Property that returns the number of entries in the collection. |
| Property that indicates if access to the collection is thread-safe. |
| Property that returns an object that can be used to synchronize access to a collection by different threads. |
| Method to copy the contents of the collection to an array. |
The IsSynchronized
and SyncRoot
properties require explanation if you are not yet familiar with threading (discussed in Chapter 13, “Asynchronous Programming and Multithreading”). Briefly, their purpose is to ensure the integrity of data in a collection while it is being accessed by multiple clients. It's analogous to locking records in a database to prevent conflicting updates by multiple users.
This interface has one member—the GetHashCode
method—that uses a custom hash function to return a hash code value for an object. The Hashtable
class uses this interface as a parameter type in one of its overloaded constructors, but other than that you will rarely see it.
For a class to support iteration using the foreach
construct, it must implement the IEnumerable
and IEnumerator
interfaces. IEnumerator
defines the methods and properties used to implement the enumerator pattern; IEnumerable
serves only to return an IEnumerator
object. Table 4-4 summarizes the member(s) provided by each interface.
Table 4-4. IEnumerable
and IEnumerator
Member | Description |
---|---|
| Returns an |
| Property that returns the current value in a collection. |
| Advances the enumerator to the next item in the collection. Returns |
| Sets the enumerator to its beginning position in the collection. This method is not included in the |
These members are implemented in a custom collection class as a way to enable users to traverse the objects in the collection. To understand how to use these members, let's look at the underlying code that implements the foreach
construct.
The foreach
loop offers a simple syntax:
foreach ( ElementType element in collection) { Console.WriteLine(element.Name); }
The compiler expands the foreach
construct into a while
loop that employs IEnumerable
and IEnumerator
members:
// Get enumerator object using collection's GetEnumerator method IEnumerator enumerator = ((IEnumerable) (collection)).GetEnumerator(); try { while (enumerator.MoveNext()) { ElementType element = (ElementType)enumerator.Current; Console.WriteLine(element.Name); } } finally // Determine if enumerator implements IDisposable interface // If so, execute Dispose method to release resources { IDisposable disposable = enumerator as System.IDisposable; If( disposable !=null) disposable.Dispose(); }
When a client wants to iterate across the members of a collection (enumerable), it obtains an enumerator object using the IEnumerable.GetEnumerator
method. The client then uses the members of the enumerator to traverse the collection: MoveNext()
moves to the next element in the collection, and the Current
property returns the object referenced by the current index of the enumerator. Formally, this is an implementation of the iteration design pattern.
Enumerating through a collection is intrinsically not a thread-safe procedure. If another client (thread) modifies the collection during the enumeration, an exception is thrown. To guarantee thread safety, lock the collection prior to traversing it:
lock( myCollection.SyncRoot ) { foreach ( Object item in myCollection ) { // Insert your code here } }
The foreach
construct provides a simple and uniform way to iterate across members of a collection. For this reason, it is a good practice—almost a de facto requirement—that any custom collection support foreach
. Because this requires that the collection class support the IEnumerable
and IEnumerator
interfaces, the traditional approach is to explicitly implement each member of these interfaces inside the collection class. Referencing Table 4-4, this means writing code to support the GetEnumerator
method of the IEnumerable
interface, and the MoveNext
, Current
, and Reset
members of IEnumerator
. In essence, it is necessary to build a state machine that keeps track of the most recently accessed item in a collection and knows how to move to the next item.
C# 2.0 introduced a new syntax referred to as iterators that greatly simplifies the task of implementing an iterator pattern. It includes a yield return
(also a yield break
) statement that causes the compiler to automatically generate code to implement the IEnumerable
and IEnumerator
interfaces. To illustrate, let's add iterators to the GenStack
collection class that was introduced in the generics discussion in Chapter 3. (Note that iterators work identically in a non-generics collection.)
Listing 4-7 shows part of the original GenStack
class with two new members that implement iterators: a GetEnumerator
method that returns an enumerator to traverse the collection in the order items are stored, and a Reverse
property that returns an enumerator to traverse the collection in reverse order. Both use yield return
to generate the underlying code that supports foreach
iteration.
Example 4-7. Using Iterators to Implement Enumeration
using System; using System.Collections.Generic; public class GenStack<T>: IEnumerable<T> { // Use generics type parameter to specify array type private T[] stackCollection; private int count = 0; // Constructor public GenStack(int size) { stackCollection = new T[size]; } // (1) Iterator public IEnumerator<T> GetEnumerator() { for (int i = 0; i < count; i++) { yield return stackCollection[i]; } } // (2) Property to return the collection in reverse order public IEnumerable<T> Reverse { get { for (int i = count - 1; i >= 0; i--) { yield return stackCollection[i]; } } } public void Add(T item) { stackCollection[count] = item; count += 1; } // other class methods go here ...
This code should raise some obvious questions about iterators: Where is the implementation of IEnumerator
? And how can a method with an IEnumerator
return type or a property with an IEnumerable
return type seemingly return a string
value?
The answer to these questions is that the compiler generates the code to take care of the details. If the member containing the yield return
statement is an IEnumerable
type, the compiler implements the necessary generics or non-generics version of both IEnumerable
and IEnumerator
; if the member is an IEnumerator
type, it implements only the two enumerator interfaces. The developer's responsibility is limited to providing the logic that defines how the collection is traversed and what items are returned. The compiler uses this logic to implement the IEnumerator.MoveNext
method.
The client code to access the GenStack
collection is straightforward. An instance of the GenStack
class is created to hold ten string
elements. Three items are added to the collection and are then displayed in original and reverse sequence.
GenStack<string> myStack = new GenStack<string>(10); myStack.Add("Aida"); myStack.Add("La Boheme"); myStack.Add("Carmen"); // uses enumerator from GetEnumerator() foreach (string s in myStack) Console.WriteLine(s); // uses enumerator from Reverse property foreach (string s in myStack.Reverse) Console.WriteLine(s);
The Reverse
property demonstrates how easy it is to create multiple iterators for a collection. You simply implement a property that traverses the collection in some order and uses tbe yield return
statement(s) to return an item in the collection.
In order to stop iteration before an entire collection is traversed, use the yield break
keywords. This causes MoveNext()
to return false
.
There are some restrictions on using yield return
or yield break
:
It can only be used inside a method, property (get accessor), or operator.
It cannot be used inside a finally
block, an anonymous method, or a method that has ref
or out
arguments.
The method or property containing yield return must have a return type of Collections.IEnumerable
, Collections.Generic. IEnumerable<>
, Collections.IEnumerator
, or Collections. Generic.IEnumerator<>
.
Chapter 2, “C# Language Fundamentals,” included a discussion of the System.Array
object and its associated Sort
method. That method is designed for primitive types such as strings and numeric values. However, it can be extended to work with more complex objects by implementing the IComparable
and IComparer
interfaces on the objects to be sorted.
Unlike the other interfaces in this section, IComparable
is a member of the System namespace. It has only one member, the method CompareTo
:
int CompareTo(Object obj)
Returned Value | Condition |
---|---|
Less than 0 | Current instance < obj |
0 | Current instance = obj |
Greater than 0 | Current instance > obj |
The object in parentheses is compared to the current instance of the object implementing CompareTo
, and the returned value indicates the results of the comparison. Let's use this method to extend the Chair
class so that it can be sorted on its myPrice
field. This requires adding the IComparable
inheritance and implementing the CompareTo
method.
public class Chair : ICloneable, IComparable { private double myPrice; private string myVendor, myID; public Upholstery myUpholstery; //... Constructor and other code // Add implementation of CompareTo to sort in ascending int IComparable.CompareTo(Object obj) { if (obj is Chair) { Chair castObj = (Chair)obj; if (this.myPrice > castObj.myPrice) return 1; if (this.myPrice < castObj.myPrice) return -1; else return 0; // Reverse 1 and –1 to sort in descending order } throw new ArgumentException("object in not a Chair"); }
The code to sort an array of Chair
objects is straightforward because all the work is done inside the Chair
class:
Chair[]chairsOrdered = new Chair[4]; chairsOrdered[0] = new Chair(150.0, "Lane","99-88"); chairsOrdered[1] = new Chair(250.0, "Lane","99-00"); chairsOrdered[2] = new Chair(100.0, "Lane","98-88"); chairsOrdered[3] = new Chair(120.0, "Harris","93-9"); Array.Sort(chairsOrdered); // Lists in ascending order of price foreach(Chair c in chairsOrdered) MessageBox.Show(c.ToString());
The previous example allows you to sort items on one field. A more flexible and realistic approach is to permit sorting on multiple fields. This can be done using an overloaded form of Array.Sort
that takes an object that implements IComparer
as its second parameter.
IComparer
is similar to IComparable
in that it exposes only one member, Compare
, that receives two objects. It returns a value of –1, 0, or 1 based on whether the first object is less than, equal to, or greater than the second. The first object is usually the array to be sorted, and the second object is a class implementing a custom Compare
method for a specific object field. This class can be implemented as a separate helper class or as a nested class within the class you are trying to sort (Chair
).
This code creates a helper class that sorts the Chair
objects by the myVendor
field:
public class CompareByVen : IComparer { public CompareByVen() { } int IComparer.Compare(object obj1, object obj2) { // obj1 contains array being sorted // obj2 is instance of helper sort class Chair castObj1 = (Chair)obj1; Chair castObj2 = (Chair)obj2; return String.Compare (castObj1.myVendor,castObj2.myVendor); } }
If you refer back to the Chair
class definition (refer to Figure 4-6 on page 166), you will notice that there is a problem with this code: myVendor
is a private member and not accessible in this outside class. To make the example work, change it to public
. A better solution, of course, is to add a property to expose the value of the field.
In order to sort, pass both the array to be sorted and an instance of the helper class to Sort
:
Array.Sort(chairsOrdered,new CompareByVen());
In summary, sorting by more than one field is accomplished by adding classes that implement Compare
for each sortable field.
The classes in this namespace provide a variety of data containers for managing collections of data. As shown in Figure 4-8, it is useful to categorize them based on the primary interface they implement: ICollection
, IList
, or IDictionary
.
The purpose of interfaces is to provide a common set of behaviors for classes. Thus, rather than look at the details of all the classes, we will look at the interfaces themselves as well as some representative classes that illustrate how the members of the interfaces are implemented. We've already looked at ICollection
, so let's begin with two basic collections that inherit from it.
The Stack
and the Queue
are the simplest of the collection classes. Because they do not inherit from IList
or IDictionary
, they do not provide indexed or keyed access. The order of insertion controls how objects are retrieved from them. In a Stack
, all insertions and deletions occur at one end of the list; in a Queue
, insertions are made at one end and deletions at the other. Table 4-5 compares the two.
Table 4-5. Stack and Queue—Selected Members and Features
Description | Stack | Queue |
---|---|---|
Method of maintaining data | Last-in, first-out (LIFO) | First-in, first-out (FIFO) |
Add an item |
|
|
Remove an item |
|
|
Return the current item without removing it |
|
|
Determine whether an item is in the collection |
|
|
Constructors |
|
|
Stacks play a useful role for an application that needs to maintain state information in order to hold tasks to be “performed later.” The call stack associated with exception handling is a classic example; stacks are also used widely in text parsing operations. Listing 4-8 provides an example of some of the basic stack operations.
Example 4-8. Using the Stack Container
public class ShowStack { public static void Main() { Stack myStack = new Stack(); myStack.Push(new Chair(250.0, "Adams Bros.", "87-00" )); myStack.Push(new Chair(100.0, "Broyhill","87-04" )); myStack.Push(new Chair(100.0, "Lane","86-09" )); PrintValues( myStack ); // Adams – Broyhill - Lane // Pop top object and push a new one on stack myStack.Pop(); myStack.Push(new Chair(300.0, "American Chair")); Console.WriteLine(myStack.Peek().ToString()); // American } public static void PrintValues( IEnumerable myCollection ) { System.Collections.IEnumerator myEnumerator = myCollection.GetEnumerator(); while ( myEnumerator.MoveNext() ) Consle.WriteLine(myEnumerator.Current.ToString()); // Could list specific chair fields with // myChair = (Chair) myEnumerator.Current; } }
Three objects are added to the stack. PrintValues
enumerates the stack and lists the objects in the reverse order they were added. The Pop
method removes “Lane” from the top of the stack. A new object is pushed onto the stack and the Peek
method lists it. Note that the foreach
statement could also be used to list the contents of the Stack
.
The ArrayList
includes all the features of the System.Array
, but also extends it to include dynamic sizing and insertion/deletion of items at a specific location in the list. These additional features are defined by the IList
interface from which ArrayList
inherits.
This interface, whose members are listed in Table 4-6, is used to retrieve the contents of a collection via a zero-based numeric index. This permits insertion and removal at random location within the list.
Table 4-6. IList
Members
Interface | Description |
---|---|
| Indicates whether the collection has a fixed size. A fixed size prevents the addition or removal of items after the collection is created. |
| Items in a collection can be read but not modified. |
| Determines the index of a specific item in the collection. |
| Adds an item to the end of a list. It returns the value of the index where the item was added. |
void Insert (index, object) void RemoveAt (index) void Remove (object) | Methods to insert a value at a specific index; delete the value at a specific index; and remove the first occurrence of an item having the specified value. |
| Remove all items from a collection. |
| Returns |
The most important thing to observe about this interface is that it operates on object
types. This means that an ArrayList
—or any collection implementing IList
—may contain types of any kind. However, this flexibility comes at a cost: Casting must be widely used in order to access the object's contents, and value types must be converted to objects (boxed) in order to be stored in the collection. As we see shortly, C# 2.0 offers a new feature—called generics—that addresses both issues. However, the basic functionality of the ArrayList
, as illustrated in this code segment, remains the same.
ArrayList chairList = new ArrayList( ); // alternative: ArrayList chairList = new ArrayList(5); chairList.Add(new Chair(350.0, "Adams", "88-00")); chairList.Add(new Chair(380.0, "Lane", "99-33")); chairList.Add(new Chair(250.0, "Broyhill", "89-01")); PrintValues(chairList); // Adams – Lane - Broyhill // chairList.Insert(1,new Chair(100,"Kincaid")); chairList.RemoveAt(2); Console.WriteLine("Object Count: {0}",chairList.Count); // PrintValues(chairList); // Adams – Kincaid - Broyhill // Copy objects to an array Object chairArray = chairList.ToArray(); PrintValues((IEnumerable) chairArray);
This parameterless declaration of ArrayList
causes a default amount of memory to be initially allocated. You can control the initial allocation by passing a size parameter to the constructor that specifies the number of elements you expect it to hold. In both cases, the allocated space is automatically doubled if the capacity is exceeded.
The Hashtable
is a .NET version of a dictionary for storing key-value pairs. It associates data with a key and uses the key (a transformation algorithm is applied) to determine a location where the data is stored in the table. When data is requested, the same steps are followed except that the calculated memory location is used to retrieve data rather than store it.
Syntax:
public class Hashtable : IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback, ICloneable
As shown here, the Hashtable
inherits from many interfaces; of these, IDictionary
is of the most interest because it provides the properties and methods used to store and retrieve data.
Collections implementing the IDictionary
interface contain items that can be retrieved by an associated key value. Table 4-7 summarizes the most important members for working with such a collection.
Table 4-7. IDictionary
Member
Member | Description |
---|---|
| Indicates whether |
| Elements in a collection can be read but not modified. |
ICollection Keys ICollection Values | Properties that return the keys and values of the collection. |
void Add (key, value) void Clear () void Remove (key) | Methods to add a key-value pair to a collection, remove a specific key, and remove all items (clear) from the collection. |
| Returns |
IDictionaryEnumerator GetEnumerator () | Returns an instance of the |
As shown in Figure 4-7 on page 169, IDictionaryEnumerator
inherits from IEnumerator
. It adds properties to enumerate through a dictionary by retrieving keys, values, or both.
Table 4-8. IDictionaryEnumerator
Members
Member | Description |
---|---|
| The variable |
object Key object Value | Properties that return the keys and values of the current collection entry. |
All classes derived from IDictionary
maintain two internal lists of data: one for keys and one for the associated value, which may be an object. The values are stored in a location based on the hash code of the key. This code is provided by the key's System.Object.GetHashCode
method, although it is possible to override this with your own hash algorithm.
This structure is efficient for searching, but less so for insertion. Because keys may generate the same hash code, a collision occurs that requires the code be recalculated until a free bucket is found. For this reason, the Hashtable
is recommended for situations where a large amount of relatively static data is to be searched by key values.
A parameterless constructor creates a Hashtable
with a default number of buckets allocated and an implicit load factor of 1. The load factor is the ratio of values to buckets the storage should maintain. For example, a load factor of .5 means that a hash table should maintain twice as many buckets as there are values in the table. The alternate syntax to specify the initial number of buckets and load factor is
Hashtable chairHash = new Hashtable(1000, .6)
The following code creates a hash table and adds objects to it:
// Create HashTable Hashtable chairHash = new Hashtable(); // Add key - value pair to Hashtable chairHash.Add ("88-00", new Chair(350.0, "Adams", "88-00"); chairHash.Add ("99-03", new Chair(380.0, "Lane", "99-03"); // or this syntax chairHash["89-01"] = new Chair(250.0, "Broyhill", "89-01");
There are many ways to add values to a Hashtable
, including loading them from another collection. The preceding example shows the most straightforward approach. Note that a System.Argument
exception is thrown if you attempt to add a value using a key that already exists in the table. To check for a key, use the ContainsKey
method:
// Check for existence of a key bool keyFound; if (chairHash.ContainsKey("88-00")) { keyFound = true;} else {keyFound = false;}
The following iterates through the Keys
property collection:
// List Keys foreach (string invenKey in chairHash.Keys) { MessageBox.Show(invenKey); }
These statements iterate through the Values
in a hash table:
// List Values foreach (Chair chairVal in chairHash.Values) { MessageBox.Show(chairVal.myVendor);}
This code lists the keys and values in a hash table:
foreach ( DictionaryEntry deChair in chairHash) { Chair obj = (Chair) deChair.Value; MessageBox.Show(deChair.Key+" "+ obj.myVendor); }
The entry in a Hashtable
is stored as a DictionaryEntry
type. It has a Value
and Key
property that expose the actual value and key. Note that the value is returned as an object that must be cast to a Chair
type in order to access its fields.
According to .NET documentation (1.x), a synchronized version of a Hashtable
that is supposed to be thread-safe for a single writer and concurrent readers can be created using the Synchronized
method:
Hashtable safeHT = Hashtable.Synchronized(newHashtable());
Unfortunately, the .NET 1.x versions of the Hashtable
have been proven not to be thread-safe for reading. Later versions may correct this flaw.
This section has given you a flavor of working with System.Collections
interfaces and classes. The classes presented are designed to meet most general-purpose programming needs. There are numerous other useful classes in the namespace as well as in the System.Collections.Specialized
namespace. You should have little trouble working with either, because all of their classes inherit from the same interfaces presented in this section.
Recall from Chapter 3 that generics are used to implement type-safe classes, structures, and interfaces. The declaration of a generic type includes one (or more) type parameters in brackets (<>
) that serve(s) as a placeholder for the actual type to be used. When an instance of this type is created, the client uses these parameters to pass the specific type of data to be used in the generic type. Thus, a single generic class can handle multiple types of data in a type-specific manner.
No classes benefit more from generics than the collections classes, which stored any type of data as an object in .NET 1.x. The effect of this was to place the burden of casting and type verification on the developer. Without such verification, a single ArrayList
instance could be used to store a string, an integer, or a custom object. Only at runtime would the error be detected.
The System.Collections.Generic
namespace provides the generic versions of the classes in the System.Collections
namespace. If you are familiar with the non-generic classes, switching to the generic type is straightforward. For example, this code segment using the ArrayList
:
ArrayList primes = new ArrayList(); primes.Add(1); primes.Add(3); int pSum = (int)primes[0] + (int)primes[1]; primes.Add("test");
can be replaced with the generics version:
List<int> primes = new List<int>(); primes.Add(1); primes.Add(3); int pSum = primes[0] + primes[1]; primes.Add("text"); // will not compile
The declaration of List
includes a type parameter that tells the compiler what type of data the object may contain—int
in this case. The compiler then generates code that expects the specified type. For the developer, this eliminates the need for casting and type verification at runtime. From a memory usage and efficiency standpoint, it also eliminates boxing (conversion to objects) when primitives are stored in the collection.
As the following side-by-side comparison shows, the classes in the two namespaces share the same name with three exceptions: Hashtable
becomes Dictionary<>
, ArrayList
becomes List<>
, and SortedList
is renamed SortedDictionary<>
.
System.Collections | System.Collections.Generic |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(not applicable) |
|
The only other points to note regard IEnumerator
. Unlike the original version, the generics version inherits from IDisposable
and does not support the Reset
method.
Switching to the generics versions of the collection classes is primarily a matter of getting used to a new syntax, because the functionality provided by the generics and non-generics classes is virtually identical. To demonstrate, here are two examples. The first uses the Hashtable
to store and retrieve instances of the Chair
class (defined in Listing 4-5); the second example performs the same functions but uses the Dictionary
class—the generics version of the Hashtable
.
This segment consists of a Hashtable
declaration and two methods: one to store a Chair
object in the table and the other to retrieve an object based on a given key value.
// Example 1: Using Hashtable public Hashtable ht = new Hashtable(); // Store Chair object in table using a unique product identifier private void saveHT(double price, string ven, string sku) { if (!ht.ContainsKey(sku)) { ht.Add(sku, new Chair(price,ven,sku)); } } // Display vendor and price for a product identifier private void showChairHT(string sku) { if (ht.ContainsKey(key)) { if (ht[key] is Chair) // Prevent casting exception { Chair ch = (Chair)ht[sku]; Console.WriteLine(ch.MyVen + " " + ch.MyPr); } else { Console.WriteLine("Invalid Type: " + (ht[key].GetType().ToString())); } } }
Observe how data is retrieved from the Hashtable
. Because data is stored as an object, verification is required to ensure that the object being retrieved is a Chair
type; casting is then used to access the members of the object. These steps are unnecessary when the type-safe Dictionary
class is used in place of the Hashtable
.
The Dictionary<K,V>
class accepts two type parameters that allow it to be strongly typed: K
is the key type and V
is the type of the value stored in the collection. In this example, the key is a string representing the unique product identifier, and the value stored in the Dictionary
is a Chair
type.
// Example 2: Using Generics Dictionary to replace Hashtable // Dictionary accepts string as key and Chair as data type Dictionary<string,Chair> htgen = new Dictionary<string,Chair>(); // private void saveGen(double price, string ven, string sku) { if (!htgen.ContainsKey(sku)) { htgen.Add(sku, new Chair(price,ven,sku)); } } private void showChairGen(string sku) { if (htgen.ContainsKey(key)) { Chair ch = htgen[sku]; // No casting required Console.WriteLine(ch.MyVen + " " + ch.MyPr); } }
The important advantage of generics is illustrated in the showChairGen
method. It has no need to check the type of the stored object or perform casting.
In the long run, the new generic collection classes will render the classes in the System.Collections
namespace obsolete. For that reason, new code development should use the generic classes where possible.
In .NET, serialization refers to the process of converting an object or collection of objects into a format suitable for streaming across a network—a Web Service, for example—or storing in memory, a file, or a database. Deserialization is the reverse process that takes the serialized stream and converts it back into its original object(s).
.NET support three primary types of serialization:
Binary. Uses the BinaryFormatter
class to serialize a type into a binary stream.
SOAP. Uses the SoapFormatter
class to serialize a type into XML formatted according to SOAP (Simple Object Access Protocol) standards.
XML. Uses the XmlSerializer
class to serialize a type into basic XML (described in Chapter 10, “Working with XML in .NET”). Web Services uses this type of serialization.
Serialization is used primarily for two tasks: to implement Web Services and to store (persist) collections of objects to a medium from which they can be later resurrected. Web Services and their use of XML serialization are discussed in Chapter 18, “XML Web Services.” This section focuses on how to use binary serialization to store and retrieve objects. The examples use File I/O (see Chapter 5) methods to read and write to a file, which should be easily understood within the context of their usage.
The BinaryFormatter
object that performs binary serialization is found in the System.Runtime.Serialization.Formatters.Binary
namespace. It performs serialization and deserialization using the Serialize
and Deserialize
methods, respectively, which it inherits from the IFormatter
interface.
Listing 4-9 provides an example of binary serialization using simple class members. A hash table is created and populated with two Chair
objects. Next, a FileStream
object is instantiated that points to a file on the local drive where the serialized output is stored. A BinaryFormatter
is then created, and its Serialize
method is used to serialize the hash table's contents to a file. To confirm the process, the hash table is cleared and the BinaryFormatter
object is used to deserialize the contents of the file into the hash table. Finally, one of the members from a restored object in the hash table is printed—verifying that the original contents have been restored.
Example 4-9. Serialization of a Hashtable
using System; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.IO; // Store Chair objects in a Hashtable Hashtable ht = new Hashtable(); // Chair and Upholstery must have [Serializable] attribute Chair ch = new Chair(100.00D, "Broyhill", "10-09"); ch.myUpholstery = new Upholstery("Cotton"); ht.Add("10-09", ch); // Add second item to table ch = new Chair(200.00D, "Lane", "11-19"); ch.myUpholstery = new Upholstery("Silk"); ht.Add("11-19", ch); // (1) Serialize // Create a new file; if file exits it is overwritten FileStream fs= new FileStream("c:\chairs.dat", FileMode.Create); BinaryFormatter bf= new BinaryFormatter(); bf.Serialize(fs,ht); fs.Close(); // (2) Deserialize binary file into a Hashtable of objects ht.Clear(); // Clear hash table. fs = new FileStream("c:\chairs.dat", FileMode.Open); ht = (Hashtable) bf.Deserialize(fs); // Confirm objects properly recreated ch = (Chair)ht["11-19"]; Console.WriteLine(ch.myUpholstery.Fabric); // "Silk" fs.Close();
Observe the following key points:
The serialization and IO namespaces should be declared.
The Chair
and Upholstery
classes must have the [Serializable]
attribute; otherwise, a runtime error occurs when Serialize()
is executed.
Serialization creates an object graph that includes references from one object to another. The result is a deep copy of the objects. In this example, the myUpholstery
field of Chair
is set to an instance of the Upholstery
class before it is serialized. Serialization stores a copy of the object—rather than a reference. When deserialization occurs, the Upholstery
object is restored.
You can selectively exclude class members from being serialized by attaching the [NonSerialized]
attribute to them. For example, you can prevent the myUpholstery
field of the Chair
class from being serialized by including this:
[NonSerialized] public Upholstery myUpholstery;
The primary reason for marking a field NonSerialized
is that it may have no meaning where it is serialized. Because an object graph may be loaded onto a machine different from the one on which it was stored, types that are tied to system operations are the most likely candidates to be excluded. These include delegates, events, file handles, and threads.
.NET 2.0 introduced support for four binary serialization and deserialization events, as summarized in Table 4-9.
Table 4-9. Serialization and Deserialization Events
Event | Attribute | Description |
---|---|---|
|
| Occurs before objects are serialized. Event handler is called for each object to be serialized. |
|
| Occurs after objects are serialized. Event handler is called once for each object serialized. |
|
| Occurs before objects are deserialized. Event handler is called once for each object to be deserialized. |
|
| Occurs after objects have been deserialized. Event handler is called for each deserialized object. |
An event handler for these events is implemented in the object being serialized and must satisfy two requirements: the attribute associated with the event must be attached to the method, and the method must have this signature:
void <event name>(StreamingContext context)
To illustrate, here is a method called after all objects have been deserialized. The binary formatter iterates the list of objects in the order they were deserialized and calls each object's OnDeserialized
method. This example uses the event handler to selectively update a field in the object. A more common use is to assign values to fields that were not serialized.
public class Chair { // other code here [OnDeserialized] void OnDeserialized(StreamingContext context) { // Edit vendor name after object is created if (MyVen == "Lane") MyVen = "Lane Bros."; } }
Note that more than one method can have the same event attribute, and that more than one attribute can be assigned to a method—although the latter is rarely practical.
Suppose the Chair
class from the preceding examples is redesigned. A field could be added or deleted, for example. What happens if one then attempts to deserialize objects in the old format to the new format? It's not an uncommon problem, and .NET offers some solutions.
If a field is deleted, the binary formatter simply ignores the extra data in the deserialized stream. However, if the formatter detects a new field in the target object, it throws an exception. To prevent this, an [OptionalField]
attribute can be attached to the new field(s). Continuing the previous example, let's add a field to Chair
that designates the type of wood finish:
[OptionalField] private string finish;
The presence of the attribute causes the formatter to assign a default null
value to the finish field, and no exception is thrown. The application may also take advantage of the deserialized event to assign a value to the new field:
void OnDeserialized(StreamingContext context) { if (MyVen == "Lane") finish = "Oak"; else finish = "Cherry"; }
Memory allocation and deallocation have always been the bane of developers. Even the most experienced C++ and COM programmer is faced with memory leaks and attempts to access nonexistent objects that either never existed or have already been destroyed. In an effort to remove these responsibilities from the programmer, .NET implements a memory management system that features a managed heap and automatic Garbage Collection.
Recall from Chapter 2, “C# Language Fundamentals,” that the managed heap is a pre-allocated area of memory that .NET uses to store reference types and data. Each time an instance of a class is created, it receives memory from the heap. This is a faster and cleaner solution than programming environments that rely on the operating system to handle memory allocation.
Allocating memory from the stack is straightforward: A pointer keeps track of the next free memory address and allocates memory from the top of the heap. The important thing to note about the allocated memory is that it is always contiguous. There is no fragmentation or complex overhead to keep track of free memory blocks. Of course, at some point the heap is exhausted and unused space must be recovered. This is where the .NET automatic Garbage Collection comes into play.
Each time a managed object is created, .NET keeps track of it in a tree-like graph of nodes that associates each object with the object that created or uses it. In addition, each time another client references an object or a reference is assigned to a variable, the graph is updated. At the top of this graph is a list of roots, or parts of the application that exist as long at the program is running (see Figure 4-9). These include static variables, CPU registers, and any local or parameter variables that refer to objects on the managed heap. These serve as the starting point from which the .NET Framework uses a reference-tracing technique to remove objects from the heap and reclaim memory.
The Garbage Collection process begins when some memory threshold is reached. At this point, the Garbage Collector (GC) searches through the graph of objects and marks those that are “reachable.” These are kept alive while the unreachable ones are considered to be garbage. The next step is to remove the unreferenced objects (garbage) and compact the heap memory. This is a complicated process because the collector must deal with the twin tasks of updating all old references to the new object addresses and ensuring that the state of the heap is not altered as Garbage Collection takes place.
The details of Garbage Collection are not as important to the programmer as the fact that it is a nondeterministic (occurs unpredictably) event that deals with managed resources only. This leaves the programmer facing two problems: how to dispose of unmanaged resources such as files or network connections, and how to dispose of them in a timely manner. The solution to the first problem is to implement a method named Finalize
that manages object cleanup; the second is solved by adding a Dispose
method that can be called to release resources before Garbage Collection occurs. As we will see, these two methods do not operate autonomously. Proper object termination requires a solution that coordinates the actions of both methods.
Garbage Collection typically occurs when the CLR detects that some memory threshold has been reached. However, there is a static method GC.Collect
that can be called to trigger Garbage Collection. It can be useful under controlled conditions while debugging and testing, but should not be used as part of an application.
Objects that contain a Finalize
method are treated differently during both object creation and Garbage Collection than those that do not contain a Finalize
method. When an object implementing a Finalize
method is created, space is allocated on the heap in the usual manner. In addition, a pointer to the object is placed in the finalization queue (see Figure 4-9). During Garbage Collection, the GC scans the finalization queue searching for pointers to objects that are no longer reachable. Those found are moved to the freachable queue. The objects referenced in this queue remain alive, so that a special background thread can scan the freachable queue and execute the Finalize
method on each referenced object. The memory for these objects is not released until Garbage Collection occurs again.
To implement Finalize
correctly, you should be aware of several issues:
Finalization degrades performance due to the increased overhead. Only use it when the object holds resources not managed by the CLR.
Objects may be placed in the freachable queue in any order. Therefore, your Finalize
code should not reference other objects that use finalization, because they may have already been processed.
Call the base Finalize
method within your Finalize
method so it can perform any cleanup: base.Finalize().
Finalization code that fails to complete execution prevents the background thread from executing the Finalize
method of any other objects in the queue. Infinite loops or synchronization locks with infinite timeouts are always to be avoided, but are particularly deleterious when part of the cleanup code.
It turns out that you do not have to implement Finalize
directly. Instead, you can create a destructor and place the finalization code in it. The compiler converts the destructor code into a Finalize
method that provides exception handling, includes a call to the base class Finalize
, and contains the code entered into the destructor:
Public class Chair { public Chair() { } ~Chair() // Destructor { // finalization code } }
Note that an attempt to code both a destructor and Finalize
method results in a compiler error.
As it stands, this finalization approach suffers from its dependency on the GC to implement the Finalize
method whenever it chooses. Performance and scalability are adversely affected when expensive resources cannot be released when they are no longer needed. Fortunately, the CLR provides a way to notify an object to perform cleanup operations and make itself unavailable. This deterministic finalization relies on a public Dispose
method that a client is responsible for calling.
Although the Dispose
method can be implemented independently, the recommended convention is to use it as a member of the IDisposable
interface. This allows a client to take advantage of the fact that an object can be tested for the existence of an interface. Only if it detects IDisposable
does it attempt to call the Dispose
method. Listing 4-10 presents a general pattern for calling the Dispose
method.
Example 4-10. Pattern for Calling Dispose()
public class MyConnections: IDisposable { public void Dispose() { // code to dispose of resources base.Dispose(); // include call to base Dispose() } public void UseResources() { } } // Client code to call Dispose() class MyApp { public static void Main() { MyConnections connObj; connObj = new MyConnections(); try { connObj.UseResources(); } finally // Call dispose() if it exists { IDisposable testDisp; testDisp = connObj as IDisposable; if(testDisp != null) { testDisp.Dispose(); } } }
This code takes advantage of the finally block to ensure that Dispose
is called even if an exception occurs. Note that you can shorten this code by replacing the try/finally block with a using construct that generates the equivalent code:
Using(connObj) { connObj.UseResources() }
When Dispose
is executed, the object's unmanaged resources are released and the object is effectively disposed of. This raises a couple of questions: First, what happens if Dispose
is called after the resources are released? And second, if Finalize
is implemented, how do we prevent the GC from executing it since cleanup has already occurred?
The easiest way to handle calls to a disposed object's Dispose
method is to raise an exception. In fact, the ObjectDisposedException
exception is available for this purpose. To implement this, add a boolean property that is set to true when Dispose
is first called. On subsequent calls, the object checks this value and throws an exception if it is true.
Because there is no guarantee that a client will call Dispose
, Finalize
should also be implemented when resource cleanup is required. Typically, the same cleanup method is used by both, so there is no need for the GC to perform finalization if Dispose
has already been called. The solution is to execute the SuppressFinalize
method when Dispose
is called. This static method, which takes an object as a parameter, notifies the GC not to place the object on the freachable queue.
Listing 4-11 shows how these ideas are incorporated in actual code.
Example 4-11. Pattern for Implementing Dispose()
and Finalize()
public class MyConnections: IDisposable { private bool isDisposed = false; protected bool Disposed { get{ return isDisposed;} } public void Dispose() { if (isDisposed == false) { CleanUp(); IsDisposed = true; GC.SuppressFinalize(this); } } protected virtual void CleanUp() { // cleanup code here } ~MyConnections() // Destructor that creates Finalize() { CleanUp(); } public void UseResources() { // code to perform actions if(Disposed) { throw new ObjectDisposedException ("Object has been disposed of"); } } } // Inheriting class that implements its own cleanup public class DBConnections: MyConnections { protected override void CleanUp() { // implement cleanup here base.CleanUp(); } }
The key features of this code include the following:
A common method, CleanUp
, has been introduced and is called from both Dispose
and Finalize
. It is defined as protected and virtual, and contains no concrete code.
Classes that inherit from the base class MyConnections
are responsible for implementing the CleanUp
. As part of this, they must be sure to call the Cleanup
method of the base class. This ensures that cleanup code runs on all levels of the class hierarchy.
The read-only property Disposed
has been added and is checked before methods in the base class are executed.
In summary, the .NET Garbage Collection scheme is designed to allow programmers to focus on their application logic and not deal with details of memory allocation and deallocation. It works well as long as the objects are dealing with managed resources. However, when there are valuable unmanaged resources at stake, a deterministic method of freeing them is required. This section has shown how the Dispose
and Finalize
methods can be used in concert to manage this aspect of an object's life cycle.
This chapter has discussed how to work with objects. We've seen how to create them, manipulate them, clone them, group them in collections, and destroy them. The chapter began with a description of how to use a factory design pattern to create objects. It closed with a look at how object resources are released through automatic Garbage Collection and how this process can be enhanced programmatically through the use of the Dispose
and Finalize
methods. In between, the chapter examined how to make applications more robust with the use of intelligent exception handling, how to customize the System.Object
methods such as Equals
and ToString
to work with your own objects, how cloning can be used to make deep or shallow copies, and how to use the built-in classes available in the System.Collections
and System.Collections.Generic
namespaces.
As a by-product of this chapter, you should now have a much greater appreciation of the important role that interfaces play in application design. They represent the base product when constructing a class factory, and they allow you to clone (ICloneable
), sort (IComparer
), or enumerate (IEnumerable
) custom classes. Knowing that an object implements a particular interface gives you an immediate insight into the capabilities of the object.