静态方法、实例方法和虚方法的区别
基础知识
对于面向对象的语言来说,所有类型都是从System.Object类型派生,正是由于这个原因,保证了每个类型都有一组最基本的方法,也就是从他们的父类System.Object继承来的方法,Object的定义如下面的代码所示,System.Object所定义的基本方法中基本包含了CLR所有的方法类型,静态方法(Static修饰,属于类成员)、虚方法(Virtural修饰,属于实例成员)、实例方法(普通的方法,属于实例成员)。可能会有人说还有抽象方法,其实抽象方法最后的编译也是一个虚方法。
CLR的最重要的特性之一就是类型安全性,在运行时,CLR总是知道一个对象是什么类型,我们看到Object中有一个GetType方法,这个方法总是能知道一个对象的确切类型,由于GetType是一个非虚实例方法,从而保证了派生类型不能重写它,所以一个类型不可能伪装成另一个类型,其实如果我们要有意的隐藏也是可以做到的(我们可以使用New关键字覆盖GetType方法),不过一般我们不推荐这样做。
那么GetType方法是如何返回一个对象的真实类型的呢?这就要引入一个新的概念,也是是“类型对象”,当我们使用New关键字在托管堆上创建一个对象的时候,大致做了一下几件事情:
class Program { static void Main(string[] args) { Person p = new Person("Aseven"); Console.ReadKey(); } } public class Person { private string _name; private int _age; public string Name { get { return _name; } set { _name = value; } } public virtual void Say() { Console.WriteLine("******"); } public static Person Find(string name) { return new Person(name);//模拟数据库查找 } public int GetAge() { return _age; } public Person() { } public Person(string name) { this._name = name; } }
1、计算类型和所有基类型(直到System.Object,虽然它没有定义实例字段)的所有实例字段(注意:没有静态字段和方法)所需要的字节数,堆上的每个对象都需要一些额外的成员---即类型对象指针和同步索引块,这些成员由CLR用于管理对象,也会计入对象大小。
2、从托管堆上分配对象的内存,并把字段初始化为零(0)。
3、初始化对象的“类型对象指针”和“同步索引块”。
4、执行实例构造函数,并向其传入在对new的调用中指定的实参(上例中的“Aseven”),大多数编译器会自动生成代码来调用基类的构造器,最终调用的而是System.Object的构造器。
在New执行了之后,会返回对堆中对象的一个引用(或指针),对上例来说,这个引用(地址)保存在变量e中,创建完这个Person对象之后,内存结构大致如下,可以看到类型对象指针指向的就是person的类型对象。
总结:一个实例对象创建之后,变量e保存了托管堆中的person对象的一个引用(指针)。而person对象指示保存了对象的一个实例字段(包括类型对象指针和同步索引块),至于静态字段、方法列表都保存在person的类型对象中,特别注意的是方法的列表,这个列表包含了静态方法、实例方法、虚方法,下面我们就来介绍对于这三种方法是如何调用的。
方法的调用
1、静态方法:当调用一个静态方法时,CLR会定位与定义静态方法对应的类型对应的类型对象(有点绕)。然后在类型对象的方法列表中查找对应的记录项,进行JIT编译(如果需要),然后调用。
2、实例方法:当调用一个非虚实例方法时,JIT编译器会找到发出调用的那个变量(p)对应的类型对应的类型对象,如果类型对象的方法列表中没有包含那个被调用的方法,JIT编译器会回溯类层次结构(一直回溯到Object),并在沿途的每个类型的方法集合中查找此方法,之所以能这样回溯,是因为每个类型对象都有一个字段引用了他的积累性,这个信息在途中没有显示。
3、虚方法:当调用一个虚方法时,会生成一些额外的代码,方法每次调用时,都会执行这些代码,这些代码首先检查发出调用的变量,然后会跟随地址(也就是我们说的p中保存的对象的指针)来到发出调用的对象,然后代码检查对象的内部的“类型对象指针”成员,这个成员指向对象的类型对象,然后代码在类型对象的方法集合中查找该方法,进行JIT编译(如果需要的话),在调用JIT编译的代码。如果没有则也会向上回溯查找基类中定义的方法。
下面用示例进行介绍:
class Program { static void Main(string[] args) { Person p = new Person("test1"); p = Person.Find("Aseven"); int Age = p.GetAge(); p.Say(); Console.ReadKey(); } } public class Person { private string _name; private int _age; public string Name { get { return _name; } set { _name = value; } } public virtual void Say() { Console.WriteLine("******"); } public static Person Find(string name) { return new Chinese(name);//模拟数据库查找 } public int GetAge() { return _age; } public Person() { } public Person(string name) { this._name = name; } } public class Chinese : Person { public Chinese(string name) { this.Name = name; } public override void Say() { Console.WriteLine("你好!"); } } public class American : Person { public American(string name) { this.Name = name; } public override void Say() { Console.WriteLine("Hello!"); } }
1、首先我们定义Person对象,Person p=new Person();这句代码执行之后和上面的内存分配基本类似。
2、我们调用Person的静态方法,p=Person.Find("Aseven");根据上面的定义,调用一个静态方法时会直接查找类型对象的方法列表,直接调用,调用之后我们看到Find方法中直接返回了一个Chinese对象,这会在托管堆中创建一个chinese对象,并且把地址存储在变量P中,这时P中保存的不在是Person对象的地址,而是Chinese对象的地址(当然也可能会是一个American对象,如果Find返回返回的是一个American对象)。
3、然后我们调用p.GetAge()的一个非虚实例方法,当CLR调用一个非虚的实例方法时,会根据发出调用者(P)的类型(Person)的类型对象(Person类型对象)去查找GetAge方法,如果找不到则回溯基类查找。这里由于Person的类型对象的方法集合中有这个方法,所以直接调用。将返回的结果(这里是0)存储到线程栈的一个Age的变量中。
4、调用P.Say()方法,Say方法是一个虚方法,CLR会根据发出调用者(P)的地址(这里是指向Chinese对象的一个指针)找到托管堆中真实的对象(chinese对象),然后根据托管堆中的对象去找到真实的类型对象(这里是Chinese类型对象),并且遍历方法集合查找Say方法,(如遇Chinese类重写了Say方法,所以Chinese类型对象的方法集合中有这个方法)进行调用。
这是内存的分配大致如下:由于person对象已经没有其它对象引用了,那么它将是下次垃圾回收的重点对象。
测试Demo
public class A { public void MethodF() { Console.WriteLine("A.F"); } public virtual void MethodG() { Console.WriteLine("A.G"); } } public class B : A { new public void MethodF() { Console.WriteLine("B.F"); } public override void MethodG() { Console.WriteLine("B.G"); } } class Test { static void Main() { B b; b = new B(); A a = b; a.MethodF(); b.MethodF(); a.MethodG(); b.MethodG(); }
输出结果:A.F、B.F、B.G、B.G
1、首先MethodF是一个非虚实例方法,这时候我们用a.MethodF();由于a是A类型的实例,所以输出的是A.F。
2、接着调用b.MethodF(),因为b是一个B类型的实例,且B重写了A的MethodF方法,那么在B类型对象的类型对象的方法表中就已经有了MethodF方法,会直接调用,所以输出的是B.F
3、由于MethodG是一个虚方法,我们用a.MethodG调用的时候,首先会根据a中保存的地址(指针)找到托管堆中的具体对象,然后根据具体对象找到真实的类型对象,这里a中保存的是一个b的实例,所以对象的类型对象也就是B的类型对象的类型对象,
调用的时候则会直接查找B类型对象的类型对象中的方法集合,查找MethodG方法并调用,所以输出的是B.G
4、对于b.MehtodG,首先会根据b中保存的地址(指针)找到托管堆中的具体对象,然后根据具体对象找到真实的类型对象,这里b中保存的是一个b的实例,所以对象的类型对象也就是B的类型对象的类型对象,调用的时候则会直接查找B类型对象的类型对象 中的方法集合,查找MethodG方法并调用,所以输出的是B.G
总结
1、方法的调用都是通过查找类型对象中的方法集合来实现的。
2、静态方法直接查找类型对象中方法集合进行调用。
3、非虚实例方法是根据发出调用者(对于上面的Demo,线程栈中的变量a、b是发出调用者)的类型去查找对应的类型对象,然后查找该类型对象的方法集合进行调用,没有找到则回溯基类进行查找。
4、虚方法是根据发出调用者(对于上面的Demo,线程栈中的变量a、b是发出调用者)的地址找到托管堆中的具体对象,然后根据对象去查找真实的类型对象,再根据类型对象去查找方法集合进行的。