C# Covariance on subclass return types
Asked Answered
M

4

7

Does anyone know why covariant return types are not supported in C#? Even when attempting to use an interface, the compiler complains that it is not allowed. See the following example.

class Order
{
    private Guid? _id;
    private String _productName;
    private double _price;

    protected Order(Guid? id, String productName, double price)
    {
        _id = id;
        _productName = productName;
        _price = price;
    }

    protected class Builder : IBuilder<Order>
    {
        public Guid? Id { get; set; }
        public String ProductName { get; set; }
        public double Price { get; set; }

        public virtual Order Build()
        {
            if(Id == null || ProductName == null || Price == null)
                throw new InvalidOperationException("Missing required data!");

            return new Order(Id, ProductName, Price);
        }
    }            
}

class PastryOrder : Order
{
    PastryOrder(Guid? id, String productName, double price, PastryType pastryType) : base(id, productName, price)
    {

    }

    class PastryBuilder : Builder
    {
        public PastryType PastryType {get; set;}

        public override PastryOrder Build()
        {
            if(PastryType == null) throw new InvalidOperationException("Missing data!");
            return new PastryOrder(Id, ProductName, Price, PastryType);
        }
    }
}

interface IBuilder<in T>
{
    T Build();
}

public enum PastryType
{
    Cake,
    Donut,
    Cookie
}

Thanks for any responses.

Messner answered 10/2, 2012 at 22:45 Comment(16)
blogs.msdn.com/b/ericlippert/archive/2007/10/16/…Scribble
blogs.msdn.com/b/ericlippert/archive/2007/10/19/…Scribble
blogs.msdn.com/b/ericlippert/archive/2007/10/22/…Scribble
blogs.msdn.com/b/ericlippert/archive/2007/10/26/…Scribble
blogs.msdn.com/b/ericlippert/archive/2007/10/24/…Scribble
blogs.msdn.com/b/ericlippert/archive/2007/10/31/…Scribble
blogs.msdn.com/b/ericlippert/archive/2007/11/02/…Scribble
blogs.msdn.com/b/ericlippert/archive/2008/05/07/…Scribble
blogs.msdn.com/b/csharpfaq/archive/2010/02/16/…Scribble
hestia.typepad.com/flatlander/2008/12/…Scribble
@Scribble Eric's blog series does not answer the question "why doesn't C# support return type covariance on virtual method overrides".Animate
You are very fast to read all of them. Or are you expecting a title "why doesn't C# support return type covariance on virtual method overrides" ?Scribble
@Scribble I read them all already, a few years ago. Method override covariance has little to do with co- and contravariance of interface and delegate type parameters. These articles discuss a co- and contravariance feature that C# does support; the question is about a covariance feature that C# doesn't support.Animate
@phoog, Good for you, then ignore my comments.Scribble
It will work if you change the return type from PastryBuilder Build method to Order instead of PastryOrder and the IBuilder<in T> should be IBuilder<out T> for covariant type parameter. This is typical abstract factory pattern.Blasphemy
possible duplicate of Does C# support return type covariance?Flame
F
26

UPDATE: This answer was written in 2011. After two decades of people proposing return type covariance for C#, it looks like it will finally be implemented; I am rather surprised. See the bottom of https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/ for the announcement; I'm sure details will follow.


First off, return type contravariance doesn't make any sense; I think you are talking about return type covariance.

See this question for details:

Does C# support return type covariance?

You want to know why the feature is not implemented. phoog is correct; the feature is not implemented because no one here ever implemented it. A necessary but insufficient requirement is that the feature's benefits exceed its costs.

The costs are considerable. The feature is not supported natively by the runtime, it works directly against our goal to make C# versionable because it introduces yet another form of the brittle base class problem, Anders doesn't think it is an interesting or useful feature, and if you really want it, you can make it work by writing little helper methods. (Which is exactly what the CIL version of C++ does.)

The benefits are small.

High cost, small benefit features with an easy workaround get triaged away very quickly. We have far higher priorities.

Fitz answered 11/2, 2012 at 1:33 Comment(2)
Thanks for your response Eric. An obvious question though is given the solution you present in the topic that you linked to, it seems like the compiler could be made to do this method rewriting just like the C++ CIL compiler does. In fact the type of rewriting the compiler does for other features (such as async/await) seems a lot more complicated than for a feature like this. I also disagree that the benefits are small; as you have mentioned other languages support the feature, and other developers besides myself have asked about it.Messner
@MgSam: You're welcome. Yes, async/await is far more complicated, and that is precisely why we're concentrating our energies on it. We have an opportunity here to make asynchronous programming fundamentally orders of magnitude easier in C#. That is way, way more important than making a syntactic sugar that makes it look like a new virtual method actually overrides a method with a different signature.Fitz
B
9

