(转)把C#中方法重载说透
首先我们说一下什么是方法重载。在面对对象这样的高级语言中都允许我们在一个类中定义多个方法名相同、方法间参数个数和参数顺序不同的方法,对于参数个数不同或者参数列表不同的情况我们称之为参数列表不同。需要注意的是这里没有提到方法的返回值。也就是决定方法是否构成重载有三个条件:
(1)在同一个类中;
(2)方法名相同;
(3)参数列表不同。
例如下面的代码:
- public void Show()//(1)
- {
- Console.WriteLine("Nothing");
- }
- public void Show(int number)//(2)
- {
- Console.WriteLine(number);
- }
- /*
- public int Show(int number)//(3)
- {
- Console.WriteLine(number);
- return number % 5;
- }
- */
对于上面的代码,(1)没有参数,(2)使用了一个int类型的参数,(1)和(2)之间就构成了重载。(2)与(3)相比仅仅返回值不同,虽然重载不关心返回值的不同,但是在C#中不允许存在方法名和参数列表相同、返回值不同的方法,所以(2)和(3)不能同时存在于代码中,(3)如果不注释掉上面的代码是没有办法通过编译的。
上面我们仅仅讨论了重载的一些基本常识,下面我们探讨一下一些情况稍微复杂的重载情况。
首先我们看第一个版本:
- using System;
- using System.Collections.Generic;
- using System.Text;
- namespace OverrideDemo
- {
- /// <summary>
- /// 说明:本实例用于讲述重载的关系
- /// 作者:周公
- /// 日期:2008-09-04
- /// 首发地址:http://blog.csdn.net/zhoufoxcn
- /// </summary>
- class Program
- {
- static void Main(string[] args)
- {
- String s = null;
- Show(s);
- Object o = "123";
- Show(o);
- }
- static void Show(string s)
- {
- Console.WriteLine("String");
- }
- static void Show(Object o)
- {
- Console.WriteLine("Object");
- }
- }
- }
大家猜猜这个程序的运行结果是什么?
以下是程序运行结果:
String
Object
对以上代码进行分析,我们发现Show()方法有两种形式,一种是string类型的参数,一种是object类型参数,在一个类中存在方法名相同、参数列表不同(参数个数或者参数类型不同)的现象我们称之为overloading,即重载。不过这里的Show()方法的参数比较特殊,因为string类继承于Object类,也就是Show()方法的参数存在一种继承关系。从结果我们可以得出两点结论:
(1)从String s = null;Show(s);最后调用的是static void Show(string s)这个方法我们可以得出,C#中方法调用是精确匹配的,也就是s是string类型,虽然string类型继承自object类型,尽管static void Show(Object o)也满足条件,但是方法声明中static void Show(string s)这个声明与s类型的最接近(因为s是string类型,与它最接近),所以执行static void Show(string s),而不执行static void Show(Object o)这个方法。
(2)从Object o = "123"; Show(o);最后调用的是static void Show(Object o)这个方法我们可以得出,C#中如果存在方法重载,会根据其refrence type(引用类型)来调用对象的方法,而不是根据instance type(实例类型)来调用。尽管”123”是string类型,但是它的refrence type是object类型的,所以会调用static void Show(Object o)这个方法而不是static void Show(string s)。
上面的Main()方法的IL代码如下:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 24 (0x18)
.maxstack 1
.locals init ([0] string s,
[1] object o)
IL_0000: nop
IL_0001: ldnull
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: call void OverrideDemo.Program::Show(string)
IL_0009: nop
IL_000a: ldstr "123"
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: call void OverrideDemo.Program::Show(object)
IL_0016: nop
IL_0017: ret
} // end of method Program::Main
从上面的IL代码我们可以看出对于string s=null;这句代码在IL中表示为:ldnull。
再根据上面的结论,我们看下面的代码:
- using System;
- public class Program
- { public static void Main()
- {
- Show(null);
- Show("");
- Show(1);
- }
- static void Show(Object o)
- {
- Console.WriteLine("Object");
- }
- static void Show(String s)
- {
Console.WriteLine("String");
-
}
-
}
猜猜上面的代码执行结果会是怎样的?
以下是程序运行结果:
String
Object
Object
从上面的运行结果我们可以得出以下结论:
(1)从Show(null)最后调用的是static void Show(String s)方法我们更进一步可以说在C#中是方法调用尽量精确匹配的。尽管null我们可以理解为一个空object对象或者一个空字符串,但是在这里C#还是精确为派生类。这就像我们没有钱,可以说没有一分钱也可以说没有500英镑,但是没有一分钱自然就没有500亿英镑,所以我们跟别人说没有钱的时候没有必要说没有500亿英镑一样。在这里自然null就表示空字符串。所以Show(null)这个方法会调用static void Show(String s)这个方法。
这有点像下面的情况:
一次活动大会上,主持人说:“身高不到1.60m的请坐在1到3排,身高不到1.75m的请做到4到6排,其他的请随便坐。”
上面的语句似乎有些逻辑方面的问题,应该说身高超过1.60m但是不到1.75m的请坐到4到6排。但是如果你面对着一群拿着枪的强盗,他说上面的话时,恰好你也在场并且你的身高是1.55m,你会坐到哪一排?你总不可能冒着挨一枪的危险去纠正他的逻辑错误吧?最好的办法是坐到1到3排。因为无论怎么说你的身高是绝对满足不到1.60m这个条件的(尽管你的身高也满足强盗说的第二个条件,即身高不到1.75米,但是你肯定不会冒这个危险,从上下句的意思我们也能推断出人家的意思就是身高在1.61m到1.74m之间的人坐4到6排)。
在上面的代码中,你在运行环境的眼中就是一个持枪的强盗,虽然null可以理解为null类型的string或者null类型的object,但是它不能向你问清楚这个到底是null类型的string或者null类型的object,因为string是Object的派生类,所以它按照null类型的string来调用相应的方法了。
(2)从Show("")最后调用static void Show(String s)这个方法进一步证明了方法调用是尽量选择参数最匹配的那个执行。因为Show("")相当于:string s = ""; Show(s);s的引用类型是string,所以会调用static void Show(String s)这个方法。
我们在这里可以假设一下:假如存在一个类A是String类的派生类(实际上string类是sealed的,也就是不可继承的,所以我说了是假设),并且存在在上面的代码改变如下:
- using System;
- public class Program
- { public static void Main()
- {
- Show(null);
- Show("");
- }
- static void Show(Object o)
- {
- Console.WriteLine("Object");
- }
- static void Show(String s)
- {
- Console.WriteLine("String");
- }
- static void Show(A a)//假设A是String的派生类,当然实际上String类并没有派生类,这里仅仅是假设
- {
- Console.WriteLine("A");
- }
- }
如果上面的假设成立,上面的代码运行结果应该如下:
A
String
(3)为什么Show(1)会调用static void Show(Object o)这个方法呢?在这个类中与Show(1)最精确的方法重载应该是static void Show(int i)这种方法声明,但是方法中没有,因为int是继承自ValueType类,所以如果没有static void Show(int i)这种声明,那么其次接近的声明应该是static void Show(ValueType v)这种声明,可惜方法中依然没有,不过ValueType类继承自Object类,所以比static void Show(ValueType v)还次一点的方法重载声明应该是static void Show(Object o),而类中也确实存在这种声明,所以会调用static void Show(Object o)这个方法。当然从int到Object这个过程中存在一次box,也就是装箱(装箱是从值类型到引用类型的转换),这个可以从下面的IL代码可以看出来。
以下是第二种情况下Main()方法的IL代码:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 32 (0x20)
.maxstack 8
IL_0000: nop
IL_0001: ldnull
IL_0002: call void OverrideDemo.Program::Show(string)
IL_0007: nop
IL_0008: ldstr ""
IL_000d: call void OverrideDemo.Program::Show(string)
IL_0012: nop
IL_0013: ldc.i4.1
IL_0014: box [mscorlib]System.Int32
IL_0019: call void OverrideDemo.Program::Show(object)
IL_001e: nop
IL_001f: ret
} // end of method Program::Main
下面我们对第二种情况的代码做一些变化,代码如下:
- using System;
- using System.Collections.Generic;
- using System.Text;
- namespace OverrideDemo
- {
- /// <summary>
- /// 说明:本实例用于讲述重载的关系
- /// 作者:周公
- /// 日期:2008-09-04
- /// 首发地址:http://blog.csdn.net/zhoufoxcn
- /// </summary>
- class Program
- {
- static void Main(string[] args)
- {
- Show(null);
- Show("");
- Show(1);
- }
- static void Show(string s)
- {
- Console.WriteLine("String");
- }
- static void Show(Object o)
- {
- Console.WriteLine("Object");
- }
- static void Show(Program p)//Program是当前方法所在的类
- {
- Console.WriteLine("Program");
- }
- }
- }
上面的代码的运行结果是什么,你能猜出来吗?
哈哈,上面的程序代码是没有运行结果的,因为它没有办法编译!编译情况如下:
为什么不能通过编译呢?
原因就出在Show(null)这个方法这里!如果仅仅有static void Show(string s)和static void Show(Object o)方法构成重载关系,那么null我们既可以理解为空string引用也可以理解为空Object引用,因为string类型的限制更精确一些,所以C#会按照最精确地匹配成string类型,因而会执行static void Show(string s)这个方法。这是在前面的代码中已经被证明的。可是现在多了一个static void Show(Program p)方法的重载,null既可以理解成空string类型引用,也可以理解成空Program类型引用,因为string类和Program类都是Object类的派生类,所以按照前面的推论自然不会当成空Object类型的引用。因为String类和Program类之间不存在继承关系,按照最精确匹配原则,编译器无法决定匹配成String类还是Program类最精确,所以编译无法通过。