为什么在使用 long 作为参数时,long、float 和 decimal 方法之间的方法调用不明确?

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

我正在尝试构建一个 NpgsqlSimpleTypeHandler<ulong> 来处理将 ulong 值转换为 long 值,以便它们可以存储在我的 PostgreSQL 数据库中,反之亦然。

当我在使用 Convert.ToInt64(value)ulong 转换为 long 后尝试调用 Npgsql 的 Int64handler.ValidtaeAndGetLength 时,尝试传递新转换的 long 给出我有一个模棱两可的方法调用错误。

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

错误信息:

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

为什么即使 Int64Handler.ValidateAndGetLength(long, NpgsqlParameter?) 存在,我还是会遇到模棱两可的方法调用错误?

Npgsql Int64Handler documentation

[2020 年 7 月 21 日 14:23 EST] 更新标签以包含 C#-8.0

C#中的一些隐式类型转换规则是从Java中借用的,它允许从float到double的隐式转换,但反之不行,它允许从任何整数类型到float的隐式转换或加倍,但反之则不然。这有一个相当奇怪的效果,即从整数类型到 float 的转换被认为比到 double 的转换更可取,尽管精度损失更大。这在调用接受所有整数类型重载的函数的重载时不是问题,但在将整数值传递给接受 floatdouble 的函数时可能有点古怪。

C# 为 Java 的类型规则添加了另一个问题,但是,它的 decimal 类型。所有整数类型都可以隐式转换为 decimal,但 decimal 不能隐式转换为任何其他浮点类型或从任何其他浮点类型隐式转换。因此,decimal 不优于 floatfloat 也不优于 decimal。如果尝试将 long 传递给接受 floatdoubledecimal 但不准备接受 long 的函数,编译器将没有依据将 floatdecimal 视为优于另一个。具有讽刺意味的是,在大多数此类情况下,最好的类型是 double,并且 decimal 类型适合的情况相对较少,但它的存在有助于提醒程序员需要显式转换为 double,而不是让编译器默默地将 long 转换为 float

ValidateAndGetLength(long, NpgsqlParameter?)Int64Handlerclass中的其他重载的区别在于第一个方法实际上是覆盖classNpgsqlSimpleTypeHandler<long>中的方法实现INpgsqlSimpleTypeHandler<long>,其他方法直接实现INpgsqlSimpleTypeHandler<T>接口中的方法。

来自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).

“成员查找”部分对此进行了进一步扩展:

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.

我设法用示例代码重现了这个问题:

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
    }
}

您可以使用将处理程序转换为 NpgsqlSimpleTypeHandler<long> 并改为调用该方法的解决方法。