C#中的多态性

相信大家都对面向对象的三个特征封装、继承、多态很熟悉,每个人都能说上一两句,但是大多数都仅仅是知道这些是什么,不知道CLR内部是如何实现的,所以本篇文章主要说说多态性中的一些概念已经内部实现的机理。

一、多态的概念

首先解释下什么叫多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。换句话说,实际上就是同一个类型的实例调用“相同”的方法,产生的结果是不同的。这里的“相同”打上双引号是因为这里的相同的方法仅仅是看上去相同的方法,实际上它们调用的方法是不同的。

说到多态,我们不能免俗的提到下面几个概念:重载、重写、虚方法、抽象方法以及隐藏方法。下面就来一一介绍他们的概念。

1、重载(overload):在同一个作用域(一般指一个类)的两个或多个方法函数名相同,参数列表不同的方法叫做重载,它们有三个特点(俗称两必须一可以):

  • 方法名必须相同
  • 参数列表必须不相同
  • 返回值类型可以不相同

如:

        public void Sleep()
        {
            Console.WriteLine("Animal睡觉");
        }
        public int Sleep(int time)
        {
            Console.WriteLine("Animal{0}点睡觉", time);
            return time;
        }

2、重写(override):子类中为满足自己的需要来重复定义某个方法的不同实现,需要用override关键字,被重写的方法必须是虚方法,用的是virtual关键字。它的特点是(三个相同):

  • 相同的方法名
  • 相同的参数列表
  • 相同的返回值。

如:父类中的定义:

        public virtual void EatFood()
        {
            Console.WriteLine("Animal吃东西");
        } 

子类中的定义:

        public override void EatFood()
        {
            Console.WriteLine("Cat吃东西");
            //base.EatFood();
        }

tips:经常有童鞋问重载和重写的区别,而且网络上把这两个的区别作为C#做常考的面试题之一。实际上这两个概念完全没有关系,仅仅都带有一个“重”字。他们没有在一起比较的意义,仅仅分辨它们不同的定义就好了。

3、虚方法:即为基类中定义的允许在派生类中重写的方法,使用virtual关键字定义。如:

        public virtual void EatFood()
        {
            Console.WriteLine("Animal吃东西");
        }

注意:虚方法也可以被直接调用。如:

            Animal a = new Animal();
            a.EatFood();

运行结果:

4、抽象方法:在基类中定义的并且必须在派生类中重写的方法,使用abstract关键字定义。如:

    public abstract class Biology
    {
        public abstract void Live();
    }
    public class Animal : Biology
    {
        public override void Live()
        {
            Console.WriteLine("Animal重写的抽象方法");
            //throw new NotImplementedException();
        }
    }

注意:抽象方法只能在抽象类中定义,如果不在抽象类中定义,则会报出如下错误:

虚方法和抽象方法的区别是:因为抽象类无法实例化,所以抽象方法没有办法被调用,也就是说抽象方法永远不可能被实现。

5、隐藏方法:在派生类中定义的和基类中的某个方法同名的方法,使用new关键字定义。如在基类Animal中有一方法Sleep():

        public void Sleep()
        {
            Console.WriteLine("Animal Sleep");
        }

则在派生类Cat中定义隐藏方法的代码为:

        new public void Sleep()
        {
            Console.WriteLine("Cat Sleep");
        }

            或者为:

        public new void Sleep()
        {
            Console.WriteLine("Cat Sleep");
        }    

注意:(1)隐藏方法不但可以隐藏基类中的虚方法,而且也可以隐藏基类中的非虚方法。

(2)隐藏方法中父类的实例调用父类的方法,子类的实例调用子类的方法。

(3)和上一条对比:重写方法中子类的变量调用子类重写的方法,父类的变量要看这个父类引用的是子类的实例还是本身的实例,如果引用的是父类的实例那么调用基类的方法,如果引用的是派生类的实例则调用派生类的方法。

好了,基本概念讲完了,下面来看一个例子,首先我们新建几个类:

    public abstract class Biology
    {
        public abstract void Live();
    }
    public class Animal : Biology
    {
        public override void Live()
        {
            Console.WriteLine("Animal重写的Live");
            //throw new NotImplementedException();
        }
        public void Sleep()
        {
            Console.WriteLine("Animal Sleep");
        }
        public int Sleep(int time)
        {
            Console.WriteLine("Animal在{0}点Sleep", time);
            return time;
        }
        public virtual void EatFood()
        {
            Console.WriteLine("Animal EatFood");
        }
    }
    public class Cat : Animal
    {
        public override void EatFood()
        {
            Console.WriteLine("Cat EatFood");
            //base.EatFood();
        }
        new public void Sleep()
        {
            Console.WriteLine("Cat Sleep");
        }
        //public new void Sleep()
        //{
        //    Console.WriteLine("Cat Sleep");
        //}
    }
    public class Dog : Animal
    {
        public override void EatFood()
        {
            Console.WriteLine("Dog EatFood");
            //base.EatFood();
        }
    }

