Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

IInterfaces Part 2 � Implementing IComparable and IComparer

0.00/5 (No votes)
6 Mar 2005 1  
Part 2 of carefully crafted examples that demonstrate the usefulness of implementing various interfaces.

Sample Image - IInterfaces_p2.jpg

Introduction

This is the second of the two parts. If you have not read part 1 yet, you are encouraged to do so. While part 1 focuses mostly on the IEnumerable and IEnumerator, and part 2 focuses primarily on IComparable and IComparer, this article is written to pick up where the first left off and assumes a familiarity with the project already.

Using the code

The layout of these code blocks does not reflect how they are represented in the source code, but rather they are laid in a fashion which follows how I approached each problem.

Example6.cs

What I really hate is disorganized fruit baskets. Internally, I know that FruitBasket stores each Fruit in an array. With the right interface, we should be able to sort our basket.

In FruitBasket, add a new method Sort.

        public void Sort()
        {
            Array.Sort( basket );
        }

Then place a call to this method in Main after we�ve added all our fruits, but before we cycle through the basket recovering the names. I�ve intentionally made it so that we only add two fruits in an incorrect ordering to later expose a bug.

        static void Main(string[] args)
        {
            FruitBasket fruitBasket = new FruitBasket();

            Console.WriteLine( "Adding a Banana" );
            fruitBasket.Add( new Banana() );
            Console.WriteLine( "Adding an Apple" );
            fruitBasket.Add( new Apple() );
            // Console.WriteLine( "Adding a Cantaloupe" );

            // fruitBasket.Add( new Cantaloupe() );


            Console.WriteLine( "" );

            Console.WriteLine( "Sorting" );
            fruitBasket.Sort();

            Console.WriteLine( "" );

            Console.WriteLine( "The basket is holding:" );
            foreach( Fruit fruit in fruitBasket )
            {
                Console.WriteLine( "  a(n) " + fruit.Name );
            }
        }

If we compile and execute, it should come as a little surprise that we crash with an exception:

An unhandled exception of type 'System.InvalidOperationException' occurred 
                                                        in mscorlib.dll

Additional information: Specified IComparer threw an exception.

Interesting, so Array.Sort uses an IComparer interface. Because we have an array of classes, and not a single dimensional array of strings, the default behavior of Array.Sort fails. The error returned is very misleading. IComparer is powerful and has some advanced capabilities. I suspect that internally some IComparable call wraps an IComparer call, and that is what really failed, but for our needs right now we actually need an IComparable interface.

Example7.cs

Because we derived Apple, Banana, and Cantaloupe from Fruit, we�ve saved ourselves some work. By implementing IComparable only in Fruit, our derived classes get this for free.

    public class Fruit : IComparable

Pressing tab creates the IComparable member CompareTo that we still need to author.

        #region IComparable Members

        public int CompareTo(object obj)
        {
            // TODO:  Add Fruit.CompareTo implementation

            return 0;
        }

        #endregion

MSDN documentation tells us that CompareTo �Compares the current instance with another object of the same type.� Initially this sounds more difficult than it really is. We are passed some object obj, and we are asked to compare it with the current instance. To do so, we recast obj as a Fruit. You can either use Fruit fruit = obj as Fruit; and check for a null should the casting fail, or just cast it Fruit fruit = (Fruit) obj; and let an exception fire if it should fail. Throwing an exception seems like the most sensible thing for us to do in this article but how this is implemented depends completely upon each situation.

        public int CompareTo(object obj)
        {
            Fruit fruit = (Fruit) obj;
            return( this.Name.CompareTo( fruit.Name ));
        }

Because we have defined the Name member as a string, we can take advantage of that situation and use strings CompareTo that we�ve inherited from that type.

Compile and run and although we are adding Banana and then Apple, the result is correctly sorted and displayed as Apple and Banana. Now, add a Cantaloupe by uncommenting the two lines in Main. Compile and run again and you�ll see that we crash.

An unhandled exception of type 'System.NullReferenceException' occurred 
                                                          in Example7.exe

Additional information: Object reference not set to an instance of an object.