The contravariant generic parameter cannot be output, because that cannot be guaranteed to be safe at compile time, and C# designers made a decision not to prolong the necessary checks to the run-time.

This is the short answer, and here is a slightly longer one...

What is variance?

Variance is a property of a transformation applied to a type hierarchy:

  • If the result of the transformation is a type hierarchy that keeps the "direction" of the original type hierarchy, the transformation is co-variant.
  • If the result of the transformation is a type hierarchy that reverses the original "direction", the transformation is contra-variant.
  • If the result of the transformation is a bunch of unrelated types, the transformation is in-variant.

What is variance in C#?

In C#, the "transformation" is "being used as a generic parameter". For example, let's say a class Parent is inherited by class Child. Let's denote that fact as: Parent > Child (because all Child instances are also Parent instances, but not necessarily the other way around, hence Parent is "bigger"). Let's also say we have a generic interface I<T>:

  • If I<Parent> > I<Child>, the T is covariant (the original "direction" between Parent and Child is kept).
  • If I<Parent> < I<Child>, the T is contravariant (the original "direction" is reversed).
  • If I<Parent> is unrelated to I<Child>, the T is invariant.

So, what is potentially unsafe?

If C# compiler actually agreed to compile the following code...

class Parent {
}

class Child : Parent {
}

interface I<in T> {
    T Get(); // Imagine this actually compiles.
}

class G<T> : I<T> where T : new() {
    public T Get() {
        return new T();
    }
}

// ...

I<Child> g = new G<Parent>(); // OK since T is declared as contravariant, thus "reversing" the type hierarchy, as explained above.
Child child = g.Get(); // Yuck!

...this would lead to a problem at run-time: a Parent is instantiated and assigned to a reference to Child. Since Parent is not Child, this is wrong!

The last line looks OK at compile-time since I<Child>.Get is declared to return Child, yet we could not fully "trust" it at run-time. C# designers decided to do the right thing and catch the problem completely at compile-time, and avoid any need for the run-time checks (unlike for arrays).

(For similar but "reverse" reasons, covariant generic parameter cannot be used as input.)

Braque answered 11/2, 2012 at 2:34 Comment(0)
A
7

Eric Lippert has written a few posts on this site about return method covariance on method overrides, without as far as I can see addressing why the feature is unsupported. He has mentioned, though, that there are no plans to support it: https://mcmap.net/q/103809/-c-covariant-return-types-utilizing-generics

Eric is also fond of saying that the answer to "why isn't X supported" is always the same: because nobody has designed, implemented, and tested (etc.) X. An example of that is here: https://mcmap.net/q/103810/-c-generics-without-lower-bounds-by-design

There may be some philosophical reason for the lack of this feature; perhaps Eric will see this question and enlighten us.

EDIT

As Pratik pointed out in a comment:

interface IBuilder<in T> 
{ 
    T Build(); 
} 

should be

interface IBuilder<out T> 
{ 
    T Build(); 
} 

That would allow you to implement PastryOrder : IBuilder<PastryOrder>, and you could then have

IBuilder<Order> builder = new PastryOrder();

There are probably two or three approaches you could use to solve your problem, but, as you note, return method covariance is not one of those approaches, and none of this information answers the question of why C# doesn't support it.

Animate answered 10/2, 2012 at 23:10 Comment(0)
I
0

Just to post this somewhere google finds it... I was looking into this because I wanted to have an interface in which I can return collections / enumerables of arbitrary classes implementing a specific interface.

If you're fine with defining the concrete types you want to return, you can simply define your interface accordingly. It will then check at compile time that the constraints (subtype of whatever) are met.

I provided an example, that might help you.

As Branko Dimitrijevic pointed out, usually it is unsafe to allow covariant return types in general. But using this, it's type-safe and you can even nest this (e. g. interface A<T, U> where T: B<U> where U : C)

(Disclaimer: I started using C# yesterday, so I might be completely wrong regarding best practices, someone with more experience should please comment on this :) )


Example:

Using

interface IProvider<T, Coll> where T : ProvidedData where Coll : IEnumerable<T>
{
  Coll GetData();
}

class XProvider : IProvider<X, List<X>>
{
  List<X> GetData() { ... }
}

calling

new XProvider().GetData

works and in this case is safe. You only have to define the types you want to return in this case.


More on this: http://msdn.microsoft.com/de-de/library/d5x73970.aspx

Impart answered 28/3, 2013 at 14:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.