Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# compiler guessing data type wrong

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?

like image 214
White Avatar asked Apr 13 '26 19:04

White


1 Answers

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.

like image 193
JonasH Avatar answered Apr 15 '26 09:04

JonasH



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!