Search code examples
c#covariancecontravarianceinvariance

puzzled by "Already defines a member with the same parameter types" error


Not understanding why these to Map methods have the same parameter types, since they don't appear too. Is this a covariance thing or just a generic signature thing?

I would like to understand it in general to avoid writing code that will have this problem. (And explain the catch-22 of how it is both the same and not-the-same)


class A { }
class B
{
    void Map<T>(T obj) where T : A { }
    void Map<T>(T obj) where T : B { } // SAME -- "Map" already defined, can't compile

    void Test(A a, B b)
    {
        Map(a);
        Map(b); // NOT SAME -- if the B-mapper is removed then this won't compile
    }
}

This change will "fix" it: void Map<T>(in T obj) ??

This seems even more "Not the Same":

Ta Map<Tb, Ta>(Tb obj) where Ta: A, new() where Tb : B, new() { return new Ta(); }
Tb Map<Ta, Tb>(Ta obj) where Ta: A, new() where Tb : B, new() { return new Tb(); } // ERROR

E.g., want to Map<Tsrc, Tdest> from Tsrc to Tdest, by convention, but can't. Need swap generic order:

Ta Map<Ta, Tb>(Tb obj) where Ta: A, new() where Tb : B, new() { return new Ta(); }
Tb Map<Ta, Tb>(Ta obj) where Ta: A, new() where Tb : B, new() { return new Tb(); } // OK NOW

Note: in these last two cases the parameter types are not changed, even though one is legal and the other isn't. The in invariance modifier would also fix this, which I believe is a clue to understanding what is going on.


Solution

  • Consider this example:

    void Map<T>(T obj) where T : struct { }
    void Map<T>(T obj) where T : unmanaged { }
    
    Map<int> map = new Map<int>(1);
    

    Which version does our map instance use? Both are just as applicable for an int. And as the actual signatures are identical, they now become ambiguous.

    Signatures are based on the number of arguments, the order of the arguments, and the argument types.

    Generic type constraints are not considered part of a signature. They are used as "rules" to be followed when generating the actual classes/methods/delegates etc for use at runtime. Once the compiler generates each different version of your generic code necessary for the runtime, the constraints are no longer needed.

    void Map<T>(in T obj) is indeed a different signature than void Map<T>(T obj) because in T is not the same as T (or out T, ref T, et al).

    void Map<T>(T obj) where T : A has an identical signature to void Map<T>(T obj) where T : B since the constraint is all that changed. The number of arguments, the order of the arguments, and the argument types are all the same.