下面来看看需要执行的代码:

    class Program
    {
        static void Main(string[] args)
        {
            //Animal的实例
            Animal a = new Animal();
            //Animal的实例,引用派生类Cat对象
            Animal ac = new Cat();
            //Animal的实例,引用派生类Dog对象
            Animal ad = new Dog();
            //Cat的实例
            Cat c = new Cat();
            //Dog的实例
            Dog d = new Dog();
            //重载
            a.Sleep();
            a.Sleep(23);
            //重写和虚方法
            a.EatFood();
            ac.EatFood();
            ad.EatFood();
            //抽象方法
            a.Live();
            //隐藏方法
            a.Sleep();
            ac.Sleep();
            c.Sleep();
            Console.ReadKey();
        }
    }

首先,我们定义了几个我们需要使用的类的实例,需要注意的是

(1)Biology类是抽象类,无法实例化;

(2)变量ac是Animal的实例,但是指向一个Cat的对象。因为Cat类型是Animal类型的派生类,所以这种转换没有问题。这也是多态性的重点。

下面我们来一步一步的分析:

(1)

            //重载
            a.Sleep();
            a.Sleep(23);

很明显,Animal的变量a调用的两个Sleep方法是重载的方法,第一句调用的是无参数的Sleep()方法,第二句调用的是有一个int 参数的Sleep方法。注意两个Sleep方法的返回值不一样,这也说明了重写的三个特征中的最后一个特征——返回值可以不相同。

运行的结果如下:

(2)

            //重写和虚方法
            a.EatFood();
            ac.EatFood();
            ad.EatFood();

在这一段中,a、ac以及ad都是Animal的实例,但是他们引用的对象不同,a引用的是Animal对象,ac引用的是Cat对象,ad引用的是Dog对象,这个差别会造成执行结果的什么差别呢,请看执行结果:

第一句Animal实例,直接调用Animal的虚方法EatFood,没有任何问题。

在第二、三句中,虽然同样是Animal的实例,但是他们分别指向Cat和Dog对象,所以调用的Cat类和Dog类中各自重写的EatFood方法,就像是Cat实例和Dog实例直接调用EatFood方法一样。这个也就是多态性的体现:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

(3)

            //抽象方法
            a.Live();

这个比较简单,就是直接重写父类Biology中的Live方法,执行结果如下:

(4)

            //隐藏方法
            a.Sleep();
            ac.Sleep();
            c.Sleep();

在分析隐藏方法时要和虚方法、重写相互比较。变量 a 调用 Animal 类的 Sleep 方法以及变量 c 调用 Cat 类的 Sleep 方法没有异议,但是变量 ac 引用的是一个 Cat 类型的对象,它应该调用 Animal 类型的 EatFood 方法呢,还是 Cat 类型的 EatFood 方法呢?答案是调用父类即Animal的EatFood方法。执行结果如下:

大多数的文章都是介绍到这里为止,仅仅是让我们知道这些概念以及调用的方法,而没有说明为什么会这样。下面我们就来深入一点,谈谈多态背后的机理。

二、深入理解多态性

要深入理解多态性,就要先从值类型和引用类型说起。我们都知道值类型是保存在线程栈上的,而引用类型是保存在托管堆中的。因为所有的类都是引用类型,所以我们仅仅看引用类型。

现在回到刚才的例子,Main函数时程序的入口,在JIT编译器将Main函数编译为本地CPU指定时,发现该方法引用了Biology、Animal、Cat、Dog这几个类,所以CLR会创建几个实例来表示这几个类型本身,我们把它称之为“类型对象”。该对象包含了类中的静态字段,以及包含类中所有方法的方法表,还包含了托管堆中所有对象都要有的两个额外的成员——类型对象指针(Type Object Point)和同步块索引(sync Block Index)。

可能上面这段对于有些没有看过相关CLR书籍的童鞋没有看懂,所以我们画个图来描述一下:

上面的这个图是在执行Main函数之前CLR所做的事情,下面开始执行Main函数(方便起见,简化一下Main函数):

            //Animal的实例
            Animal a = new Animal();
            //Animal的实例,引用派生类Cat对象
            Animal ac = new Cat();
            //Animal的实例,引用派生类Dog对象
            Animal ad = new Dog();
            a.Sleep();
            a.EatFood();
            ac.EatFood();
            ad.EatFood();

下面实例化三个Animal实例,但是他们实际上指向的分别是Animal对象、Cat对象和Dog对象,如下图:

请注意,变量ac和ad虽然都是Animal类型,但是指向的分别是Cat对象和Dog对象,这里是关键。

当执行a.Sleep()时,由于Sleep是非虚实例方法,JIT编译器会找到发出调用的那个变量(a)的类型(Animal)对应的类型对象(Animal类型对象)。然后调用该类型对象中的Sleep方法,如果该类型对象没有Sleep方法,JIT编译器会回溯类的基类(一直到Object)中查找Sleep方法。

