C# Canonical Forms—Type-Safe Forms


Jump to: navigation, search
Visual C# Tutorials
C# Tutorials

C# Canonical Forms

© 2006 Weldon W. Nash, III

Implement Type-Safe Forms of Interface Members and Derived Methods

I already covered this topic with respect to reference types in the "Prefer Type Safety at All Times" section. Most of those same points are applicable to value types, along with some added efficiency considerations. These efficiency problems stem from explicit conversion operations from value types to reference types, and vice versa. As you know, these conversions produce hidden boxing and unboxing operations in the generated IL code. Boxing operations can easily kill your efficiency in many situations. The points made previously about how type-safe versions of the enumeration methods help the C# compiler create much more efficient code in a foreach loop apply tenfold to value types. That is because boxing operations from conversions to and from value types take much more processor time when compared to a typecast of a reference type, which is relatively quick.

You’ve already seen how the ComplexNumber value type implements an interface—in this case, IComparable. That is because you still want value types to be sortable if they’re stored within a container. You’ll notice that core types within the CLR, such as System.Int32, also support interfaces such as IComparable. However, from an efficiency standpoint, you don’t want to box a value type each time you want to compare it to another. In fact, as it is currently written, the following code boxes both values:

public void Main()
{
 
   ComplexNumber num1 = 
      new ComplexNumber( 1, 3 );
   ComplexNumber num2 = 
      new ComplexNumber( 1, 2 );
 
   int result = 
      ((IComparable)num1).CompareTo( num2 );
}

Can you see both of the boxing operations? As was shown in the previous section, the num1 instance must be boxed in order to acquire a reference to the IComparable interface on it. Secondly, since CompareTo() accepts a reference of type System.Object, the num2 instance must be boxed. This is terrible for efficiency. Technically, I didn’t have to box num1 in order to call through IComparable. However, if the previous ComplexNumber example had implemented the IComparable interface explicitly, I would have had no choice.

To solve this problem, you want to implement a type-safe version of the CompareTo method, while at the same time implementing the IComparable.CompareTo method. Using this technique, the comparison call in the previous code will incur absolutely no boxing operations. Let’s look at how to modify the ComplexNumber struct to do this:

using System;
 
public struct ComplexNumber : 
   IComparable,
   IComparable<ComplexNumber>,
   IEquatable<ComplexNumber>
{
   public ComplexNumber
      ( double real, double imaginary ) {
      this.real = real;
      this.imaginary = imaginary;
   }
   public bool Equals( ComplexNumber other ) {
      return (this.real == other.real) &&
             (this.imaginary == other.imaginary);
   }
 
   public override bool Equals( object other ) {
      bool result = false;
      if( other is ComplexNumber ) {
         ComplexNumber that = 
            (ComplexNumber) other ;
 
         result = Equals( that );
      }
 
      return result;
   }
 
   public override int GetHashCode() {
      return (int) this.Magnitude;
   }
 
   public static bool operator ==
         ( ComplexNumber num1,
           ComplexNumber num2 ) {
      return num1.Equals(num2);
   }
 
   public static bool operator !=
         ( ComplexNumber num1,
          ComplexNumber num2 ) {
      return !num1.Equals(num2);
   }
 
   public int CompareTo( ComplexNumber that ) {
      int result;
      if( Equals(that) ) {
         result = 0;
      } else if( this.Magnitude > 
                 that.Magnitude ) {
         result = 1;
      } else {
         result = -1;
      }
 
      return result;
   }
 
   int IComparable.CompareTo( object other ) {
      if( !(other is ComplexNumber) ) {
         throw new ArgumentException
            ( "Bad Comparison!" );
      }
 
      return CompareTo( (ComplexNumber) other );
   }
 
   public double Magnitude {
      get {
         return Math.Sqrt
            ( Math.Pow(this.real, 2) +
              Math.Pow(this.imaginary, 2) );
      }
   }
 
   // Other methods removed for clarity
 
   private readonly double real;
   private readonly double imaginary;
}
 
public sealed class EntryPoint
{
   static void Main()
   {
      ComplexNumber num1 = 
         new ComplexNumber( 1, 3 );
      ComplexNumber num2 = 
         new ComplexNumber( 1, 2 );
 
      int result = num1.CompareTo( num2 );
 
      // Now, try the type-generic version
      result = 
         ((IComparable)num1).CompareTo( num2 );
 
   }
}

After the modifications, the first call to CompareTo() in Main() will incur no boxing operations. You’ll also notice that I went one step further and implemented the IComparable.CompareTo method explicitly in order to make it harder to call the typeless version of CompareTo() inadvertently without first explicitly casting the value instance to a reference of type IComparable. For good measure, the Main method demonstrates how to call the typeless version of CompareTo(). Now, the idea is that clients who use the ComplexNumber value can write code in a natural-looking way and get the benefits of better performance. Clients who require going through the interface, such as some container types, can use the IComparable interface, albeit with some boxing. If you’re curious, go ahead and open up the compiled executable with the previous example code inside ILDASM and examine the Main() method. You’ll see that the first call to CompareTo() results in no superfluous boxing, whereas the second call to CompareTo() does, in fact, result in two boxing operations as expected.

As a general rule of thumb, you can apply this idiom to just about any value type’s methods that accept or return a boxed instance of the value type. So far, you’ve seen two such examples of the idiom in use. The first was while implementing Equals() for the ComplexNumber type, and the second was while implementing IComparable.CompareTo().


Previous_Page_.gif Next_Page_.gif

Share this page
  • del.icio.us
  • Facebook
  • Google+
  • StumbleUpon