Friday, September 12, 2008

Simulating covariant return types in C#

For several years, Microsoft engineers have refused to add support for covariant return types, a trivially simple feature that should have been in the CLR from the beginning.

Suppose you want to write a Clone() method that returns a copy of the current object. Naturally you want to write the following, but it is illegal:
class MyStuff : ICloneable {
public MyStuff Clone() { ... }
}

Since you are implementing an interface, you can use this workaround that uses explicit interface implementation:
class MyStuff : ICloneable {
public MyStuff Clone() { ... }
object ICloneable.Clone() { return Clone(); }
}

The above workaround is okay for implementing an interface, but what if you are writing a class hierarchy, and you want a Clone() method that is virtual but has the appropriate return type?
class BaseNode : ICloneable
{
object ICloneable.Clone() { return Clone(); }
public virtual BaseNode Clone() { ... }
}
class ComplexNode : BaseNode
{
override BaseNode BaseNode.Clone() { return Clone(); } // Error!
public ComplexNode Clone() { ... }
}

Oops, the workaround that you use for interfaces is illegal for class inheritance. There is still a solution, though:
class BaseNode : ICloneable
{
object ICloneable.Clone() { return Clone(); }
public BaseNode Clone() { BaseNode c; Clone(out c); return c; }
protected virtual void Clone(out BaseNode clone) { ... }
}
class ComplexNode : BaseNode
{
public new ComplexNode Clone() { ComplexNode c; Clone(out c); return c; }
protected override void Clone(out BaseNode clone) { clone = Clone(); }
protected virtual void Clone(out ComplexNode clone) { ... }
}

That's right. You need six Clone() methods. The last method is virtual in case you want to make a class derived from ComplexNode, e.g. VeryComplexNode:
class VeryComplexNode : ComplexNode
{
public new VeryComplexNode Clone() { VeryComplexNode c; Clone(out c); return c; }
protected override void Clone(out BaseNode clone) { clone = Clone(); }
protected override void Clone(out ComplexNode clone) { clone = Clone(); }
protected virtual void Clone(out VeryComplexNode clone) { ... }
}

Without covariant return types, you have to to define an additional virtual function for each additional derived class.

5 comments:

Anonymous said...

I don't understand..
can you give more details in the core of the methode clone(out complex) and clone(out base).... i don't how it works..
i don't understand why we have to override the clone(out base c)...

Anonymous said...

thks by advance

Qwertie said...

if you don't override Clone(out BaseNode clone) then anyone who calls Clone(out c) (where c is a BaseNode) will be calling the base class implementation, which will clone only the BaseNode, not the entire complex node.

Anonymous said...

thks for your explanation.
but the methode clone(out basenode c) is of protected type...so how it will be called ?

just to detail the core of the protected method to understand how the complex object will be filed in.

I add two members in you base node class Double m_A, double m_B
class BaseNode :
protected virtual void Clone(out BaseNode clone)
{
clone = (BaseNode)this.MemberwiseClone(); //here because the members are simple
}
}

in complex objet,
member data : string m_C;
protected virtual void Clone(out ComplexNode clone)
{
//? i don't see how to reuse the base node method for cloning the part of basenode and complete the part of complexenode
}}

I hope you understand what i mean

Qwertie said...

The protected method Clone(out BaseNode c) is called by the public function.

public BaseNode Clone() { BaseNode c; Clone(out c); return c; }

It's true that you can't easily re-use the base-node version as written. Of course, if the implementation is just calling MemberwiseClone then there's no reason to go to the trouble of trying to re-use the base class version; just call MemberwiseClone in the derived class too.

Remember, this article isn't really about cloning objects. The article is about covariant return types and Clone() is merely one of many examples of reasons you might want a covariant return type.