August 2, 2009

Abstract Classes, Inheritance, Generics, Covariance and Type Constraints

I was asked a question on Friday regarding type constraints. I have to claim complete ignorance to the terms as I've never come across it before so it's no suprise that I completely floundered.

In order to discuss the concept, I was presented with an example about inheritance which as a fairly seasoned OO programmer is something I do with very little if any thought. So consider the example I was given of two types of tires which we want to have common types of behaviour: SportsTire and RoadTire. Now as we want to be able to inherit certain behaviours and force the implementation of each of the two types of tire to contain certain behaviours we will consider some abstract methods and properties, so let's define a base class called TireBase:

public abstract class TireBase
{
 public abstract bool Inflated{ get; }
 public abstract void Inflate();
 public abstract void Deflate();
 public abstract void DisplayInflateState();
 public TireBase(string TireDisplayName)
 {
  Console.WriteLine("New {0} reporting for duty!", TireDisplayName);
 }
}

Now we will define our two concrete classes inheriting TireBase, first our RoadTire class:

public class RoadTire : TireBase
{
 private string _DisplayName = "Road tire";
 public string DisplayName
 { 
  get
  {
   return _DisplayName;
  }
 }

 private bool _Inflated;
 public override bool Inflated
 {
  get
  {
   return _Inflated;
  }
 }

 public override void Inflate()
 {
  _Inflated = true;
  DisplayInflateState();
 }

 public override void Deflate()
 {
  _Inflated = false;
  DisplayInflateState();
 }

 public override void DisplayInflateState()
 {
  Console.WriteLine("{0} {1}", _DisplayName, _Inflated ? "inflated" : "deflated");
 }

 public RoadTire(): base("road tire") {}
}

And now our SportsTire class:

public class SportsTire : TireBase
{
 private string _DisplayName = "Sports tire";
 public string DisplayName
 { 
  get
  {
   return _DisplayName;
  }
 }

 private bool _Inflated;
 public override bool Inflated 
 {
  get
  {
   return _Inflated;
  }
 }

 public override void Inflate()
 {
  _Inflated = true;
  DisplayInflateState();
 }

 public override void Deflate()
 {
  _Inflated = false;
  DisplayInflateState();
 }

 public override void DisplayInflateState()
 {
  Console.WriteLine("Sports tire {0}", _Inflated ? "inflated" : "deflated");
 }

 public SportsTire(): base("sports tire") {}         
}

You will notice that the two classes are extremely similar - for obvious reasons, after all this is just an example and really we are only demonstrating the need to be able to work with both classes.

So we've got our base class and our two classes deriving from TireBase. Now, as it was stated to me, if my understanding is correct, if I present the following method signature:

InflateAll(List<TireBase> tires)
{
 tires.ForEach(i => i.Inflate());
}

This is the exactly the same the code below, but uses the more concise lambda expression form:

InflateAll(List<TireBase> tires)
{
 foreach(tire i in tires)
 {
     i.Inflate();
 }
}

If I pass in a list of SportsTire or a list of RoadTire, then when the application reaches the point of execution of this method, I would get a runtime error. I wasn't convinced - but since we were using a whiteboard, I had no way to be 100% sure until I returned to my computer.

For instance:

var tires = new List<SportsTire>();
tires.Add(new SportsTire());
tires.Add(new SportsTire());

InflateAll(tires);

Thinking about the argument slightly differently, I approached it thus, what if we didn't define our list as that of SportsTire or RoadTire, but of TireBase? Such that the following was our definition:

var tires = new List<TireBase>();
tires.Add(new SportsTire());
tires.Add(new SportsTire());

InflateAll(tires);

My hypothesis is that this should work just fine - and indeed it does. However, the other person wasn't convinced and stated that again this would not be possible due to the inability to derive which type we are working with when we are trying to work down the hierarchy from TireBase to RoadTire or SportsTire. I didn't argue, but I still wasn't convinced - hence this blog post to describe my practical findings from sitting in front of my computer with Visual Studio 2008 rather than our theoretical musings on the whiteboard.

I will continue with the example of tires and I will do the following:

var tires = new list<TireBase>();
tires.Add(new SportsTire());
tires.Add(new SportsTire());
tires.Add(new SportsTire());
tires.Add(new RoadTire());
tires.Add(new RoadTire());
tires.Add(new SportsTire());

InflateAll(tires);

According to the person I was speaking with - if I understood the point he was trying to make - this should fail at the point I enter the InflateAll method and attempt to work with one of the derived types - my theory is that as long as I've defined the abstract method Inflate() in my base class, then much the same as defining the method in an interface, things should still be fine.

Upon running my code, indeed everything runs perfectly and here is my output:

New sports tire reporting for duty!
New sports tire reporting for duty!
New sports tire reporting for duty!
New road tire reporting for duty!
New road tire reporting for duty!
New sports tire reporting for duty!
Sports tire inflated
Sports tire inflated
Sports tire inflated
Road tire inflated
Road tire inflated
Sports tire inflated
Press any key to continue . . .

So my theory is correct - and the practical output is exactly the same as if I'd used an interface ITire and had my concrete classes implement that interface instead.

Okay, on to type constraints which is what this post was supposed to be about - so far I've really just been setting the scene but wanted to clear up one inaccurate point before I moved on. So, what if our InflateAll method had the following signature?:

InflateAll<T>(List<T> tires)
{
 T.Inflate(i => i.Inflate());
}

Suddenly it can receive a list of any type of object which as a programmer may make you panic. How do we determine that the objects passed in our list will actually have a method I can use to inflate them? Well as it turns out, there's this neat little idea called Type Constraints that actually allow us to limit what T can be.

InflateAll<T>(List<T> tires) where T : TireBase
{
 T.Inflate(i => i.Inflate());
}

This means that we can now constrain the list to any class that derives from TireBase - in the same way I we can did it before, but using generics. We've now defined a method that can receive a list of any type, providing it falls within the definition of the T type constraint.

Whereas before we had to define the list we passed in as List<TireBase>, we can now pass in an instance of List<TireBase>, List<SportsTire> or List<RoadTire> or indeed a list of any other type of tire deriving from TireBase - in a similar manner as if we had used an interface:

interface ITire
{
 public bool Inflated{ get; }
 public void Inflate();
 public void Deflate();
 public void DisplayInflateState();
}

The one slight difference is that the classes that implement the interface would have to handle their own constructor logic instead of passing it up to the base class as we did in the previous example.

3 comments:

  1. I'm confused as to why you would want to do something like this:

    InflateAll T (List T tires) where T : TireBase

    Instead of using the interface "ITire" ... I see your comment about the constructor logic, but I wanted to know what other benefits this technique grants you. I'm working with a developer that doesn't like the concept of generics outside of using them in lists (etc) ... I'm trying to explain (somewhat unsuccessfully) why this technique is useful.

    ReplyDelete
  2. The interface ITire would require that all your tires have to implement their own methods and constructors whereas having Tirebase would allow you to pass the constructor logic right up to the base class and you could also implement common methods in the baseclass.

    ReplyDelete
  3. It also means that you can pass any kind of List - be it List(Of Tirebase), List(Of SportsTire), List(Of RoadTire). You could equally easily use List(Of ITire) but when you start only using the interface in lieu of base classes, there's no ability to impliment common logic in a centralized place. Your interface is just a specification for building your class, your base class actually provides a partial implementation.

    ReplyDelete