C# 的重载决策

2024-02-26 14:31:03 浏览数 (2)

前言

重载是许多编程语言支持的特性。所谓重载,就是指可以定义多个名称相同但参数(个数、类型和顺序)不同的方法(函数)。

正文

先来看一个例子

代码语言:javascript复制
void Main()
{
    char cvalue = 'a';
    male m = new male();
    m.write(cvalue);
}

class human
{
    public void write(char value)
    {
        Console.WriteLine("char:"   value);
    }
}

class male : human
{
    public void write(int value)
    {
        Console.WriteLine("int:"   value);
    }
}

这个例子中,父类human中有个一个参数类型为char的函数write,子类(derived class)male中提供了参数类型为int的重载函数,在Main方法中实例化了一个子类male的对象m,对象m调用write方法,并传递char类型的参数。

最终执行的结果是int:97,而不是char:a。为何在函数调用的时候,没有找到父类中参数类型完全匹配的函数,而是进行了类型转换呢?

有人说这是因为 .NET的类型推断(type inference)不够智能。其实,这里并不涉及类型推断,因为类型推断主要是针对隐式类型和泛型的。

这里是由重载决策机制决定的。重载决策是一种绑定时机制,用于在给定参数列表和一组候选函数成员的情况下选择要调用的最佳函数成员。

C# 中支持重载的有以下几种情况:

  • 方法(函数)重载
  • 构造函数重载
  • 索引器重载
  • 操作符重载

虽然上述四种情况都有自己独有地定义重载函数和参数列表的方式,但是重载决策选择最佳函数调用地过程是一致的。

首先,根据给定的参数列表从候选函数集合中找到适用的函数成员,如果没有找到则会报编译错误

然后,从适用的候选函数成员集中找到最佳函数成员。如果集合只包含一个函数成员,则该函数成员是最佳函数成员。

否则,根据更好的函数成员规则,找到相对于其他函数成员更好的一个函数成员作为最佳函数,如果没有一个函数成员优于所有其他函数成员,则函数成员调用不明确,并且会发生绑定时错误。

根据C# Language Specification中成员查找(Member lookup)的描述,方法调用的候选集不包括标记为override的方法。

根据方法调用(Method invocations)的描述,子类中只要有一个方法适用,则父类的函数不是候选函数。这也就解释了开篇的例子中为何没有选择父类中参数类型完全匹配的函数。

适用的函数成员

当满足以下所有条件时,函数成员被称为与参数列表A的适用函数成员:

参数列表A中的每实参都对应于函数成员声明中的一个参数,每个形参最多对应一个实参,并且任何没有实参对应的形参都是可选形参。

参数列表A中的每个实参,实参的传递模式与对应形参的传递模式相同。

  • 对于值类型参数或者参数数组,允许实参到对应形参存在隐式转换
  • 对于带有ref或者out修饰符的参数,允许实参到对应形参存在恒等转换(identity conversion)
  • 对于带有in修饰符的参数,允许实参到对应形参存在恒等转换(identity conversion)
  • 对于in传递模式,如果没有带in修饰符,允许实参到对应形参存在隐式转换

接下来用几个例子来说明函数是否适用

代码语言:javascript复制
void Main()
{
    int i = 10; uint ui = 34U;var vi=5;dynamic di=5;

    M1(in i);   // M1(in int)合适
    M1(in ui);  // 没有精准匹配, 因此M1(in int)不适用
    M1(i);      // M1(int) 和 M1(in int)都适用
    M1(ui);     // uint到int不存在隐式转换,所以M1(int) 不适用
    M1(in vi);   //恒等转换(identity conversion),M1(in int)适用

    M2(ui);     //uint隐式转换为long,因此M2(long)适用;同时传参没带in修饰符,允许装箱为object的隐式转换,M2(in object)适用
    M2(di);     //不适用
    M2(in di)   //恒等转换(identity conversion),M2(in object)适用
    M2(in vi)   //恒等转换(identity conversion),M2(in object)适用
    
    M3(ui)     //ref和out传递模式不支持忽略ref和out修饰符匹配,因此M3(ref object)不适用
}

public static void M1(int p1) { Console.WriteLine("M1 int:" p1); }
public static void M1(in int p1) { Console.WriteLine("M1 in int:" p1); }


public static void M2(long p1) { Console.WriteLine("M2 long:" p1); }
public static void M2(in object p1) { Console.WriteLine("M2 in object" p1); }

public static void M3(ref object p1) { Console.WriteLine("M3 ref object"   p1); }

更好的成员函数

假设调用函数时传递的参数为{E₁, E₂, ..., Eᵥ},有两个适用的函数Mᵥ(P₁, P₂, ..., Pᵥ)Mₓ(Q₁, Q₂, ..., Qᵥ),满足以下条件时则认为Mᵥ是更合适的函数:

  • 对于每一个参数,从EᵥQᵥ的隐式转换没有比EᵥPᵥ的隐式转换更好
  • 至少有一个参数满足,从EᵥPᵥ的转换比EᵥQᵥ的转换好。

如果按照上述规则比较,函数Mᵥ(P₁, P₂, ..., Pᵥ)Mₓ(Q₁, Q₂, ..., Qᵥ)是等价的(例如每个PᵢQᵢ是恒等转换关系),则继续根据以下规则判断更好的函数:

  • 如果Mᵢ是非泛型方法,而Mₑ是泛型方法,则认为Mᵢ更合适
  • 如果Mᵢ是普通方法,而Mᵢ是扩展方法,则认为Mᵢ更合适
  • 如果MᵢMᵢ都是扩展方法,并且Mᵢ的参数更少,则认为Mᵢ更合适
  • 如果Mᵢ的参数中有比Mᵢ的对应参数更具体地类型,则认为Mᵢ更合适

更好的参数传递模式

当两个重载方法中对应的形参仅在形参传递模式上不同,并且两个函数形参中的一个具有值传递模式,例如

代码语言:javascript复制
public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

前边适用的函数成员部分提到,调用M(10)方法时,两个重载方法都适用。这种情况下,值传递模式是更好的参数传递模式。

ref和out传递模式不支持忽略ref和out修饰符匹配,必须精准匹配

0 人点赞