Covariance, in general terms, is the measure of the differences (or lack thereof) of two objects. In programming parlance (at least for our purposes) covariance is generally tied to functions. Return type covariance allows a function to be defined with one return type yet implemented
to return another. This should not be confused with polymorphism and inheritance.
With inheritance we can use derived classes anywhere a base class is allowed.
However, as far as the compiler is concerned, it is still the base class and hence
we are limited to the base class'es functionality. For example suppose we
wanted to define a simple cloning class that allowed us to copy the object to a
new instance. We might define something like this:
public abstract class CloneBase
{
public abstract CloneBase Clone ();
}
public class ConcreteClass : CloneBase
{
public override CloneBase Clone () { return new ConcreteClass(); }
}
class Program
{
static void Main ( string[] args )
{
ConcreteClass cls = new ConcreteClass();
ConcreteClass cls2 = (ConcreteClass)cls.Clone();
}
Notice that, although we know the class is ConcreteClass, the compiler
didn't so we have to convert to the desired type. We could use overloading
here but then what has the base class given us? This where covariance comes
in. With covariance the compiler is smart enough to realize that, since ConcreteClass
derives from CloneBase, then it is safe to use ConcreteClass in
lieu of CloneBase. This would eliminate the need for casting and
would look something like this:
public abstract class CloneBase
{
public abstract CloneBase Clone ();
}
public class ConcreteClass : CloneBase
{
public override ConcreteClass Clone () { return new ConcreteClass();
}
}
class Program
{
static void Main ( string[] args )
{
ConcreteClass cls = new ConcreteClass();
ConcreteClass cls2 = cls.Clone();
}
}
Nice, clean syntax. Remember that the return type of a function is not part
of the function signature so type matching remains unchanged by covariance.
However covariance can be harder to identify at runtime so debugging may be more
difficult.
C#, as of v2, supports return type covariance for delegates only. This means
that you can modify the return type of any handler associated with a delegate provided
the return type derives from the same base type as the return type defined for the
delegate. You would never probably do this but here is what the above example
would look like if we switched to using delegates and covariance.
public class CloneBase
{
public delegate CloneBase CloneDelegate ();
public event CloneDelegate CloneMe;
}
public class ConcreteClass : CloneBase
{
public ConcreteClass CustomClone () { return new ConcreteClass();
}
}
class Program
{
static void Main ( string[] args )
{
ConcreteClass cls = new ConcreteClass();
cls.CloneMe += cls.CustomClone;
}
}
Again, not a realistic example but it proves the point. The delegate is defined
to return CloneBase but a derived class uses ConcreteClass instead,
which derives from CloneBase. Through covariance the compiler is
smart enough to realize that the derived class'es method still meets the requirements
of the delegate and can be used.