Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambiguous call between overloads of two-way implicit castable types when a derived type of one is passed as parameter

(Trying to find a title that sums up a problem can be a very daunting task!)

I have the following classes with some overloaded methods that produce a call ambiguity compiler error:

public class MyClass
{
    public static void OverloadedMethod(MyClass l) { }
    public static void OverloadedMethod(MyCastableClass l) { }

    //Try commenting this out separately from the next implicit operator. 
    //Comment out the resulting offending casts in Test() as well.
    public static implicit operator MyCastableClass(MyClass l)
    {
        return new MyCastableClass();
    }

    //Try commenting this out separately from the previous implicit operator.
    //Comment out the resulting offending casts in Test() as well.
    public static implicit operator MyClass(MyCastableClass l)
    {
        return new MyClass();
    }

    static void Test()
    {
        MyDerivedClass derived = new MyDerivedClass();
        MyClass class1 = new MyClass();
        MyClass class2 = new MyDerivedClass();
        MyClass class3 = new MyCastableClass();
        MyCastableClass castableClass1 = new MyCastableClass();
        MyCastableClass castableClass2 = new MyClass();
        MyCastableClass castableClass3 = new MyDerivedClass();

        OverloadedMethod(derived); //Ambiguous call between OverloadedMethod(MyClass l) and OverloadedMethod(MyCastableClass l)
        OverloadedMethod(class1);
        OverloadedMethod(class2);
        OverloadedMethod(class3);
        OverloadedMethod(castableClass1);
        OverloadedMethod(castableClass2);
        OverloadedMethod(castableClass3);

    }

public class MyDerivedClass : MyClass {  }

public class MyCastableClass { }

There are two very interesting things to note:

  1. Commenting out any of the implicit operator methods removes the ambiguity.
  2. Trying to rename the first method overload in VS will rename the first four calls in the Test() method!

This naturally poses two questions:

  1. What it the logic behind the compiler error (i.e. how did the compiler arrive to an ambiguity)?
  2. Is there anything wrong with this design? Intuitively there should be no ambiguity and the offending call should be resolved in the first method overload (OverloadedMethod(MyClass l, MyClass r)) as MyDerivedClass is more closely related to MyClass rather than the castable but otherwise irrelevant MyCastableClass. Furthermore VS refactoring seems to agree with this intuition.

EDIT: After playing around with VS refactoring, I saw that VS matches the offending method call with the first overload that is defined in code whichever that is. So if we interchange the two overloads VS matches the offending call to the one with the MyCastableClass parameter. The questions are still valid though.

like image 722
GDS Avatar asked Oct 28 '14 10:10

GDS


2 Answers

What it the logic behind the compiler error (i.e. how did the compiler arrive to an ambiguity)?

First we must determine what methods are in the method group. Clearly there are two methods in the method group.

Second, we must determine which of those two methods are applicable. That is, every argument is convertible implicitly to the corresponding parameter type. Clearly both methods are applicable.

Third, given that there is more than one applicable method, a unique best method must be determined. In the case where there are only two methods each with only one parameter, the rule is that the conversion from the argument to the parameter type of one must be better than to the other.

The rules for what makes one conversion better than another is in section 7.5.3.5 of the specification, which I quote here for your convenience:

Given a conversion C1 that converts from a type S to a type T1, and a conversion C2 that converts from a type S to a type T2, C1 is a better conversion than C2 if at least one of the following holds:

• An identity conversion exists from S to T1 but not from S to T2

• T1 is a better conversion target than T2

Given two different types T1 and T2, T1 is a better conversion target than T2 if at least one of the following holds:

• An implicit conversion from T1 to T2 exists, and no implicit conversion from T2 to T1 exists

The purpose of this rule is to determine which type is more specific. If every Banana is a Fruit but not every Fruit is a Banana, then Banana is more specific than Fruit.

• T1 is a signed integral type and T2 is an unsigned integral type.

Run down the list. Is there an identity conversion from MyDerivedClass to either MyCastableClass or MyClass? No. Is there an implicit conversion from MyClass to MyCastableClass but not an implicit conversion going the other way? No. There is no reason to suppose that either type is more specific than the other. Are either integral types? No.

Therefore there is nothing upon which to base the decision that one is better than the other, and therefore this is ambiguous.

Is there anything wrong with this design?

The question answers itself. You've found one of the problems.

Intuitively there should be no ambiguity and the offending call should be resolved in the first method overload as MyDerivedClass is more closely related to MyClass

Though that might be intuitive to you, the spec does not make a distinction in this case between a user-defined conversion and any other implicit conversion. However I note that your distiction does count in some rare cases; see my article for details. (Chained user-defined explicit conversions in C#)

like image 83
Eric Lippert Avatar answered Sep 29 '22 10:09

Eric Lippert


What it the logic behind the compiler error?

Well, the compiler determines the signature based on a few things. The number and type of the parameters is next to the name, one of the most important ones. The compiler checks if a method call is ambiguous. It doesn't only use the actual type of the parameter, but also the types it can be implicitly casted to (note that explicit casts are out of the picture, they are not used here).

This gives the issue you describe.

Is there anything wrong with this design?

Yes. Ambiguous methods are a source of a lot of problems. Especially when using variable types, like dynamic. Even in this case, the compiler can't choose which method to call, and that is bad. We want software to be deterministic, and with this code, it can't be.

You didn't ask for it, but I guess the best option is:

  1. To rethink your design. Do you really need the implicit casts? If so, why do you need two methods instead of one?
  2. Use explicit casting instead of implicit casting, to make casting a deliberate choice a compiler can understand.
like image 24
Patrick Hofman Avatar answered Sep 29 '22 11:09

Patrick Hofman