Why is there an ambiguous method call between long, float and decimal methods when using a long as a paramater?

Viewed 212

I am trying to build a NpgsqlSimpleTypeHandler<ulong> to handle converting ulong values to long values so they can be stored in my PostgreSQL database, and vise-versa.

When I try and call Npgsql's Int64handler.ValidtaeAndGetLength after converting a ulong to a long with Convert.ToInt64(value), attempting to pass the newly converted long gives me an ambiguous method call error.

private readonly Int64Handler handler;
...
public override int ValidateAndGetLength(ulong value, NpgsqlParameter? parameter)
{
    long val = Convert.ToInt64(value);
    return handler.ValidateAndGetLength(val, parameter);
}

Error Message:

Error   CS0121
The call is ambiguous between the following methods or properties:
'Int64Handler.ValidateAndGetLength(float, NpgsqlParameter?)' and
'Int64Handler.ValidateAndGetLength(decimal, NpgsqlParameter?)'

Why, when even with Int64Handler.ValidateAndGetLength(long, NpgsqlParameter?) exsisting, do I wind up with the ambiguous method call error?

Npgsql Int64Handler documentation

[7/21/2020 14:23 EST] Updated tags to include C#-8.0

2 Answers

The difference between ValidateAndGetLength(long, NpgsqlParameter?) and other overloads in the Int64Handler class is that the first method is actually overriding the method in the class NpgsqlSimpleTypeHandler<long> that implements INpgsqlSimpleTypeHandler<long>, and other methods are implementing the methods in the INpgsqlSimpleTypeHandler<T> interface directly.

From C# 6 draft language specification:

For example, the set of candidates for a method invocation does not include methods marked override (Member lookup), and methods in a base class are not candidates if any method in a derived class is applicable (Method invocations).

The "Member lookup" section expands on this further:

First, a set of accessible members named N is determined:

If T is a type parameter, then the set is the union of the sets of accessible members named N in each of the types specified as a primary constraint or secondary constraint (Type parameter constraints) for T, along with the set of accessible members named N in object.

Otherwise, the set consists of all accessible (Member access) members named N in T, including inherited members and the accessible members named N in object. If T is a constructed type, the set of members is obtained by substituting type arguments as described in Members of constructed types. Members that include an override modifier are excluded from the set.

I managed to reproduce the issue with the sample code:

using System;

interface IA<T>
{
    int X(T x);
}

abstract class A<T> : IA<T>
{
    public abstract int X(T x);
}

class B : A<long>, IA<float>, IA<decimal>
{
    public override int X(long x)
    {
        return 8;
    }
    
    public int X(float x)
    {
        return 8;
    }
    
    public int X(decimal x)
    {
        return 8;
    }
}
                    
public class Program
{
    public static void Main()
    {
        var b = new B();
        var a = (A<long>)b;
        Console.WriteLine(a.X(42L));
        // Console.WriteLine(b.X(42L)); // broken
    }
}

You can use the workaround of casting the handler to NpgsqlSimpleTypeHandler<long> and calling that method instead.

Some of the implicit type conversion rules in C# are borrowed from those of Java, which allow implicit conversion from float to double, but not vice versa, and which allow implicit conversions from any integer type to float or double, but not vice versa. This has the rather curious effect of causing conversions from integer types to float to be regarded as unambiguously preferable to conversions to double, despite the greater loss of precision. This wouldn't be a problem when calling overloads of a function which accepts overloads of all integer types, but can be a bit quirky when passing an integer value to a function that accepts both float and double.

C# adds another wrinkle to Java's type rules, however, with its decimal type. All integer types may be implicitly converted to decimal, but decimal cannot implicitly be converted to or from any other floating-point type. Thus, decimal isn't preferable to float, and nor is float preferable to decimal. If an attempt is made to pass a long to a function which would accept float, double, and decimal, but isn't prepared to accept long, a compiler would have no basis for regarding either float or decimal as being preferable to the other. Ironically, in most such cases the best type would be double, and there are relatively few cases where the decimal type would be appropriate, but its existence helps alert the programmer to the need to explicitly convert to double, rather than having the compiler silently convert the long to float.

Related