Does this mean that we can�t handle more than two different fruits in our basket? If you break the code from the exception handler, you can see that we crash when trying to write the first name. Upon further inspection, the first element of the basket array is an undefined value. This is why we crash, the fruit we are currently trying to evaluate is a null; Array.Sort sorts everything, including the unassigned elements. What�s worse is that null gets sorted before something. Personally, I don�t understand why the framework was designed like this, but fortunately this is easily fixed. Sort has some eight different overloads; number six has arguments for a beginning index and a length. We know that our index starts at 0, and all so conveniently, we�ve been keeping track of how many elements we�ve added to the array with count.

        public void Sort()
        {
            Array.Sort( basket, 0, count );
        }

A quick change and we are no longer limited to baskets of fruit in powers of two.

Example8.cs

So you can iterate through the array and you can sort by the fruit�s name, what do you do if you want to sort by some other metric like mass or color? This is where IComparer comes into play.

    public class Fruit : IComparable
    {
        public enum SortMetric
        {
            Name,
            Mass,
            Color
        };
...
        public virtual float Mass
        {
            get
            {
                return( float.NaN );
            }
        }

        public virtual string Color
        {
            get
            {
                return( null );
            }
        }
...
        public class Apple : Fruit
        {
...
        public override float Mass
        {
            get
            {
                return( 119.0f );
            }
        }

        public override string Color
        {
            get
            {
                return( "Red" );
            }
        }
    }

    public class Banana : Fruit
    {
...
        public override float Mass
        {
            get
            {
                return( 92.0f );
            }
        }

        public override string Color
        {
            get
            {
                return( "Yellow" );
            }
        }
    }

    public class Cantaloupe : Fruit
    {
...
        public override float Mass
        {
            get
            {
                return( 380.0f );
            }
        }

        public override string Color
        {
            get
            {
                return( "Brown" );
            }
        }
    }

For brevity I�ve only listed the new content and tried to give some context as to where these new members go. The ellipses aren�t part of the code, but indicate that something has been omitted.

In a fashion similar to IEnumerable and IEnumerator, we define IComparable and IComparer in different classes, but in this case, IComparer classes are nested inside Fruit. You will see later how this makes things easier for us, but it is also done for an organizational standpoint. Sorting by Mass and Color make sense only in the context of Fruit.

        #region IComparable Members

        public int CompareTo(object obj)
        {
            Fruit fruit = (Fruit) obj;
            return( this.Name.CompareTo( fruit.Name ));
        }
        #endregion

        class SortByMassClass : IComparer
        {
            #region IComparer Members

            public int Compare(object x, object y)
            {
                // TODO:  Add SortByMassClass.Compare implementation

                return 0;
            }
            #endregion
        }

        class SortByColorClass : IComparer
        {
            #region IComparer Members

            public int Compare(object x, object y)
            {
                // TODO:  Add SortByColorClass.Compare implementation

                return 0;
            }
            #endregion
        }
    }

Having already implemented CompareTo, Compare is not much more difficult. Compare as you can imagine �Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other.� Unlike CompareTo where we only cast one argument, now we cast two.

        public class SortByMassClass : IComparer
        {
            #region IComparer Members

            public int Compare(object x, object y)
            {
                Fruit fruitx = (Fruit) x;
                Fruit fruity = (Fruit) y;

                return( ((IComparable) fruitx.Mass).CompareTo( fruity.Mass ));
            }

            #endregion

        }

        public class SortByColorClass : IComparer
        {
            #region IComparer Members

            public int Compare(object x, object y)
            {
                Fruit fruitx = (Fruit) x;
                Fruit fruity = (Fruit) y;

                return( String.Compare( fruitx.Color, fruity.Color ));
            }

            #endregion

        }

In SortByMassClass, the most interesting part is the return. So that fruitx implements the CompareTo method, it is cast as IComparable. It is also wrapped with an additional set of parenthesis to limit the scope of that cast only to fruitx. We then allow the default float CompareTo method to perform the evaluation.

So that we may sort by these different methods, we overload Sort so that it can receive some sort of criteria.

        public void Sort( Fruit.SortMetric sortMetric )
        {
            switch( sortMetric )
            {
                case Fruit.SortMetric.Name:
                    Array.Sort( basket, 0, count );
                    break;
                case Fruit.SortMetric.Mass:
                    Array.Sort( basket, 0, count, 
                                   (IComparer) new Fruit.SortByMassClass());
                    break;
                case Fruit.SortMetric.Color:
                    Array.Sort( basket, 0, count,
                                  (IComparer) new Fruit.SortByColorClass());
                    break;
            }
        }

