具有相同名称的静态方法和扩展方法

本文关键字:静态方法 扩展 方法 | 更新日期: 2023-09-27 18:27:06

我创建了扩展方法:

public static class XDecimal
{
    public static decimal Floor(
        this decimal value,
        int precision)
    {
        decimal step = (decimal)Math.Pow(10, precision);
        return decimal.Floor(step * value) / step;
    }
}

现在我尝试使用它:

(10.1234m).Floor(2)

但编译器说Member 'decimal.Floor(decimal)' cannot be accessed with an instance reference; qualify it with a type name instead。我知道有静态decimal.Floor(decimal)方法。但它有不同的特征。为什么编译器无法选择正确的方法?

具有相同名称的静态方法和扩展方法

这里有两个很好的正确答案,但我知道简单引用规范的答案并不总是那么有启发性。让我补充一些额外的细节。

你可能有一个过载分辨率的心理模型,它是这样的:

  • 把所有可能的方法放在一个大桶里——扩展方法、静态方法、实例方法等等
  • 如果要使用的方法会出错,请将它们从bucket中删除
  • 在剩下的方法中,选择参数表达式与参数类型最匹配的唯一方法

尽管这是许多人超负荷解决的心理模式,但令人遗憾的是,这是一个微妙的错误。

真实的模型——我将忽略这里的一般类型推理问题——如下所示:

  • 将所有实例和静态方法放在一个bucket中。虚拟重写不算作实例方法
  • 消除由于参数与参数不匹配而不适用的方法

在这一点上,我们要么有方法,要么没有。如果我们在bucket中有任何方法,则不会检查扩展方法。这是最重要的一点。模型是而不是"如果正常的过载解析产生了错误,那么我们检查扩展方法"。模型是"如果正常的过载解析没有产生任何适用的方法,那么我们检查扩展方法"。

如果bucket中有方法,那么就会更多地消除基类方法,最后根据参数与参数的匹配程度来选择最佳方法。

如果这种情况恰好选择了一个静态方法,那么C#将假设您打算使用类型名称并错误地使用了实例,而不是希望搜索扩展方法。重载解析已经确定有一个实例或静态方法的参数与您给出的参数匹配,并且它要么选择其中一个,要么给出错误;它不会说"哦,你可能是想调用这个恰好在范围内的古怪扩展方法"。

从你的角度来看,我理解这是令人烦恼的。您显然希望模型是"如果重载解析产生错误,则返回到扩展方法"。在你的例子中,这是有用的,但这种行为在其他情况下会产生糟糕的结果。例如,假设你有类似的东西

mystring.Join(foo, bar);

这里给出的错误是它应该是string.Join。如果C#编译器说"哦,string.Join是静态的。用户可能想使用对字符序列进行连接的扩展方法,让我试试……",然后你会收到一条错误消息,说序列连接运算符(与这里的代码无关)没有正确的参数,那就太奇怪了。

或者更糟的是,如果你奇迹般地给了它有效的参数,但打算调用静态方法,那么你的代码就会以一种非常奇怪且难以调试的方式被破坏。

扩展方法是在游戏后期添加的,查找它们的规则使它们故意倾向于给出错误,而不是神奇地工作。这是一个安全系统,以确保扩展方法不受意外约束。

决定调用哪个方法的过程在C#语言规范中有很多小细节描述。适用于您的场景的关键点是,只有当编译器在接收类型本身的方法(即decimal)中找不到要调用的方法时,才考虑调用扩展方法。

以下是规范的相关部分:

构建了方法调用的候选方法集。对于与方法组M相关联的每个方法F:

  • 如果F是非泛型的,则F是以下情况下的候选者:

  • M没有类型参数列表,并且

  • F适用于A(§7.5.3.1)。

根据以上内容,double.Floor(decimal)是有效的候选。

如果生成的候选方法集为空,则放弃按照以下步骤进行的进一步处理,而是尝试将调用作为扩展方法调用进行处理(§7.6.5.2)。如果失败,则不存在适用的方法,并发生绑定时间错误。

在您的情况下,候选方法集不是空的,因此不考虑扩展方法。

decimal.Floor的签名是

public static Decimal Floor(Decimal d);

我不是类型推理方面的专家,但我想,由于存在从intDecimal的隐式转换,编译器选择此方法作为最佳拟合方法。

如果您将签名更改为

public static decimal Floor(
    this decimal value,
    double precision)

并称之为

(10.1234m).Floor(2d)

它是有效的。当然,double这样的精度有些奇怪。

编辑:引用Eric Lippert关于算法的话:

任何接收类型的方法都比任何扩展方法更接近。

Floor是一种"接收类型"(Decimal)的方法。关于为什么C#开发人员这样实现它,我无法发表任何声明。