Using C# to Program Generic .NET Interfaces

By Robert J. Oberg

Date: Feb 15, 2002

Article is provided courtesy of Prentice Hall.

Return to the article


C# as a language is elegant and powerful, but to fully use its capabilities you need to understand how it works within the .NET Framework. This article by Robert Oberg shows you how to make your C# classes work effectively with generic .NET interfaces.

C# as a language is elegant and powerful, but to fully use its capabilities, you need to understand how it works within the .NET Framework. This article shows you how to make your C# classes work effectively with generic .NET interfaces.

This article is based on the books Introduction to C# Using .NET (Prentice Hall PTR, 2001, ISBN: 0130418013), by Robert J. Oberg; and Application Development Using C# and .NET (Prentice Hall PTR, 2001, ISBN: 013093383X), by Michael Stiefel and Robert J. Oberg. These books are part of The Integrated .NET Series from Object Innovations and Prentice Hall PTR.

You can download a full-length version of this article (Chapter 18 from Introduction to C# Using .NET) and sample code here.

Make Your C# Classes Work with the .NET Framework

The .NET Framework is a powerful class library. As such, there is a great deal of functionality (more than 2500 classes!) that you can call from your code. The basic organization of a library that supplies pre-written code that you can call is familiar from many programming environments. The fact that the .NET Framework is a class library means that the functions are grouped together into meaningful classes, and you can customize the behavior of the supplied classes by inheritance, creating your own derived classes.

To fully understand—and exploit—the .NET Framework, you need to understand another key concept. Not only does your code call the Framework, but also the Framework calls your code. Your program can be viewed as the middle layer of a sandwich, as illustrated in Figure 1.

Figure 1 The .NET Framework sandwich.

There are several ways to make your classes plug into the architecture of the .NET Framework and thus be "good .NET citizens."

Every class in C# inherits from the root class System.Object (in C#, you can use the keyword object for this root class). The class object has a basic set of methods:

If you are used to a language such as Smalltalk, the set of behaviors specified in object may seem quite limited. Smalltalk, which introduced the concept of a class hierarchy rooted in a common base class, has a very rich set of methods defined in its Object class. I counted 38 methods!

NOTE

The methods of Smalltalk's Object class are described in Chapters 6 and 14 of Smalltalk-80: The Language and its Implementation (Addison-Wesley, 1989, ISBN: 0201136880), by Adele Goldberg and David Robson.

These additional methods support features such as comparing objects and copying objects. The .NET Framework class library has similar methods and many more. But rather than putting them all in a common root class, .NET defines a number of standard interfaces that classes can optionally support. This kind of organization, which is also present in Microsoft's Component Object Model (COM) and in Java, is very flexible.

In this article, we will show you how to use interfaces to perform the common operations of copying and comparing.

Copy Semantics and ICloneable

Many times in programming, you have occasion to make a copy of a variable. When you program in C#, it is very important that you have a firm understanding of exactly what happens when you copy various kinds of data. In this section, we will look carefully at the copy semantics of C#. We will compare reference copy, shallow memberwise copy, and deep copy. You will see that by implementing the ICloneable interface in your class, you can enable deep copy.

Reference Copy

Recall that C# has value types and reference types. A value type contains all its own data, whereas a reference type refers to data stored somewhere else. If a reference variable gets copied to another reference variable, both will refer to the same object. If the object referenced by the second variable is changed, the first variable will also reflect the new value.

As an example, consider what happens when you copy an ArrayList, which is a reference type. Consider the program ReferenceCopy. (The download file contains this program and the others mentioned in the article.) This program makes a copy of a Course. The Course class consists of a title and a collection of students.

// Course.cs

using System;
using System.Collections;

public class Course
{
  public string Title;
  public ArrayList Roster;
  public Course(string title)
  {
   Title = title;
   Roster = new ArrayList();
  }
  public void AddStudent(string name)
  {
   Roster.Add(name);
  }
  public void Show(string caption)
  {
   Console.WriteLine("-----{0}-----", caption);
   Console.WriteLine("Course : {0} with {1} students", 
     Title, Roster.Count);
   foreach (string name in Roster)
   {
     Console.WriteLine(name);
   }
  }
}

The test program constructs a Course instance c1 and then makes a copy c2 by the straight assignment c2 = c1. Now we get two references to the same object, and if we make any change through the first reference, we will see the same change through the second reference. The test program illustrates such an assignment.

// ReferenceCopy.cs

using System;
using System.Collections;

public class ReferenceCopy
{
  private static Course c1, c2;
  public static void Main()
  {
   Console.WriteLine("Copy is done via c2 = c1");
   InitializeCourse();
   c1.Show("original");
   c2 = c1;
   c2.Show("copy");
   c2.Title = ".NET Programming";
   c2.AddStudent("Charlie");
   c2.Show("copy with changed title and new student");
   c1.Show("original");
  }
  private static void InitializeCourse()
  {
   c1 = new Course("Intro to C#");
   c1.AddStudent("John");
   c1.AddStudent("Mary");
  }
}

We initialize with the title "Intro to C#" and two students. We make the assignment c2 = c1 and then change the title and add another student for c2. We then show both c1 and c2, and we see that both reflect both of these changes. Here is the output:

Copy is done via c2 = c1
-----original-----
Course : Intro to C# with 2 students
John
Mary
-----copy-----
Course : Intro to C# with 2 students
John
Mary
-----copy with changed title and new student-----
Course : .NET Programming with 3 students
John
Mary
Charlie
-----original-----
Course : .NET Programming with 3 students
John
Mary
Charlie

Shallow Copy and Deep Copy

A struct in C# automatically implements a memberwise copy, sometimes known as a shallow copy. The object root class has a protected method, MemberwiseClone, which will perform a memberwise copy of members of a class.

If one or more members of a class are of a reference type, this memberwise copy may not be good enough. The result will be two references to the same data, not two independent copies of the data. To actually copy the data itself and not merely the references, you will need to perform a deep copy. Deep copy can be provided at either the language level or the library level. In C++, deep copy is provided at the language level through a copy constructor. In C#, deep copy is provided by the .NET Framework through a special interface, ICloneable, which you can implement in your classes in order to enable them to perform deep copy.

MemberwiseClone

The next way we will illustrate doing a copy is with a memberwise copy, which can be accomplished using the MemberwiseClone method of object. Because this method is protected, we cannot call it directly from outside our Course class. Instead, in Course, we define a method, ShallowCopy, which is implemented using MemberwiseClone. Our sample solution is ShallowCopy.

// Course.cs

using System;
using System.Collections;

public class Course 
{
  ...
  public Course ShallowCopy()
  {
   return (Course) this.MemberwiseClone();
  }
  ...
}

Here is the modified test program, which calls the ShallowCopy method. Again, we change the title and a student in the second copy.

// ShallowCopy.cs
...

  public static void Main()
  {
   Console.WriteLine(
     "Copy is done via MemberwiseClone");
   InitializeCourse();
   c1.Show("original");
   c2 = c1.ShallowCopy();
   c2.Show("copy");
   c2.Title = ".NET Programming";
   c2.AddStudent("Charlie");
   c2.Show("copy with changed title and new student");
   c1.Show("original");
  }
  ...

Here is the output. Now the Title field has its own independent copy, but the Roster collection is just copied by reference, so each copy refers to the same collection of students.

Copy is done via c2 = c1.ShallowCopy()
...
-----copy with changed title and new student-----
Course : .NET Programming with 3 students
John
Mary
Charlie
-----original-----
Course : Intro to C# with 3 students
John
Mary
Charlie

Using ICloneable

The final version of our program relies on the fact that our Course class supports the ICloneable interface, and implements the Clone method. To clone the Roster collection, we use the fact that ArrayList implements the ICloneable interface, as discussed earlier in the chapter. Note that the Clone method returns an object, so we must cast to ArrayList before assigning to the Roster field. Our sample solution is DeepCopy.

// Course.cs

using System;
using System.Collections;

public class Course : ICloneable
{
  ...
  public object Clone()
  {
   Course course = new Course(Title);
   course.Roster = (ArrayList) Roster.Clone();
   return course;
  }
}

Here is the third version of the test program, which calls the Clone method. Again, we change the title and a student in the second copy.

// DeepCopy.cs
...

  public static void Main()
  {
   Console.WriteLine(
     "Copy is done via c2 = c1.Clone()");
   InitializeCourse();
   c1.Show("original");
   c2 = (Course) c1.Clone();
   c2.Show("copy");
   c2.Title = ".NET Programming";
   c2.AddStudent("Charlie");
   c2.Show("copy with changed title and new student");
   c1.Show("original");
  }
  ...

Here is the output from the third version of the program. Now we have completely independent instances of Course. Each has its own title and set of students.

Copy is done via c2 = c1.Clone()
...
-----copy with changed title and new student-----
Course : .NET Programming with 3 students
John
Mary
Charlie
-----original-----
Course : Intro to C# with 2 students
John
Mary

Comparing Objects

We have quite exhaustively studied issues involved in copying objects. We will now examine the issues involved in comparing objects. In order to compare objects, the .NET Framework uses the interface IComparable. In this section, we will examine the use of the interface IComparable through an example of sorting an array.

Sorting an Array

The System.Array class provides a static method, Sort, which can be used for sorting an array. The program ArrayName\Step0 illustrates an attempt to apply this Sort method to an array of Name objects, where the Name class simply encapsulates a string through a read-only property Text.

// ArrayName.cs - Step 0

using System;

public class Name
{
  private string text;
  public Name(string text)
  {
   this.text = text;
  }
  public string Text
  {
   get
   {
     return text;
   }
  }
}
public class ArrayName
{
  public static int Main(string[] args)
  {
   Name[] array = new Name[10];
   array[0] = new Name("Michael");
   array[1] = new Name("Charlie");
   array[2] = new Name("Peter");
   array[3] = new Name("Dana");
   array[4] = new Name("Bob");
   Array.Sort(array);
   return 0;
  }
}

Anatomy of Array.Sort

What do you suppose will happen when you run this program? Here is the result:

Exception occurred: System.ArgumentException: [icc]
At least one object must implement IComparable.

The static method Sort of the Array class relies on some functionality of the objects in the array. The array objects must implement IComparable.

Suppose we don't know whether the objects in our array support IComparable. Is there a way we can find out programmatically at runtime?

Using the is Operator

There are in fact three ways to dynamically check to see whether an interface is supported:

In this case, the most direct solution is to use the is operator (which is applied to an object, not to a class). See ArrayName\Step1.

// ArrayName.cs - Step 1

...
public class ArrayName
{
  public static int Main(string[] args)
  {
   Name[] array = new Name[10];
   array[0] = new Name("Michael");
   array[1] = new Name("Charlie");
   array[2] = new Name("Peter");
   array[3] = new Name("Dana");
   array[4] = new Name("Bob");
   if (array[0] is IComparable)
     Array.Sort(array);
   else
     Console.WriteLine(
      "Name does not implement IComparable");
   return 0;
  }
}

Here is the output from running the program. We're still not sorting the array, but at least we fail more gracefully.

Name does not implement IComparable

Use of Dynamic Type Checking

We can use dynamic-type checking of object references to make our programs more robust. We can degrade gracefully rather than fail completely.

For example, in our array program, the desired outcome is to print the array elements in sorted order. We could check whether the objects in the array support IComparable, and if not, we could go ahead and print out the array elements in unsorted order, obtaining at least some functionality.

Implementing IComparable

Consulting the documentation for System, we find the following specification for IComparable:

public interface IComparable
{
    int CompareTo(object object);
}

We will implement IComparable in the class Name. See ArrayName\Step2. We also add a simple loop in Main to display the array elements after sorting.

// ArrayName.cs - Step 2

using System;

public class Name : IComparable
{
  private string text;
  public Name(string text)
  {
   this.text = text;
  }
  public string Text
  {
   get
   {
     return text;
   }
  }
  public int CompareTo(object obj)
  {
   string s1 = this.Text;
   string s2 = ((Name) obj).Text;
   return String.Compare(s1, s2);
  }
}
public class ArrayName
{
  public static int Main(string[] args)
  {
   ...
   foreach (Name name in array)
     Console.WriteLine(name);
   return 0;
  }
}

Incomplete Solution

If we run the above program, we do not exactly get the desired output:





Name
Name
Name
Name
Name

The first five lines of output are blank, and in place of the string in Name, we get the class name Name displayed. The unassigned elements of the array are null, and they compare successfully with real elements, always being less than a real element.

Complete Solution

We should test for null before displaying. The most straightforward way to correct the issue of the strings in Name not displaying is to use the Text property. A more interesting solution is to override the ToString method in our Name class. Here is the complete solution, in the directory ArrayName\Step3.

// ArrayName.cs - Step 3

using System;

public class Name : IComparable
{
  private string text;
  public Name(string text)
  {
   this.text = text;
  }
  public string Text
  {
   get
   {
     return text;
   }
  }
  public int CompareTo(object obj)
  {
   string s1 = this.Text;
   string s2 = ((Name) obj).Text;
   return String.Compare(s1, s2);
  }
  override public string ToString()
  {
   return text;
  }
}

public class ArrayName
{
  public static int Main(string[] args)
  {
   Name[] array = new Name[10];
   array[0] = new Name("Michael");
   array[1] = new Name("Charlie");
   array[2] = new Name("Peter");
   array[3] = new Name("Dana");
   array[4] = new Name("Bob");
   if (array[0] is IComparable)
     Array.Sort(array);
   else
     Console.WriteLine(
      "Name does not implement IComparable");
   foreach (Name name in array)
   {
     if (name != null)
      Console.WriteLine(name);
   }
   return 0;
  }
}

Here is the output:

Bob
Charlie
Dana
Michael
Peter

Compiler Warning

You may notice a compiler warning when you build this program. The compiler can tell that the IComparable interface is implemented by the class Name, so the runtime check is superfluous. The Step 4 solution shows a more general situation in which Name, in fact, does not implement the interface. Rather, the interface is implemented in a derived class SortableName, and there will be no compiler warning. The runtime check makes sense. You can examine this version of the program by downloading the sample chapter from the book.

Summary

This article showed you how to make your C# classes work effectively with generic .NET interfaces. We illustrated issues involving copying, which are dealt with in C#—not at the language level (as in C++ with the copy constructor), but rather through the .NET Framework and the ICloneable interface. We also looked at comparing objects, which is a requirement for sorting. The key interface here is IComparable. Once you understand the basic concept of working with generic interfaces in .NET, you should be able to make your program work with the many other interfaces defined by the .NET Framework, as required for your particular situation.