Covariance

What is this about?

Sunday, April 27, 2003

In object-oriented designs, the need for return type covariance arises. It means that virtual methods can modify their formal return type down the inheritance chain. It is quite useful if there are mirroring inheritance hierarchies. For example, one might devise collections that restrict their element type; factories that can be inherited, each sub class instantiating objects of a more specialized type; and so on. This kind of modification does not affect compatibility, since the return type can only be narrowed.

Note that this works with return values only. For out parameters, one might say, it would be useful as well, but then, method overloading rules stand in the way (method virtualness only applies to methods that don't overload each other), and compatibility is in fact broken (that is because parameters must be assigned to, not assigned from).

It is important to stress that this is about overriding methods, not ones that hide the base class definition by signature, because the formal type of the object reference should not affect method resolution.

What return type covariance comes down to, practically, is that you have to cast a seldom as possible. If you have a formal WebRequest that is an actual HttpWebRequest, and cast it to its actual type, you shouldn't have to cast again when you call the GetResponse method - which returns an actual HttpWebResponse, but the formal return type of the method is at issue here.

The status in .NET

Officially, .NET languages do not support return type covariance (this is in the Common Language Specification). Although VC++ now supports return type covariance (as part of the Ansi standard), this does not apply to Managed Extensions.

However, there is a way to fake it. And you fake it by breaking down the method into two, just like this:

  1. Define methods whose return types vary. Use explicit hiding by signature (in C#), or hiding by Name (in VB.NET - the Overloads modifier can't be used here).
  2. Delegate the implementation to protected virtual methods, in order to preserve dynamic lookup for methods containing the actual implementation (this involves some casting).

First strike: Producing

So here's an example:

class CoVarBase {

    public CoVarBase() : base() {
    }

    public object Product{
        get{
            return this.Produce();
        }
    }

    protected virtual object Produce(){
        return new object();
    }

} // class CoVarBase

class CoVarDerived : CoVarBase {

    public CoVarDerived() : base() {
    }

    public new string Product{
        get{
            return (string) this.Produce();
        }
    }

    protected override object Produce(){
        return "StringProduct";
    }

} // class CoVarDerived

class Test{

    public static void Main(string[] args){

        CoVarDerived d = new CoVarDerived();
        CoVarBase b = d;

        string s;
        s = (string) b.Product;  // cast succeeds: string returned
        s = d.Product;           // new formal return type
    }

} // class Test

Second strike: Consuming

There is an extra complication here: If you have a reference of type CoVarBase refering to an instance of CoVarDerived, and virtual methods work as they should, and pass data to that method that is only suitable for CoVarBase, compile-time type checking is limited. This is the same situation as in arrays (a consequence of array covariance). For the runtime type check, we need a special exception class:

class TypeMismatchException : System.Exception {

    public TypeMismatchException()
    : base ("The type of an object is not assignable.") {
    }
    public TypeMismatchException(string sMessage)
    : base (sMessage) {
    }
    public TypeMismatchException(string sMessage, Exception inner)
    : base (sMessage, inner) {
    }

} // class TypeMismatchException

This exception is thrown if incompatible data is passed to a "covariant" property on an instance of the derived class through a reference of the base class. Here's the consumer side:

class CoVarBase {

    public CoVarBase() : base() {
    }

    public object Product{
        get{
            return this.Produce();
        }
        set{
            this.Consume(value);
        }
    }

    protected virtual object Produce(){
        return new object();
    }
    protected virtual void Consume(object o){
        // ... consume it
    }

} // class CoVarBase

class CoVarDerived : CoVarBase {

    public CoVarDerived() : base() {
    }

    public new string Product{
        get{
            return (string) this.Produce();
        }
        set{
            this.Consume(value);
        }
    }

    protected override object Produce(){
        return "StringProduct";
    }
    protected override void Consume(object o){
        if(false == (o is string)){
            throw new TypeMismatchException();
        }
        // ... consume it
    }

} // class CoVarDerived

class Test{

    public static void Main(string[] args){

        CoVarDerived d = new CoVarDerived();
        CoVarBase b = d;

        b.Product = new Object(); // throws type mismatch
        b.Product = "String";     // OK
        d.Product = new Object(); // compile error
        d.Product = "String";     // OK
    }

} // class Test

So we go through a property in order to have covariant methods on our object in a consumer role. That is because arguments cannot be covariant (as explained above; however, in VB.NET, we could use the Shadows modifier for this; in fact, we would have to use Shadows for the property approach as well, because Overloads isn't an exact equivalent of C#'s "new" [it shadows by signature as well, but can't be used if the return type is different]). There is a bit of magic here: under the hood, the property is implemented as a set of methods, and the value is just an argument of the set-accessor - so at the lower level, it's more like overloading: but C# and VB.NET are high-level enough to crush these concerns.

A concern that remains is that when a property is writeable, a narrowed property type breaks compatibility, which is an extra obstacle to return type covariance in languages that formally support properties. But the same problem occurs if properties are not supported as a language feature, but are used informally (through get/set method conventions), and the type of some data had or held by a class is actually narrowed by derived classes, even if no fool like me attempts to trick the compiler into covariance.

On the bright side, covariance is indeed available in .NET - arrays of reference types are like containers with covariant Item properties. Analoguous to the TypeMismatchException shown above, there is the System.ArrayTypeMismatchException for situations where an array of a base type refers to an array of a derived type, and an attempt is made to store an element of either the base type or otherwise incompatible derived type in it.