C#的重载决策

c#,重载,决策 · 浏览次数 : 44

小编点评

重载是许多编程语言支持的特性,它可以定义多个名称相同但参数(个数、类型和顺序)不同的方法(函数)。 重载决策机制用于在给定参数列表和一组候选函数成员的情况下选择要调用的最佳函数成员。在C#中,重载决策选择最佳函数调用地过程是一致的,因为它根据参数列表和函数成员的规则从候选函数集合中找到最佳函数成员,并根据参数传递模式选择最适合的调用方式。 以下是重载决策的步骤: 1.根据给定的参数列表从候选函数集合中找到适用的函数成员。 2.从适用的候选函数成员集中找到最佳函数成员。 3.如果集合只包含一个函数成员,则该函数成员是最佳函数成员。 4.否则,根据更好的函数成员规则,找到相对于其他函数成员更好的一个函数成员作为最佳函数。 在方法调用(Method invocations)的描述中,子类中只要有一个方法适用,则父类的函数不是候选函数。这也就解释了开篇的例子中为何没有选择父类中参数类型完全匹配的函数。

正文

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

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修饰符,允许实参到对应形参存在隐式转换

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

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ᵢ更合适

更好的参数传递模式

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

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

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

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

与C#的重载决策相似的内容:

C#的重载决策

重载是许多编程语言支持的特性。所谓重载,就是指可以定义多个名称相同但参数(个数、类型和顺序)不同的方法(函数)。先来看一个例子: ```c# void Main() { char cvalue = 'a'; male m = new male(); m.write(cvalue); } class

c++临时对象导致的生命周期问题

对象的生命周期是c++中非常重要的概念,它直接决定了你的程序是否正确以及是否存在安全问题。 今天要说的临时变量导致的生命周期问题是非常常见的,很多时候没有一定经验甚至没法识别出来。光是我自己写、review、回答别人的问题就犯了或者看到了许许多多这类问题,所以我想有必要做个简单的总结,自己备忘的同时

[转帖]单字节字符串、宽字符串和多字节字符串

调查报告:了解单字节字符串、宽字符串 和多字节字符串等C/C++语言字符串表示方 法,总结其原理、存储、操作、应用等特 征,并编制报告。 单字节字符串(Single-Byte): 原理:每个字符用一个字节表示。这就决定了单字节字符集不可能包含256个以上 的字符。单字节字符包含拉丁文字母表,重音字符

C++ 重载运算符在HotSpot VM中的应用

C++支持运算符重载,对于Java开发者来说,这个可能比较陌生一些,因为Java不支持运算符重载。运算符重载本质上来说就是函数重载。下面介绍一下HotSpot VM中的运算符重载。 1、内存分配与释放 在C++中可以通过new运算符创建一个C++的类实例,这个操作实际上上包含了如下3个步骤: 调用o

[转帖]lua-book-元表

http://me.52fhy.com/lua-book/chapter9.html 在Lua5.1语言中,元表 (metatable) 的表现行为类似于 C++ 语言中的操作符重载,类似PHP的魔术方法。Python里也有元类(metaclass)一说。 通过元表,Lua有了更多的扩展特性。Lua

C++多态与虚拟:运算符重载(Operator Overloading)

运算符重载:与function overloading异曲同工的是,C++提供所谓的Operator overloading。所谓operators是像 +(加)-(減)*(乘)/(除)>>(位右移)<<(位左移)之类的符号,代表一种动作。 面对operators,我们应该把他想像是一种函数,只不过

C#.Net筑基-运算符Family

C#运算符 内置了丰富的运算符操作类型,使用方便,极大的简化了编码,同时还支持多种运算符重载机制,让自定义的类型也能支持运算符行为。

C# System.Threading.Timer 详解及示例

System.Threading.Timer 基于线程池的定时器,相较于另外几种定时器,其安全性较高,适用性最强,因此本文通过重载、属性、方法等方面介绍此定时器的相关内容。

C++指针和地址偏移在HotSpot VM中的应用

在前面我们介绍过new运算符,这个操作实际上上包含了如下3个步骤: 调用operator new的标准库函数。此函数会分配一块内存空间以便函存储相应类型的实例; 调用相应类的构造函数; 返回一个指向该对象的指针。 在第一步中,其实我们可以自己写个operator new函数对标准库函数进行重载,通常

用现代C++写一个python的简易型list

std::variant介绍:en.cppreference.com/w/cpp/utility/variant 通过泛型模板(仅提供了int, double, string三种类型的存储),实现了append, pop, front, back, size等方法,并且通过重载运算符实现了对负数索引