views:

181

answers:

3

I wonder why the C# team decided not to support co-/contravariance for non-generics, considering they could be just as safe. The question is rather subjective, as I don't expect a team member to respond, but someone may have the insight I (and Barbara Liskov) lack.

Lets look at this sample interface:

public interface ITest
{
 object Property
 {
  get;
 }
}

The following implementation will fail, although completely safe (we could always return a more specific type without violating the interface - not in C#, but at least in theory).

public class Test : ITest
{
 public string Property
 {
  get;
 }
}

The code would naturally not be safe, if the interface included a setter, but this is no reason for limiting implementation overall, as this could be pointed out by using out/in to declare safety, just as for generics.

+2  A: 

The CLR doesn't support covariant return types, whereas it's supported delegate/interface generic variance from .NET 2.0 onwards.

In other words, it's not really up to the C# team, but the CLR team.

As to why the CLR doesn't support normal variance - I'm not sure, other than adding complexity, presumably without the requisite amount of perceived benefit.

EDIT: To counter the point about return type covariance, from section 8.10.4 of the CLI spec, talking about vtable "slots":

For each new member that is marked "expect existing slot", look to see if an exact match on kind (i.e., field or method), name, and signature exists and use that slot if it is found, otherwise allocate a new slot.

From partition II, section 9.9:

Specifically, in order to determine whether a member hides (for static or instance members) or overrides (for virtual methods) a member from a base class or interface, simply substitute each generic parameter with its generic argument, and compare the resulting member signatures.

There is no indication that the comparison is done in a way which allows for variance.

If you believe the CLR does allow for variance, I think that given the evidence above it's up to you to prove it with some appropriate IL.

EDIT: I've just tried it in IL, and it doesn't work. Compile this code:

using System;

public class Base
{
    public virtual object Foo()
    {
        Console.WriteLine("Base.Foo");
        return null;
    }
}

public class Derived : Base
{
    public override object Foo()
    {
        Console.WriteLine("Derived.Foo");
        return null;
    }
}

class Test
{
    static void Main()
    {
        Base b = new Derived();
        b.Foo();
    }
}

Run it, with output:

Derived.Foo

Disassemble it:

ildasm Test.exe /out:Test.il

Edit Derived.Foo to have a return type of "string" instead of "object":

.method public hidebysig virtual instance string Foo() cil managed

Rebuild:

ilasm /OUTPUT:Test.exe Test.il

Rerun it, with output:

Base.Foo

In other words, Derived.Foo no longer overrides Base.Foo as far as the CLR is concerned.

Jon Skeet
"The method signature defines the calling convention, type of the parameters to the method, and the return type of the method" (from ECMA-335, 8.11.1).As CLR is an implementation of CLI, wouldn't it support return type overloading - and thus allowing variance?
troethom
No, because supporting return type overloading *isn't* the same thing as supporting variance. I'll look up the relevant bit on *overriding* which is the important part.
Jon Skeet
This could be worked around in the C# compiler, by adding extra metadata to the assembly and inserting casts in the right places (like how java deals with generics - type erasure + casting)
thecoop
@thecoop: I think that would lead to more complexity and hard-to-understand behaviour than the benefit gained.
Jon Skeet
+1  A: 
  1. Returned value of method is always "out", they are always on right side of an assignment expression.
  2. CLR Assembly format has metadata which assigns instance methods to interface methods, but this method inference is only dependent on input parameters, they might need to create new format to support, not only that it also becomes ambigious.
  3. New methods mapping algorithm between instance method and interface signature could be complex and CPU intensive as well. And I belive method signatures of interface methods are resolved at compile time. Because at runtime it may be too expensive.
  4. Method inference/resolution could be problem as explained below.

Consider following sample with allowed dynamic method resolution with different return types only (Which is absolutely wrong)

public class Test{
     public int DoMethod(){ return 2; }
     public string DoMethod() { return "Name"; }
} 
Test t;
int n = t.DoMethod();  // 1st method
string txt = t.DoMethod(); // 2nd method
object x = t.DoMethod(); // DOOMED ... which one??
Akash Kava
This problem exists already today, if you have a class that implements two interfaces and passes the instance to a method which has overloads for both interfaces.
troethom
Like I commented to Jon Skeet, in CTS, the method signature includes the return type.
troethom
+1  A: 

The CLR does not support variance on method overrides, but there is a workaround for interface implementations:

public class Test : ITest
{
    public string Property
    {
        get;
    }

    object ITest.Property
    {
        get
        {
            return Property;
        }
    }
}

This will achieve the same effect as the covariant override, but can only be used for interfaces and for direct implementations

thecoop
That's how I ended up solved it, but as more classes use a similar approach, the code gets ugly. I guess I don't have any choice though.
troethom