当执行ac.EatFood时,由于EatFood是虚实例方法,JIT编译器调用时会在方法中生成一些额外的代码,这些代码会首先检查发出调用的变量(ac),然后跟随变量的引用地址找到发出调用的对象(Cat对象),找到发出调用的对象对应的类型对象(Cat类型对象),最后在该类型对象中查找EatFood方法。同样的,如果在该类型对象中没有查找到EatFood方法,JIT编译器会回溯到该类型对象的基类中查找。

上面描述的就是JIT编译器在遇到调用类型的非虚实例方法以及虚实例方法时的不同执行方式,也这是处理这两类方法的不同方式造成了表面上我们看到的面向对象的三个特征之一——多态性。

好了,本篇博文开始回顾了一些关于多态性的基本概念,然后解释了多态性的内部机理。内部JIT编译器的部分基本是参照《CLR via C#》书中的第四章的内容,有这本书的同学可以回去翻翻看看。写的不好的地方,请大家批评指正。

时间: 2024-10-05 19:03:55

C#中的多态性的相关文章

[搬运]如何在C++中实现多态性

也没什么好说的,仅仅做了个测试,了解一下为什么会有一些莫名其妙的规定. 以前学C++时我对这些是一直没弄懂的,但愿对某些人还是有所帮助的--下述源代码在VC++6.0下通过.Tab变成只占1格了,将就看看吧=.=或者copy到编辑器中=.= // File Name : polymorphism_test.cpp// Author : keakon// Create Date : 2006/5/11// Last Edited Date : 2006/5/26// 通过3次测试,演示了如何实现多

【Java_基础】java中的多态性

方法的重载.重写和动态链接构成了java的多态性. 1.方法的重载 同一个类中多个同名但形参有所差异的方法,在调用时会根据参数的不同做出选择. 2.方法的重写 子类中重新定义了父类的方法,有关方法重写的规则请参考文章:Java中方法重写的注意事项. 3.动态链接 动态链接出现在父类引用指向子类对象的场景. 因为子类中有一个隐藏的引用super指向父类实例,当出现父类引用指向子类对象时,子类对象就会将其隐藏的父类实例返回给该父类引用.因此,父类引用还是指向了父类实例,而不是像表面看到的那样指向了子

浅谈java中的多态性

讲到多态,就必须牵扯到继承和接口.至于多态强大的功能,目前水平有限,暂时还没有很明显地体会到. 我们先看 多态+继承 package test; public class Test { public static void main(String[] args) { A test = new B(); test.testA(100); } } class A { int i = 0; void testA(int i) { this.i = i; System.out.println("i的值为

Java中的多态性

1.上溯造型 之前我们已经知道将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用.取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作"上溯造型"--因为继承树的画法是基础类位于最上方.但这样做也会遇到一个问题,如下例所示: //Inheritance & upcasting class Note { private int value; private Note(int val) { value = val; } public static final N

C++中多态的实现原理

1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数. 2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的. 3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性. 4. 多态用虚函数来实现,结合动态绑定. 5. 纯虚函数是虚函数再加上= 0. 6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()= 0:即抽象类!必须在子类实

js 中面向对象的多态

什么是多态: 实际上是不同对象作用与同一操作产生不同的效果.多态的思想实际上是把"想做什么"和"谁去做"分开,多态的好处是什么呢?为什么要多态?我们来看看 Martin Fowler 在<重构:改善既有代码的设计>里写到: 多态的最根本好处在于,你不必再向对象询问"你是什么类型"而后根据得到的答 案调用对象的某个行为--你只管调用该行为就是了,其他的一切多态机制都会为你安 排妥当. 换句话说,多态最根本的作用就是通过把过程化的条件分支

java中的instanceof

instanceof是Java.php的一个二元操作符(运算符),和==,>,<是同一类东西.由于它是由字母组成的,所以也是Java的保留关键字.它的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据.可以用来判断继承中的子类的实例是否为父类的实现.相当于c#中的is操作符.java中的instanceof运算符是用来在运行时指出对象是否是特定类的一个实例.instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例. instanceof

多态性与虚函数

多态性 多态性是面向对象程序设计的一个重要特征.如果一种语言只支持类而不支持多态,是不能被称为面向对象语言的.只能说是基于对象的,如Ada,VB就属于此类. 在C++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数. 在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可以用自己的方式去相应共同的消息.所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的

C++多态性与虚函数

面向对象程序设计中的多态性是指向不同的对象发送同一个消息,不同对象对应同一消息产生不同行为.在程序中消息就是调用函数,不同的行为就是指不同的实现方法,即执行不同的函数体.也可以这样说就是实现了"一个接口,多种方法". 从实现的角度来讲,多态可以分为两类:编译时的多态性和运行时的多态性.前者是通过静态联编来实现的,比如C++中通过函数的重载和运算符的重载.后者则是通过动态联编来实现的,在C++中运行时的多态性主要是通过虚函数来实现的. 赋值兼容     不过在说虚函数之前,先介绍一个有关