C# Canonical Forms—Type-Safe Forms
| Visual C# Tutorials |
| C# Tutorials |
|
|
© 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().
|

