Chapter 4. Working with Objects in C#

Topics in This Chapter

  • Creating ObjectsLearn how to use a factory design pattern to create objects.

  • Exception HandlingEffective 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 MethodsFamiliarity 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 SerializationObjects 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.

Object Creation

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.

Factory with one factory class

Figure 4-1. Factory with one factory class

Factory with multiple factory classes

Figure 4-2. Factory with multiple factory classes

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.

Example: Creating Objects with Multiple Factories

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.

Exception Handling

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.

System.Exception Class

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.

.NET exception classes hierarchy

Figure 4-3. .NET exception classes hierarchy

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

HelpLink

string

Contains a URL that points to help documentation.

InnerException

Exception

Is set to null unless the exception occurs while a previous exception is being handled. A GetBaseException method can be used to list a chain of previous inner exceptions.

Message

string

The text describing the exception.

Source

string

Name of the assembly that generated the exception.

StackTrace

string

Contains the sequence of method names and signatures that were called prior to the exception. It is invaluable for debugging.

TargetSite

MethodBase

Provides details about the method that threw the exception. The property is an object of type MethodBase. It returns the name of the method in which the exception occurred. It also has a DeclaringType property that returns the name of the class containing the method.

HResult

Int32

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 HRESULT in the COM world of unmanaged code.

Writing Code to Handle Exceptions

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.

Code blocks used for exception handling

Figure 4-4. Code blocks used for exception handling

The try Block

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.

The catch 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.

The finally Block

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.

Example: Handling Common SystemException Exceptions

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

ex.Message

Attempted to divide by zero

ex.Source

zeroexcept (assembly name)

ex.TargetSite

Void Main()

ex.StackTrace

at MyApp.Main()

Core Recommendation

Core Recommendation

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.

How to Create a Custom Exception Class

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:

    1. A parameterless constructor to serve as the default.

    2. A constructor with one string parameter—usually the message.

    3. 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

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 1618.

Unhandled Exceptions in a Windows Forms Application

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.

Exception Handling Guidelines

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.

Implementing System.Object Methods in a Custom Class

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.”

ToString() to Describe an Object

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

Equals() to Compare Objects

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.

Steps in overriding Equals()

Figure 4-5. Steps in overriding Equals()

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;

Overriding GetHashCode

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.

Determining If References Point to the Same Object

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.

Cloning to Create a Copy of an Object

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;
Comparison of shallow and deep copy

Figure 4-6. Comparison of shallow and deep copy

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.

How to Create a Shallow Copy

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.

Working with .NET Collection Classes and Interfaces

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.

Collection Interfaces

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

ICollection

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.

IComparer

Exposes a method that is used to compare two objects and plays a vital role in allowing objects in a collection to be sorted.

IDictionary

Allows an object to represent its data as a collection of key-and-value pairs.

IDictionaryEnumerator

Permits iteration through the contents of an object that implements the IDictionary interface.

IEnumerator
IEnumerable

Supports a simple iteration through a collection. The iteration only supports reading of data in the collection.

IHashCodeProvider

Supplies a hash code for an object. It contains one method: GetHashCode(object obj)

IList

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.

System.Collections Interface diagram

Figure 4-7. System.Collections Interface diagram

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.

ICollection Interface

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

int Count

Property that returns the number of entries in the collection.

bool IsSynchronized

Property that indicates if access to the collection is thread-safe.

object SyncRoot

Property that returns an object that can be used to synchronize access to a collection by different threads.

void CopyTo( array, index)

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.

IHashCodeProvider Interface

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.

Using the IEnumerable and IEnumerator Interfaces to List the Contents of a Collection

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

IEnumerable.GetEnumerator()

Returns an IEnumerator object that is used to iterate through collection.

IEnumerator.Current

Property that returns the current value in a collection.

IEnumerator.MoveNext()

Advances the enumerator to the next item in the collection. Returns bool.

IEnumerator.Reset()

Sets the enumerator to its beginning position in the collection. This method is not included in the Generic namespace.

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.

Core Note

Core Note

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
   }
}

Iterators

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.

Core Note

Core Note

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<>.

Using the IComparable and IComparer Interfaces to Perform Sorting

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.

IComparable

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());

IComparer

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.

System.Collections Namespace

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.

Selected classes in System.Collections

Figure 4-8. Selected classes in System.Collections

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.

Stack and Queue

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

Push()

EnQueue()

Remove an item

Pop()

DeQueue()

Return the current item without removing it

Peek()

Peek()

Determine whether an item is in the collection

Includes()

Includes()

Constructors

Stack()

  • Empty stack with default capacity

Stack(ICollection)

  • Stack is filled with received collection

Stack(int)

  • Set stack to initial int capacity

Queue()

  • Default capacity and growth factor

Queue(ICollection)

  • Filled with received collection

