3

I've stumbled upon this (in my point of view error) weird behavior, and I would appreciate it if someone could tell me why the compiler behaves like this, or maybe I've got lucky and found a compiler bug ;-)

All is compiled against .net6.0 (using VS 2022)

This is the first version of the code:

internal class Program
{

    private static void Main(string[] args)
    {
        long longValue = 0;
        string stringValue = "";
        bool boolValue = false;
        DateTime dateTimeValue = DateTime.Now;
        double doubleValue = 0;

        Console.Write("expect long => ");
        SetValue(val => longValue = val);

        Console.Write("expect string => ");
        SetValue(val => stringValue = val);

        Console.Write("expect bool => ");
        SetValue(val => boolValue = val);

        Console.Write("expect DateTime => ");
        SetValue(val => dateTimeValue = val);

        Console.ReadKey();
    }

    private static void SetValue(Action<long> setter)
    {
        Console.WriteLine("long called");
        setter(1337);
    }

    private static void SetValue(Action<string> setter)
    {
        Console.WriteLine("string called");
        setter("foo");
    }

    private static void SetValue(Action<bool> setter)
    {
        Console.WriteLine("bool called");
        setter(true);
    }

    private static void SetValue(Action<DateTime> setter)
    {
        Console.WriteLine("DateTime called");
        setter(DateTime.Now.AddDays(10));
    }
}

It produces this output:

expect long => long called
expect string => string called
expect bool => bool called
expect DateTime => DateTime called

Now I add a setValue call for doubleValue

        Console.Write("expect double => ");
        SetValue(val => doubleValue = val);

Here I'm expecting a compiler error complaining there is no suitable overloaded version of SetValue. But instead, it compiles fine and produces this output:

expect long => long called
expect string => string called
expect bool => bool called
expect DateTime => DateTime called
expect double => long called

Notice the last line where we can see that the program has called the wrong setValue version (at this point, there is no "right" setValue version for the double data type).

If I now add a SetValue version that handles doubles:

    private static void SetValue(Action<double> setter)
    {
        Console.WriteLine("double called");
        setter(13.37);
    }

the compiler complains CS0121 The call is ambiguous between the following methods or properties: 'Program.SetValue(Action<long>)' and 'Program.SetValue(Action<double>)'

I can now make the call unambiguous in several ways. First, as you can see in the final version of the code:

internal class Program
{

    private static void Main(string[] args)
    {
        long longValue = 0;
        string stringValue = "";
        bool boolValue = false;
        DateTime dateTimeValue = DateTime.Now;
        double doubleValue = 0;

        Console.Write("expect long => ");
        SetValue(val => longValue = val);

        Console.Write("expect string => ");
        SetValue(val => stringValue = val);

        Console.Write("expect bool => ");
        SetValue(val => boolValue = val);

        Console.Write("expect DateTime => ");
        SetValue(val => dateTimeValue = val);

        Console.Write("expect double => ");
        SetValue(new Action<double>(val => doubleValue = val));

        Console.Write("expect double => ");
        SetValue((double val) => doubleValue = val);

        Console.ReadKey();
    }

    private static void SetValue(Action<long> setter)
    {
        Console.WriteLine("long called");
        setter(1337);
    }


    private static void SetValue(Action<string> setter)
    {
        Console.WriteLine("string called");
        setter("foo");
    }

    private static void SetValue(Action<bool> setter)
    {
        Console.WriteLine("bool called");
        setter(true);
    }

    private static void SetValue(Action<DateTime> setter)
    {
        Console.WriteLine("DateTime called");
        setter(DateTime.Now.AddDays(10));
    }

    private static void SetValue(Action<double> setter)
    {
        Console.WriteLine("double called");
        setter(13.37);
    }
}

Output:

expect long => long called
expect string => string called
expect bool => bool called
expect DateTime => DateTime called
expect double => double called
expect double => double called

Why are these two versions (long, double) ambiguous for the compiler, and why is it still able to call the correct setValue version for the long variable but fails at the double value?

Erçin Dedeoğlu
  • 4,950
  • 4
  • 49
  • 69
White
  • 157
  • 9
  • 3
    They are ambiguous because they are both valid options - you can assign long to double just fine there is nothing wrong with compiler selecting this overload. Nether of the overloads is 'more' correct than the other - from compilers perspective; its just your opinion that Action is better - so you get the error. – Rafal Sep 09 '22 at 08:36
  • Possible duplicate of https://stackoverflow.com/questions/24011887/ambiguous-method-call-with-actiont-parameter-overload – Matthew Watson Sep 09 '22 at 08:49
  • @Rafal yes you can assign a long to a double but the compiler chooses the other way and assign a double to a long (which it normally complains about if you type longValue=doubleValue it states "Cannot implicit convert type") – White Sep 09 '22 at 09:10
  • 2
    @White "but the compiler chooses the other way and assign a double to a long" No it doesn't. Read your code again - it says `doubleValue = val`. `val` is a long. – Sweeper Sep 09 '22 at 09:14

1 Answers1

7

Here I'm expecting a compiler error complaining there is no suitable overloaded version of SetValue. But instead it compiles fine and produces this output:

The compiler looks at val => doubleValue = val and knows that val have to be something that can be assigned to a double. So it looks thru the list of options and find SetValue(Action<long> setter), long is assignable to double, so it selects this overload and everything is fine. Requiring the types to match exactly would likely lead just making everything more complicated with extra type-annotations everywhere.

Once you add SetValue(Action<double> setter) the compiler has two candidates that both fit the pattern, and it does not try to figure out which one is a better match. I'm not sure if this is due to technical difficulties, or to force the developer to clarify any ambiguities and avoid any possible unintentional behavior.

The language specification states the following

Specifically, an anonymous function F is compatible with a delegate type D provided:

...

  • If F has an implicitly typed parameter list, D has no ref or out parameters.

  • If the body of F is an expression, and either D has a void return type or F is async and D has the return type Task, then when each parameter of F is given the type of the corresponding parameter in D, the body of F is a valid expression (w.r.t §11) that would be permitted as a statement_expression (§12.7).

My interpretation of this is that it tries to convert the lambda to each of the possible delegates, and all but one fails due to type miss match. Once you add the Action<double> overload, two of the conversions succeed, and you have an ambiguity. So possibly the inference is done in the inverse order than might be implied by the first paragraph, but I'm not sure it matters if you just want an understanding of the principles.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • I think you are slightly missing the point here. The compiler matches long = double not double = long (the second is perfectly fine because a double can hold any value of a long in it) – White Sep 09 '22 at 09:12
  • I would love to see some quotes from the spec about this. I've trying to find how the lambda parameter types are inferred but can't find anything at all. All I could find was about inferring type arguments. – Sweeper Sep 09 '22 at 09:18
  • @White, I'm not sure what you mean with "The compiler matches long = double". Are you referring to the direction of inference? The compiler has no idea what type `val` is, so it has to look at how it is used, i.e. assignment to double, and what candidates are available, i.e. long, string, datatime, bool. Even if it starts by checking the candidates rather than the inverse, is there any practical difference? – JonasH Sep 09 '22 at 09:30
  • @Sweeper I added the information I could find in the language specifications, – JonasH Sep 09 '22 at 09:58
  • @JonasH Thank you. I must have missed it because the wording was totally not what I was expecting. "the body of F is a valid expression that would be permitted as a statement_expression" What a powerful requirement! – Sweeper Sep 09 '22 at 10:04