Introduction
This is the first of two parts. While part 1 focuses mostly on the IEnumerable
and IEnumerator
, and part 2 focuses primarily on IComparable
and IComparer
, the articles are written to seamlessly flow from one to the other.
Although there are a few articles written that explain the use of interfaces, I found myself struggling to understand how to implement them. The distinction between what is necessary and what is auxiliary was often blurred to the point that I was confused about what was needed, and often times I ported code that just didn�t matter in my implementation.
As part of another DirectX project I�m working on, I implemented a class that is an array container of another type of class, that needed to be sortable, that needed to be iteratable with a foreach()
construct, and that needed to grow dynamically. While I may not stick with this design, when I�m done (I�m not sure it needs to be dynamic after all), it at long last forced me to crack the code that is interfaces.
Two days later, I rediscovered Arrays UNDOCUMENTED by Wesner Moise. I was brainstorming about an XML parser that I want to write that will probably require a lot of work in MSIL and I thought that reading up on how arrays are implemented might suggest if I�m dreaming an impossible dream. A quarter of the way into the article, Moise implements an array resize function that seemed like a much better design than what I had written.
public static Array Resize(Array array, int newSize)
{
Type type = array.Type;
Array newArray = Array.CreateInstance(type.GetElementType(), newSize);
Array.Copy(array, 0, newArray, 0, Math.Min(newArray.Length, newSize))1
return newArray;
}
Where my code had required casting of the returned array to make the resize work properly, surely using this code, with its clever use of Array.CreateInstance()
, that information would be intrinsic to the design.
Unfortunately, a number of errors in that small chunk of code make it unusable directly. I tracked down the bugs changing array.Type
to array.GetType()
and the 1
back into a ;
. While this new version is probably more efficient than my first draft, I still need to cast the result. I glanced through the comments and was going to mention the bugs I found, but others had already beat me to it� no one mentioned that you need to cast the result. I decided that, I could contribute a small example of the Resize()
code in use but I quickly realized that in order to make it useful, I needed to add other Interfaces until parts of it began to resemble the code I had written for my DirectX project. I set out to fix Resize()
, and instead found I had the basis of an article on interfaces.
With this project, I tried to provide a clean implementation of various interfaces, while at the same time exploring some of the powerful ways that the object-orientated nature of C# allows you to derive from and extend classes. It is as much an exploration of what I believed C# would allow me to do (and verified), as it is an expose on IEnumerable
, IEnumerator
, IComparable
and IComparer
.
Background
The difference between deriving from a class, class myClass : StringDictionary
and implementing an interface, class myClass : IEnumerable
seems subtle. One of my reference texts, A Programmer�s Introduction to C#, Second Edition by Eric Gunnerson, says that �The choice between class and interface should be fairly straightforward. Classes are only appropriate for �is-a� relationships (where the derived class is really an instance of the base class), and interfaces are appropriate for all others.�
Since StringDictionary
implements IEnumerable
, the distinction was not clear to me. I implemented a parameter value pair parser and derived it from StringDictionary
. I implemented a command-line argument parser and derived it from StringDictionary
. With StringDictionary
, I didn�t need anything else.
Then I created a one-dimensional array of moderately complex structures. None of the classes I had used in the past, StringDictionary
, StringCollection
, NameValueCollection
, etc., accurately described this mess I had created. If I wanted to use a foreach()
construct, then I needed to implement the IEnumerable
interface.
Finally the difference was clear to me. The parameter value pair parser and the command-line argument parser were string dictionaries. In fact, a lot of the data structures that I had been working with were easily described that way. Generally, I would have some type of key, and then there would be some sort of value associated with that key � or perhaps no value, but a null
value is still a value in this exercise, it�s just an unspecified value. This is why I was able to easily create new classes derived from StringDictionary
, and thereby automatically inheriting the richness associated with those classes.
Implementing an interface seems a step backwards by comparison, and in some ways it is. In fact, implementing IEnumerable
only has one public method, GetEnumerator
, and that�s something that you have to implement. Why would you possibly want to inherit an interface then? According to MSDN, the GetEnumerator
method <blink>�Returns an enumerator that can iterate through a collection.�</blink>. Although it won�t actually be blinking for you (does anyone else miss the good ole days of Netscape 1.1n?), that subtle sentence from MSDN summarizes the actual reason we implement interfaces.
Recall that, part of the reason I started using StringDictionary
derived classes earlier was the ease in which I could manage those resources. I could add to the dictionary, search for a value based on a key, and/or use a foreach()
construct to iterate through the collection. What�s more, StringDictionary
derived classes can do that because they too implement IEnumerable
. Implementing an interface isn�t so much about inheriting cool methods that make your life easier, it�s about broadcasting that you�ve implemented cool methods that allow other functions and methods in other classes to work with your code to make your life easier.
An interface is a contract. It says that if you implement your class with the public properties and methods that are defined by the interface, then code from somewhere else can work with your code without any prior knowledge or even an understanding about what it does.
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.
Example1.cs
At the heart of everything lies the corrected array resize code. As you can see, this implementation is not very different from the previously listed version, but there are a few changes.
using System;
namespace ArrayResizeExample
{
public class Fruit
{
public string Name
{
get
{
return( "Fruit" );
}
}
}
public class FruitBasket
{
Fruit[] basket = new Fruit[1];
int count = 0;
public void Add( Fruit fruit )
{
if( count >= basket.Length )
{
basket = (Fruit[]) Resize( basket, basket.Length * 2 );
}
basket[count++] = fruit;
}
public static Array Resize( Array array, int newSize )
{
Type type = array.GetType().GetElementType();
Array newArray = Array.CreateInstance( type, newSize );
Array.Copy( array, 0, newArray, 0, Math.Min( array.Length, newSize ));
return newArray;
}
}
class ArrayResizeExample
{
[STAThread]
static void Main(string[] args)
{
FruitBasket fruitBasket = new FruitBasket();
fruitBasket.Add( new Fruit() );
fruitBasket.Add( new Fruit() );
}
}
}
While it is perhaps successful in implementing and testing the array resize code without setting breakpoints and stepping through the code, it is difficult to see the fruits of our labor. (Of course the pun is intended, anyone who tells you a pun isn�t intended is lying).
Example2.cs
To make things interesting, I wanted to iterate over the fruit I added to my basket, and to print out the contents.
static void Main(string[] args)
{
FruitBasket fruitBasket = new FruitBasket();
fruitBasket.Add( new Fruit() );
fruitBasket.Add( new Fruit() );
Console.WriteLine( "The basket is holding:" );
foreach( Fruit fruit in fruitBasket )
{
Console.WriteLine( " a(n) " + fruit.Name );
}
}
I changed the Main
function to include the code I want to use. If you try to compile this now, you will get an error:
Example2.cs(53): foreach statement cannot operate on variables of type
'ArrayResizeExample.FruitBasket' because 'ArrayResizeExample.FruitBasket' does not
contain a definition for 'GetEnumerator', or it is inaccessible.
The foreach()
statement is looking for a class or struct that inherits IEnumerable
and which implements GetEnumerator()
. Add using System.Collections
to the top of the file.
using System;
using System.Collections;
Then add IEnumerable
to the FruitBasket
class.
public class FruitBasket : IEnumerable
When you do, Visual Studio 2003 (I�m not sure about earlier versions) does something wonderful. It prompts you to press tab
whereupon, it creates a region prepopulated with the methods and fields that you must support.
#region IEnumerable Members
public IEnumerator GetEnumerator()
{
return null;
}
#endregion
While it will crash with an exception,
An unhandled exception of type 'System.NullReferenceException' occurred in Example2.exe
Additional information: Object reference not set to an instance of an object.
the code will compile.
Stepping through the code reveals that it is crashing with the foreach
call; since we haven�t done anything with our GetEnumerator
method yet, this shouldn�t be too surprising.
Example3.cs
Looking at the MSDN documentation in more detail (and even the implemented GetEnumerator
function more closely), we find that GetEnumerator
is supposed to return an IEnumerator
. What this means for us, is that we need a class to implement IEnumerator
and the associated methods and properties. This can be done by using a comma to separate the two interfaces in the class definition.
public class FruitBasket : IEnumerable, IEnumerator
GetEnumerator
is also updated to reflect that this class is implementing the IEnumerator
members.
#region IEnumerable Members
public IEnumerator GetEnumerator()
{
return( this );
}
#endregion
#region IEnumerator Members
public void Reset()
{
}
public object Current
{
get
{
return null;
}
}
public bool MoveNext()
{
return false;
}
#endregion
Because we haven�t finished implementing the details, we are far from finished, but this will now compile and won�t crash � it won�t do what it�s supposed to do either, but at least it isn�t crashing now.
Example4.cs
I prefer separating the IEnumerable
and IEnumerator
implementations out. I think this makes maintenance of the code easier, but it is a personal preference. If you can so code it, the code doesn�t care and you can support several different interfaces in one class. For example4.cs and the rest of this article, I will use a separate class called FruitBasketEnumerator
. Return the class FruitBasket
to its former itself,
public class FruitBasket : IEnumerable
create FruitBasketEnumerator
,
public class FruitBasketEnumerator : IEnumerator
{
FruitBasket fruitBasket;
#region IEnumerator Members
public void Reset()
{
}
public object Current
{
get
{
return null;
}
}
public bool MoveNext()
{
return false;
}
#endregion
internal FruitBasketEnumerator( FruitBasket fruitBasket )
{
this.fruitBasket = fruitBasket;
Reset();
}
}
and modify GetEnumerator
to reflect this change.
#region IEnumerable Members
public IEnumerator GetEnumerator()
{
return( new FruitBasketEnumerator( this ));
}
#endregion
There is another reason to do it this way. By using a separate FruitBasketEnumerator
class, I can also make a constructor that takes care of various details when I�ve instantiated the class, or more accurately when foreach()
has. One such detail is resetting the index.
The MSDN documentation for Reset
says that it �Sets the enumerator to its initial position, which is before the first element in the collection.� With a 0 based index, the position just before the first element is -1. This means two things for us, first we need to keep track of the index for the life of the class so add a new field,
public class FruitBasketEnumerator : IEnumerator
{
FruitBasket fruitBasket;
int index;
and then set index
to -1 in the Reset
method.
public void Reset()
{
index = -1;
revision = fruitBasket.Revision;
}
MoveNext
, according to MSDN, �Advances the enumerator to the next element of the collection.� It is also important to note, that it returns a bool. �true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.�
Fortunately for us, fruitBasket
has already been keeping track of how many Fruit
classes have been added in its private field int count
. Adding an accessor Count
to the FruitBasket
class allows us to safely expose that data. The internal
keyword restricts access only to files within the same assembly. For our purposes, that is enough exposure.
internal int Count
{
get
{
return( count );
}
}
This allows us to implement the MoveNext
method of the IEnumerator
interface.
public bool MoveNext()
{
if( ++index >= fruitBasket.Count )
return( false );
else
return( true );
}
At this point, compiling and executing fails with another exception:
An unhandled exception of type 'System.NullReferenceException'
occurred in Example4.exe.
Additional information: Object reference not set to an instance of an object.
We haven�t yet implemented Current
, and as Visual Studio has preauthored it, we are returning a null
. This is in fact causing our System.NullReferenceException
. Current
, according to MSDN is simple enough; it just �gets the current element in the collection.� While we have access to the instance of the fruitBasket
class, we haven�t yet exposed a way for our FruitBasketEnumerator
to access the Fruit
kept in the fruitBasket
. In the FruitBasket
class, add this accessor. This will let you store and retrieve Fruit
from the basket at specific indices.
internal Fruit this[int index]
{
get
{
return( basket[index] );
}
set
{
basket[index] = value;
}
}
Then change the Current
method to return the Fruit
to the foreach()
construct.
public object Current
{
get
{
return( fruitBasket[index] );
}
}
If you compile and run this code, you will see that foreach()
now operates as we would have expected it to. We add fruit to the basket, and when we retrieve it, we look at each of those objects we�ve added.
This is still a bit like when we first implemented the array resize code. If we step through the code, we can see it pulling data from the correct structures, but it still difficult to tell when we just run it from the command line. At this point, fruit is just fruit.
Example5.cs
Q: When is fruit more than just fruit? A: When fruit is Apples, Bananas and Cantaloupes. Things are going to get sticky. Change the Fruit
class� Name
member to a virtual
member.
public class Fruit
{
public virtual string Name
{
get
{
return( "Fruit" );
}
}
}
Add the following derived classes to the namespace, overriding the virtual
member.
public class Apple : Fruit
{
public override string Name
{
get
{
return( "Apple" );
}
}
}
public class Banana : Fruit
{
public override string Name
{
get
{
return( "Banana" );
}
}
}
public class Cantaloupe : Fruit
{
public override string Name
{
get
{
return( "Cantaloupe" );
}
}
}
Eric Gunnerson would be proud because an Apple
is-a Fruit
, a Banana
is-a Fruit
, and a Cantaloupe
is-a Fruit
. Derived classes are the perfect choice because we can derive and extend from a base class that might already have members that make our life easier. This also makes maintaining the code easier, as we will see a little later. For now, this is enough to make our code do magic. Change Main
so that we are no longer adding just fruit,
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 Fruit" );
fruitBasket.Add( new Fruit() );
Console.WriteLine( "" );
Console.WriteLine( "The basket is holding:" );
foreach( Fruit fruit in fruitBasket )
{
Console.WriteLine( " a(n) " + fruit.Name );
}
}
Compile and run. Not only does our fruitBasket
now keep different kinds of fruit, but we can also recall the names of the fruit we added. Still, we can do more as documented in part 2.
Summary
In part 1, the example code demonstrates the need for, and the process of, implementing IEnumerable
and IEnumerator
interfaces. Please continue with part 2 where we will add IComparable
and IComparer
interfaces to the project to introduce additional capabilities.
History
- 5 March, 2005
Original article.