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( "" );
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)
{
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)
{
return 0;
}
#endregion
}
class SortByColorClass : IComparer
{
#region IComparer Members
public int Compare(object x, object y)
{
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.