Queue(int)

  • Set queue to initial int capacity

Queue(int, float)

  • Set initial capacity and growth factor

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.

ArrayList

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.

IList Interface

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

bool IsFixedSize

Indicates whether the collection has a fixed size. A fixed size prevents the addition or removal of items after the collection is created.

bool IsReadOnly

Items in a collection can be read but not modified.

int IndexOf(object)

Determines the index of a specific item in the collection.

int Add(object)

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.

void Clear()

Remove all items from a collection.

bool Contains(object)

Returns true if a collection contains an item with a specified value.

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.

Hashtable

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.

IDictionary Interface

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

bool IsFixedSize

Indicates whether IDictionary has a fixed size. A fixed size prevents the addition or removal of items after the collection is created.

bool IsReadOnly

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.

bool Contains

Returns true if a collection contains an element with a specified key.

IDictionaryEnumerator
   GetEnumerator ()

Returns an instance of the IDictionaryEnumerator type that is required for enumerating through a dictionary.

IDictionaryEnumerator Interface

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

DictionaryEntry Entry

The variable Entry is used to retrieve both the key and value when iterating through a collection.

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.

Create a Hashtable

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;}

List Keys in a Hashtable

The following iterates through the Keys property collection:

// List Keys
foreach (string invenKey in chairHash.Keys)
   { MessageBox.Show(invenKey); }

List Values in a Hashtable

These statements iterate through the Values in a hash table:

// List Values
foreach (Chair chairVal in chairHash.Values)
   { MessageBox.Show(chairVal.myVendor);}

List Keys and Values in a Hashtable

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.

Core Note

Core Note

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.

System.Collections.Generic Namespace

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.

Comparison of System.Collections and System.Collections.Generic Namespaces

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

Comparer

Comparer<T>

Hashtable

Dictionary<K,T>

ArrayList

List<T>

Queue

Queue<T>

SortedList

SortedDictionary<K,T>

Stack

Stack<T>

ICollection

ICollection<T>

IComparable

IComparable<T>

IComparer

IComparer<T>

IDictionary

IDictionary<K,T>

IEnumerable

IEnumerable<T>

IEnumerator

IEnumerator<T>

IKeyComparer

IKeyComparer<T>

IList

IList<T>

(not applicable)

LinkedList<T>

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.

An Example Using a Generics Collections Class

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.

Object Serialization

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:

  • BinaryUses the BinaryFormatter class to serialize a type into a binary stream.

  • SOAPUses the SoapFormatter class to serialize a type into XML formatted according to SOAP (Simple Object Access Protocol) standards.

  • XMLUses 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.

Binary Serialization

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.

Excluding Class Members from Serialization

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.

Core Note

Core Note

A class cannot inherit the Serializable attribute from a base class; it must explicitly include the attribute. On the other hand, a derived class can be made serializable only if its base class is serializable.

Binary Serialization Events

.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

OnSerializing

[Serializing]

Occurs before objects are serialized. Event handler is called for each object to be serialized.

OnSerialized

[Serialized]

Occurs after objects are serialized. Event handler is called once for each object serialized.

OnDeserializing

[Deserializing]

Occurs before objects are deserialized. Event handler is called once for each object to be deserialized.

OnDeserialized

[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.

Handling Version Changes to a Serialized Object

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";
}

Object Life Cycle Management

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.

.NET Garbage Collection

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.

.NET Garbage Collection process

Figure 4-9. .NET Garbage Collection process

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.

Core Note

Core Note

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.

Object Finalization

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.

IDisposable.Dispose()

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() }

Using Dispose and Finalize

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.

Summary

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.

Test Your Understanding

1:

What are the advantages of using a class factory to create objects?

2:

Which class should custom exceptions inherit from? Which constructors should be included?

3:

How does the default System.Object.Equals method determine if two objects are equal?

4:

Which interface(s) must a class implement in order to support the foreach statement?

5:

What is the main advantage of using generics?

6:

What is the purpose of implementing IDisposable on a class?

7:

Refer to the following code:

public class test: ICloneable
{
   public int Age;
   public string Name;
   public test(string myname)
   { Name = myname;    }
   public Object Clone()
   { return MemberwiseClone(); }
}
// Create instances of class
test myTest = new test("Joanna");
myTest.Age = 36;
test clone1 = (test) mytest.Clone();
test clone2 = myTest;

Indicate whether the following statements evaluate to true or false:

  1. Object.ReferenceEquals(myTest.Name, clone1.Name)
    
  2. Object.ReferenceEquals(myTest.Age, clone1.Age)
    
  3. myTest.Name = "Julie";
    Object.ReferenceEquals(myTest.Name, clone1.Name)
    
  4. Object.ReferenceEquals(myTest.Name, clone2.Name)
    

8:

How does implementing Finalize on an object affect its Garbage Collection?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset