Limitations of generic base classes
This post was originally published here.
I thought I had created something fairly useful with a generic Value Object in a previous post. Generic base classes are nice, and there are several recommended base classes for creating collections classes. Whenever I try to make an interesting API with a generic base class, limitations and faulty assumptions always reduce the usefulness of that base class. Let’s first start with a couple of classes that we’d like to include in a generic API:
public class Address
{
private readonly string _address1;
private readonly string _city;
public Address(string address1, string city)
{
_address1 = address1;
_city = city;
}
public string Address1
{
get { return _address1; }
}
public string City
{
get { return _city; }
}
}
public class ExpandedAddress : Address
{
private readonly string _address2;
public ExpandedAddress(string address1, string address2, string city)
: base(address1, city)
{
_address2 = address2;
}
public string Address2
{
get { return _address2; }
}
}
Fairly basic classes, just two types of addresses, with one a subtype of the other. So what kinds of issues do I usually run in to with generic base classes? Let’s look at a few different types of generic base classes to see.
Concrete generic implementations
A concrete generic implementation is a concrete class that inherits a generic base class:
public class AddressCollection : Collection<Address>
{
public AddressCollection() {}
public AddressCollection(IList<Address> list) : base(list) {}
}
Following the Framework Design Guidelines, I created a concrete collections class by inheriting from System.Collections.ObjectModel.Collection
- Create an ExpandedAddressCollection class inheriting from AddressCollection
-
Create an ExpandedAddressCollection class inheriting from Collection
- Use the existing AddressCollection class and put ExpandedAddress instances in it.
Inherit from AddressCollection
Here’s what the ExpandedAddressCollection class would look like:
public class ExpandedAddressCollection : AddressCollection
{
public ExpandedAddressCollection() {}
public ExpandedAddressCollection(IList<Address> list) : base(list) {}
}
That’s not very interesting, it didn’t add any information to the original AddressCollection. What’s more, this ExpandedAddressCollection ultimately inherits Collection<Address>, _not_ Collection<ExpandedAddress>. Everything I try to put in or get out will be an Address, not an ExpandedAddress. For example, this code wouldn’t compile: <div class="CodeFormatContainer"> <pre>List<ExpandedAddress> addresses = <span class="kwrd">new</span> List<ExpandedAddress>();<br />addresses.Add(new ExpandedAddress(“Address1”, “Austin”, “TX”));
ExpandedAddressCollection addressList = new ExpandedAddressCollection(addresses);
-
Because of limitations in generic variance, namely that C# [does not support](http://msdn2.microsoft.com/en-us/library/ms228359(vs.80).aspx) generic [covariance or contravariance](http://blogs.msdn.com/rmbyers/archive/2005/02/16/375079.aspx). Even though ExpandedAddress is a subtype of Address, and ExpandedAddress[] is a subtype of Address[], IList<ExpandedAddress> **is not** a subtype of IList<Address>.
#### Inherit from Collection<ExpandedAddress>
In this example, I’ll just implement the ExpandedAddressCollection in the same manner as AddressCollection:
<div class="CodeFormatContainer">
<pre><span class="kwrd">public</span> <span class="kwrd">class</span> ExpandedAddressCollection : Collection<ExpandedAddress><br />
{
public ExpandedAddressCollection() {}
public ExpandedAddressCollection(IList<ExpandedAddress> list) : base(list) { }
}
Now I my collection is strongly types towards ExpandedAddresses, so the example I showed previously would now compile. It seems like I’m on the right track, but I run into even more issues:
* ExpandedAddressCollection is not a subtype of AddressCollection
* Collection hierarchy does not match hierarchy of Addresses (one is a tree, the other is flat)
* I can’t pass an ExpandedAddressCollection into a method expecting an AddressCollection
* Since there is no relationship between the two collection types, I can’t use many patterns where a relationship is necessary</ul>
So even though my collection is strongly typed, it becomes severely limited in more interesting scenarios.
#### Use existing AddressCollection class
In this instance, I won’t even create an ExpandedAddressCollection class. Any time I need a collection of ExpandedAddresses, I’ll use the AddressCollection class, and cast as necessary. I won’t be able to pass an IList<ExpandedAddress> to the constructor because of the variance issue, however. If I need to include some custom logic in the collection class, I’ll run into the same problems highlighted earlier if I’m forced to create a new subtype of AddressCollection.
So we’ve seen the limitations of concrete generic implementations, what other options do I have?
### Constrained generic base classes
I’d like a way to propagate the concrete type parameter back up to the original Collection<T>. What if I make the AddressCollection generic as well? Here’s what that would look like:
<div class="CodeFormatContainer">
<pre><span class="kwrd">public</span> <span class="kwrd">class</span> AddressCollection<T> : Collection<T><br />
where T : Address
{
public AddressCollection() {}
public AddressCollection(IList<T> list) : base(list) {}
}
public class ExpandedAddressCollection : AddressCollection<ExpandedAddress>
{
public ExpandedAddressCollection() {}
public ExpandedAddressCollection(IList<ExpandedAddress> list) : base(list) { }
}
So now I have a constrained base class for an AddressCollection, and an implementation for an ExpandedAddressCollection. What do I gain from this implementation?
* ExpandedAddressCollection is completely optional, I could just define all usage through an AddressCollection<ExpandedAddress>
* Any AddressCollection concrete type will be correctly strongly typed for an Address type</ul>
Again, with some more interesting usage, I start to run into some problems:
* I can never reference only AddressCollection, as I always have to give it a type parameter.
* Once I give it a type parameter, I run into the same variance issues as before, namely AddressCollection<ExpandedAddress> is not a subtype of AddressCollection<Address>
* Since I can never define any method in terms of solely an AddressCollection, I either need to make the containing class generic or the method generic.</ul>
For example, I can write the following code:
<div class="CodeFormatContainer">
<pre><span class="kwrd">public</span> <span class="kwrd">void</span> TestGenerics()<br />
{
AddressCollection<Address> addresses = new AddressCollection<Address>();
addresses.Add(new Address(“132 Anywhere”, “Austin”));
int count = NumberOfAustinAddresses(addresses);
AddressCollection<ExpandedAddress> expAddresses = new AddressCollection<ExpandedAddress>();
expAddresses.Add(new ExpandedAddress(“132 Anywhere”, “Apt 123”, “Austin”));
count = NumberOfAustinAddresses(addresses);
}
private int NumberOfAustinAddresses<T>(AddressCollection<T> addresses)
where T : Address
{
int count = 0;
foreach (T address in addresses)
{
if (address.City == “Austin”)
count++;
}
return count;
}