Array.Sort has several overloads that are expecting an IComparer class to handle the sort. Recall the crash in Example6.cs. Array.Sort, when called without an IComparer class is providing a default one of its own. This is why when it fails to sort we get some cryptic error message about IComparer. By providing our own IComparer class, we can now sort by any property that we can imagine and evaluate.

Now sorting in by Mass or Color is as simple as calling Sort with the appropriate enum.

        Console.WriteLine( "" );

        Console.WriteLine( "Sorting by Mass" );
        fruitBasket.Sort( Fruit.SortMetric.Mass );

        Console.WriteLine( "" );

        Console.WriteLine( "The basket is holding:" );
        foreach( Fruit fruit in fruitBasket )
        {
            Console.WriteLine( "  a(n) " + fruit.Name + 
                             " w/ Mass = " + fruit.Mass );
        }

        Console.WriteLine( "" );

        Console.WriteLine( "Sorting by Color" );
        fruitBasket.Sort( Fruit.SortMetric.Color );

        Console.WriteLine( "" );

        Console.WriteLine( "The basket is holding:" );
        foreach( Fruit fruit in fruitBasket )
        {
            Console.WriteLine( "  a(n) " + fruit.Name + " w/ Color = " 
                                                 + fruit.Color );
        }

It works, but declaring things are still a little sloppy in the Sort method.

Example9.cs

Because we are calling the Sort routine deep in another class, we can get away with the code being a little ugly. At the level we actually make the calls, the actual Array.Sort is embedded in a switch/case structure and won�t be needed to be typed multiple times. Unfortunately, this will not always be the case. We can use static accessors to instantiate the classes and to tidy things up. In FruitBasket adds these two properties.

        internal static IComparer SortByMass
        {
            get
            {
                return( (IComparer) new Fruit.SortByMassClass());
            }
        }

        internal static IComparer SortByColor
        {
            get
            {
                return( (IComparer) new Fruit.SortByColorClass());
            }
        }

Now in Sort, where the Array.Sort call is actually made, you can simply refer to the property.

        case Fruit.SortMetric.Mass:
            Array.Sort( basket, 0, count, SortByMass );
            break;
        case Fruit.SortMetric.Color:
            Array.Sort( basket, 0, count, SortByColor );
            break;

As an added benefit, because the interface is cast in the property, we�ve significantly cleaned up the calls made in Sort, making the code far more readable and manageable.

Example10.cs

While this has been a thorough, highly encompassing overview concerning IEnumerable, IEnumerator, IComparable, and IComparer, there�s yet more you can and should do.

    public class FruitBasket : IEnumerable
    {
        Fruit[] basket = new Fruit[1];
        int count = 0;
        int revision = 0;

        internal Fruit this[int index]
        {
            get
            {
                CheckIndex( index );
                return( basket[index] );
            }
            set
            {
                CheckIndex( index );
                basket[index] = value;
                revision++;
            }
        }
...
        internal void CheckIndex( int index )
        {
            if( index >= count )
                throw new ArgumentOutOfRangeException( 
                                 @"Index value out of range" );
        }
...
    public class FruitBasketEnumerator : IEnumerator
    {
        FruitBasket fruitBasket;
        int index;
        int revision;

        #region IEnumerator Members

        public void Reset()
        {
            index = -1;
            revision = fruitBasket.Revision;
        }

        public object Current
        {
            get
            {
                if( revision != fruitBasket.Revision )
                    throw new InvalidOperationException( 
                            @"Collection modified while enumerating." );
                return( fruitBasket[index] );
            }
        }

In an effort to make this more thread safe, a revision counter is maintained in both FruitBasket and FruitBasketEnumerator. Should the revision values differ, then you can no longer be sure that you have valid data.

Safeguards were also put in place to make sure that when either writing to or reading from the array that the index is in a legitimate range. Because that range is limited by count, you can�t use the property to add Fruit, you can only do that with the Add method, but it should be possible to change an Apple into a Banana in this way.

Conclusion

With a little work, implementing interfaces in your code can go a long ways to adding additional capabilities. In this particular case, I knew what I wanted to implement foreach and Array.Sort, but implementing an interface opens up the possibility that something I hadn�t considered needing might already be able to interact with my classes.

History

  • Version - 6 March 2005

    Original article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here