Generics & Invariance: An Interesting Problem#

Setting The Stage: Invariance

I had a rather interesting problem using generics in C# 2.0 today, but before I dig in to the actual issue, let me briefly introduce you to the "invariant" nature of generics.

As you know, you can certainly assign a string to a variable of type object because string is a subclass of object:

string s = "some value";
object o = s; // No problem

Now suppose you have a List<object> and a List<string>, which are generic types describing a list of objects and a list of strings, respectively. Given that string is a subclass of object, you might assume that you can assign this List<string> to a List<object> as well:

List<string> strings = new List<string>();
List<object> objects = strings; // Fails

The compiler, however, rudely disagrees with you: error CS0029: Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'. This is because generic types in .NET are said to be invariant, which means that they don't adhere to the same inheritance tree as their type parameters (string and object in this case).

Although similar in nature, arrays don't share this nature and are said to be covariant, which means that the following code is perfectly legal:

string[] strings = new string[] {};
object objects[] = strings; // No problem

For more information and the reasoning behind this, check out Rick Byers' excellent post on Generic type parameter variance in the CLR.

The Problem Manifestation

With that in mind, imagine the following class hierarchy:

So there are two interfaces describing a document (IDocument) and a cabinet (ICabinet) which basically holds a generic list of these IDocuments. The Document class implements the IDocument interface in the obvious way. The interesting part is the Cabinet class:

public interface ICabinet
{
string Name { get; }
string Location { get; }
List<IDocument> Documents { get; }
}
public class Cabinet : ICabinet
{
  private string name = null;
  private string location = null;
  private List<IDocument> documents = new List<IDocument>();
 
  public string Name { get { return name; } }
  public string Location { get { return location; } }
  public List<IDocument> Documents { get { return documents; } }
}

To implement the interface, the Cabinet class has to expose the List<IDocument> through the Documents property, and since generics are invariant, this means that the return type cannot be List<Document> (note the use of the class here, not the interface). This implies that the private documents backing field must also be a List<IDocument> in stead of a List<Document>, for the same reason.

Now there's currently no link between the concrete cabinet type and the concrete document type and I really want this Documents property on my Cabinet class to expose a List<Document>, so that's a problem. The reason I need this, is that my cabinet class will be exposed over a Web Service and must therefore be serializable. As you might know, serializable types cannot publicly expose interface types because the serializer cannot handle them. But there could be other valid reasons why you would want the concrete cabinet type to return the concrete document classes.

Trying To Solve The Problem

I came up with a moderately satisfying solution... I implemented the private backing field and the Documents property using the "class version", and explicitly implemented the interface as such:

[Serializable]
public class Cabinet : ICabinet
{
  private string name = null;
  private string location = null;
  private List<Document> documents = new List<Document>();
 
  public string Name { get { return name; } }
  public string Location { get { return location; } }
  public List<Document> Documents { get { return documents; } }
 
  [XmlIgnore]
  List<IDocument> ICabinet.Documents // Explicit implementation
  {
    get
    {
      return this.Documents.ConvertAll<IDocument>(
        delegate(Document target)
        {
          return (IDocument)target;
        }
      );
    }
  }
}

The explicit interface implementation will not only "hide" the property from normal consumers of the class and resolve the obvious naming conflict of the two Documents properties, but the xml serializer will also ignore the explicitly implemented property (and just to be sure, I also applied the [XmlIgnore] attribute to it). If this property is queried anyway, it will convert the "class version" to the "interface version" using the generic ConvertAll method, which returns a new list of another type by converting each list item through an anonymous method. In this case, a simple cast is enough because they are covariant. This yields the following picture:

An Additional Problem

There's still an issue with this approach, though: if you come in through the interface and try to manipulate the documents list (for example, if you add an item to it), you will be working on this freshly-created copy of the list and your changes won't show up in the original list:

ICabinet cabinet = new Cabinet(); // Declared as the interface
cabinet.Documents.Add(new Document()); // Won't work as expected
Assert.AreEqual(cabinet.Documents.Count, 0);

Luckily, I had the option to change the interface to return a ReadOnlyCollection<IDocument> so that modifications weren't even possible and the problem went away. Luckily.

But if I weren't as blessed, I would probably have given up and steered away from these generic types altogether in this scenario since they generate quite some issues when used in inheritance hierarchies as you've seen. I'm a big fan of generics and they've surprised me in many interesting ways (just look at the sheer beauty of that ConvertAll combined with the anonymous delegate above), but this time I think I've stretched them too far...

Blog | Programming | .NET | Quirks | Whidbey
Tuesday, December 13, 2005 2:40:00 PM (Romance Standard Time, UTC+01:00)
I could not do what you described above, given the fact that my container class needs to add to the given collection.

ConvertAll converts it to another List[T], which cannot be converted to ReadOnlyCollection<[T]...

I am thinking of adding a function say GetDocuments() in the interface, which will return the converted List[], but still the problem is there...

Any suggestions ?


PS :
[] are &lt; and &gt; :)
Comments are closed.
All content © 2008, Jelle Druyts
On this page

Recent Photos
www.flickr.com
This is a Flickr badge showing public photos from Jelle Druyts. Make your own badge here.
Advertising
Top Picks
Statistics
Total Posts: 344
This Year: 7
This Month: 0
This Week: 0
Comments: 522
Archives
Sitemap
Disclaimer
This is my personal website, not my boss', not my mother's, and certainly not the pope's. My personal opinions may be irrelevant, inaccurate, boring or even plain wrong, I'm sorry if that makes you feel uncomfortable. But then again, you don't have to read them, I just hope you'll find something interesting here now and then. I'll certainly do my best. But if you don't like it, go read the pope's blog. I'm sure it's fascinating.

Powered by:
newtelligence dasBlog 2.0.7226.0

Sign In