各位牛人大家来讨论 
多态性
包括虚函数.
动态编联 
动态方法,
虚似方法,
抽向方法. 
先说理论最好有实例说明.

解决方案 »

  1.   

    建议打开DELPHI的SOURCE看看DELPHI的控件源码。
      

  2.   

    仅供参考
    OOP的英文全称是Object  Oriented  Programming,翻译过来就是面向对象编程。OOP是一种非 常重要的编程思想。也许你会对这种抽象的东西不感兴趣,可是几乎任何一个高手都会告诉你:“语言并 不重要,重要的是编程思想。” 
    大家知道,Delphi的语言基础是Object Pascal。这是Borland在传统的Pascal语言中增加了面向 对象的特性后发展而成,并且特地冠以Object的字样,以示与传统的Pascal语言的差别,可见面向对象技 术对其影响之大。可以说,Delphi构建在Object Pascal的基础之上,而Object Pascal构建在面向对象技 术之上。 
    事实上,不但Delphi,OOP也是C++、Java等其他许多现代编程语言的基础(Visual Basic不完全 地支持OOP)。熟练地掌握OOP技术,是深入掌握Delphi的必要条件,是迈入高手境界的必经之路,也是一 个成熟的程序员的标志之一。理解了OOP技术之后,很多以前令你困惑的东西会迎刃而解。 
    有趣的是,虽然Delphi是完全基于OOP的,但是一个完全不了解OOP的程序员也能够使用Delphi编 写程序,因为Delphi会自动完成绝大多数的工作。当你开始学习Delphi的时候,你可能无法想象,当简单 地往窗体上添加一个按钮时,Delphi会完成多么复杂的工作吧!但是既然有志于深入Delphi的世界,成为 一个真正的程序员,我们就应该对Delphi的细节具有好奇心。 
    这些理论可能会让人觉得枯燥和望而生畏,其实当你掌握了它之后就会觉得没什么了。当然,你 需要有足够的毅力。 
    OOP有三个主要的特征: 1.1 数据封装 让我们先看一段代码: type 
       TDate = class 
    Mouth,day,Year:Integer; 
    procedure SetValue(m,d,y:Integer); 
    function LeapYear:Boolean; 
       end; 我们首先会看到class关键字,它的中文译名为“类”。类是一个非常重要的概念。根据权威的 定义,类是:一种用户定义的数据类型,它具有自己的说明和一些操作。一个类中含有一些内部数据和一 些过程或函数形式的对象方法,通常来描述一些非常相似的对象所具有的共同特征和行为。 
    这个定义可能比较晦涩。你可以把类想象为一种特殊的Record类型,其中不但可能包含数据,而 且可能包含函数和过程(在OOP中称之为方法)。这些数据和方法被统称为类的成员。 
    上面这个类很显然是一个日期类型,它包括Mouth,Day,Year这三个数据成员,和SetValue、 LeapYear这两个方法。顺便说一句,在Delphi中,习惯以字母T作为每个类的前缀,就象Viusal C++中习 惯以字母C作为每个类的前缀一样。 
    Mouth,Day,Year这三个数据成员指定了该日期的年、月、日。SetValue方法为这三个数据成员 赋值,而LeapYear检查当前日期所在的那一年是否是闰年。下面我们给出这两个方法的实现部分: procedure TDate.SetValue(m,d,y):Integer; 
    begin 
    Mouth := m; 
    Day := d; 
    Year := y;   
    end; function TDate.LeapYear:Boolean; 
    begin 
    if (Year mod 4 <> 0) then 
       LeapYear := False 
    else if (Year mod 100 <> 0) 
       LeapYear := True 
    else if (Year mod 400 <> 0) 
       LeapYear := False 
    else 
       LeapYear := True; 
    end; 实现了这些定义之后,就可以这样调用它们: var 
       ADay: TDate; 
    begin 
    //建立一个对象 
    ADay := TDate.create; 
    //使用之 
    ADay.SetValue(1,1,2000); 
    if ADay.LeapYear then  
       ShowMessage('闰年:' + Inttostr(ADay.year)); 
    //释放对象 
    ADay.free; 
    end; 我们来逐行解释这些代码的含义。var后面那一行声明了一个TDate类的变量。 
    声明了变量之后,我们怎么使用它呢?使用TDate类的Create方法可以建立一个该类的对象,并 将其赋予ADay变量。 
    现在我们又接触到了OOP中另一个重要的概念:对象。什么是对象?简言之,对象就是类的实例 ,或者说,是类定义的数据类型的变量。当建立一个类的对象时,系统为它分配一块内存。例如我们定义 一个变量A为Integer类型,那么,Integer是一个数据类型,A就是一个实例。类与对象的关系就类似于这 两者之间的关系。区别类和对象是非常重要的,甚至一些专业的程序员都往往将他们搞混。 
    细心的读者可能注意到,在TDate类的定义中,并没有Create这个方法。那么这个Create方法是 从哪里来的呢?Create方法是每一个Class都具有隐含的方法,它的作用是建立这个类的实例。请注意, 在这里,类和其他的数据类型是不同的。其他的数据类型都是声明了变量之后就可以直接使用,而类类型 必须在使用Create方法创建它的实例(对象)之后才能使用。 
    事实上,在C++和其他大多数的OOP语言中,声明一个类的变量就能够同时建立起这个类的对象。 而Delphi(包括它的孪生兄弟C++ Builder)在这方面与众不同,必须要Create一下才能真正建立对象。 同时,在这个对象不再需要时,必须要手工调用free方法释放这个对象(当然,free方法也是每个类隐含 的)。这和Delphi独特的“对象引用模型”有关,有兴趣的朋友可以查阅有关资料,我就不多说了。 
    这种情况造成了一个非常有趣的现象,那就是,编程的初学者往往忘记在使用对象之前create它 ,从而出错,但从C++转向Delphi的高手也常常犯同样的错误…… 
    顺便告诉大家一个诀窍,当编译器出现“Read of Address: ffffffff”这样的错误时,多半是 因为在使用对象之前忘了Create,可以从这方面入手检查代码。另外,也千万不要忘记在不需要它时使用 free释放掉,否则可能造成内存泄漏。 
    在建立和释放对象的代码的中间,是使用对象的代码。访问对象的数据成员非常简单,和Record 类型没有什么区别。可以点号表达式来访问它们: ADay.Year := 2000; 
    ADay.Mouth := 1; 
    ADay.Day := 1; 同样,也可以使用点号表达式来调用对象的方法。如果你阅读了方法实现部分的代码,你可以很 容易地发现,ADay.SetValue(1,1,2000)这一句分别为三个数据成员赋了值,而ADay.LeapYear调用则返回 当前日期所在年是否为闰年。至此,整段代码的意义也就清楚了。 
    然而,类不仅仅这么简单。上面这个例子是一个非常简单的类,可以直接访问它的任何成员(数 据和方法)。但某些类的成员是不能被随便访问的。Delphi中用三个关键字区分这些成员的访问权限: 表1 
    Private 
    该类型的成员只能在声明类中被访问 
    Public 
    该类型的成员可以被程序中的任何地方的代码访问 
    Protected 
    该类型的成员只能在声明类以及声明类的派生类中被访问 Protected类型的成员以及什么是“派生类”等问题我们留到以后再进行讨论,现在我们将注意 力集中在前两者。 
    Public类型就是在上面例子中的那种类型,这个很好理解。而Private类型,根据表格中的简单 解释,只能在该成员被声明的那个类(也就是该成员所属的那个类啦)中被访问,越出这个界限,它就是 不可见的。那么,Private类型的成员将如何被使用呢?简单地说,就是通过一个Public类的方法来访问 它。 
    让我们看一个新的例子: type 
       TDate = class 
       private 
    Mouth,day,Year:Integer; 
       Public 
    procedure SetValue(m,d,y:Integer); 
    function LeapYear:Boolean; 
    function GetText:String; 
       end; 
    在这个类中,Mouth,Day,Year这三个成员被声明为Private成员,因此它们在类以外的其它地 方是不可访问的。也就是说,如果你使用 ADay.Year := 2000; 这样的代码,那么编译器将会报错。但是,我们可以照样通过SetValue方法为它们赋值: ADay.SetValue(1,1,2000); 这行代码是合法的,因为SetValue本身是TDate类的成员,而且它又是一个Public成员。而使用GetText方 法则可以得到当前日期值(这也是得到当期日期值的唯一办法)。 
    这样的设置使得类的一些成员被隐含起来,用户只能用一些专门的方法来使用它们。那 些可以被外部代码访问的成员称之为类的接口。这样做有什么好处呢?首先,这让类的作者可以检测被赋 值的内容。比如,用户可能给一个对象赋予13月40日这样的无效日期。而在隐含了一些成员之后,类的作 者可以在方法的代码中检测这些值是否有效,从而大大地减少了产生错误的机会。其次,使用规范的类, 作者可以随时修改类内部的代码,而使用该类的代码却无需任何修改!这样使得代码的维护成了一件轻松 的事件,特别是对于多人协作的大型软件而言。 
    这就叫做数据的封装(encapsulation)。这是OOP的第一个特征。一个优秀的OOP程序员,应该 在设计类的时候,就确定将哪些重要的数据封装起来,并给出一个高效率的接口。 
    需要指出的一点是,表1中Private部分的论述对于“标准的”OOP语言(例如C++)是完全正确的 ,但对于Delphi有一个例外。在Delphi中,Private成员除了在声明类中可以访问外,在声明类所在的单 元(.pas文件)中的任何地方都能被访问,不论这些代码与声明类的关系如何。严格来说,这是违反OOP 的原则的,我不明白Borland为何要这么做(据说是为了方便)。在关于Delphi的优劣性的讨论中,这是 常被涉及的一个问题。 
      

  3.   

    1.2 继承与派生 我们再来看一段代码: type 
       TNewDate = class(TDate) 
       Public 
    function GetTextNew:String; 
       end; function GetText:String; 
    begin 
    return := inttostr(Mouth) + ':' + inttostr(Day) + ':' + inttostr(Year); 
    end; 可以看到,在class后面出现一个包含在括号中的类名。这种语法表示新的类继承了一个旧的类 。继承了原有类的类称之为派生类,也叫子类,被继承的类称之为基类,也叫父类。 
    派生类与基类之间是什么关系呢?当派生类继承自一个基类时,它自动具有基类的所有数据、方 法以及其他类型,无须在派生类中再做说明。例如,可以象下面这段代码这样使用TNewDate类: var 
       ADay: TNewDate; 
    begin 
    ADay := TNewDate.create; 
    ADay.SetValue(1,1,2000); 
    if ADay.LeapYear then  
       ShowMessage('闰年:' + Inttostr(ADay.year)); 
    ADay.free; 
    end; 而且,派生类还可以在基类的基础上加入自己的数据和方法。可以看到在TnewDate类中增加了一 个新的方法GetTextNew。下面给出这个方法的实现部分: function GetTextNew:String; 
    begin 
    return := GetText; 
    end; 然后调用它: ADay.GetTextNew; 这个新的方法工作得很好。 
    为什么GetTextNew方法必须调用基类中的GetText方法,而不能直接使用GetText方法中的那些代 码呢?原因是,Mouth,Day,Year这三个成员被声明为Private成员,因此它们即使在派生类中也是不能 被访问的,所以必须调用基类中的GetText方法,间接地使用它们。如果要直接使用它们的话,可以将这 三个成员的属性从Private改为Protected。在表1中可以看到,Protected属性的成员可以在声明类以及声 明类的派生类中被访问,然而仍然不能被这两种情况以外的其他代码所访问。现在我们终于可以理解了, 这个特殊的属性实际上提供了极大的方便:它使得类的成员被封装,避免了混乱,同时又能够让派生类方 便地使用它们。 
    (如果你是一个细心的人,你可能发现上面的话中间有一个小小的仳漏。当你真的在GetTextNew 方法中访问了基类的Private成员的话,你可能会惊奇地发现程序也能够编译通过而且正常运行!其实, 这个问题和OOP本身没有关系。上面我已经说过,在Delphi中,Private成员在声明类所在的单元文件中的 任何地方都能被访问,因此如果TNewDate类和TDate类在同一个.pas文件中时,这种情况就不足为怪了。 ) 
    怎么样,是不是觉得非常奇妙?通过这种继承的机制,类不再仅仅是数据和方法的封装,它提供 了开放性。你可以方便地继承一个功能强大的类,然后添加进自己需要的特性,同时,你又不需要对基类 进行任何的修改。相反,原作者对基类的任何改动,都可以在你的新类中立即反映出来。这非常符合代码 的重用要求。 
    这种继承机制也非常符合现实世界中的情形。可以设想,一般意义上的“动物”是一个类,具有 自己的一些特征(成员);而“狗”是“动物”的派生类,它具有动物的所有特征,同时还具有自己独有 的特征(四条腿,汪汪叫,等等)。而“狗”这个类可以继续派生下去,例如“黑狗”“白狗”,它们除 了保留狗的全部特征之外,还具有自己的特征(黑颜色,白颜色,等等)。而具体到一只活生生的狗,可 以认为它就是“黑狗”或“白狗”(或其他什么狗)的一个实例(对象)。 
    OOP这种对现实世界的模拟不仅极大地简化了代码的维护,而且使得整个编程思想产生了革命性 的变化,较之模块化编程有了飞跃的进步。 
    如果你曾经仔细阅读过VCL的资料甚至它的源代码,你就可以发现,整个VCL都是建立在这种强大 的封装-继承的机制之上的。你可以看到一张详细的VCL层次结构图,就象是一个庞大的家谱,各种VCL构 件通过层层继承而产生。例如,一个简简单单的TForm类,就是许多次继承之后的产物: 
    TObject - TPersistent - TConponent - TControl - TWinControl - TScrollingWinControl -  TCustomForm - TForm 
    不但Delphi的VCL,Visual C++中的著名的MFC(Microsoft Foundation Class,微软基本类库) ,以及以前Borland C++中风光一时的OWL(Object Window Library,对象窗口类库),都是建立在这种 机制之上。所不同的是,对于前两种语言,你要花上好几个月的功夫去基本掌握那些繁复无比的类,才能 写出比较有实用价值的程序,而在Delphi中,大部分的工作Delphi都已经自动帮你完成了。例如,每次你 向程序中加入一个窗体时,Delphi就自动为你从TForm派生一个新类(默认为TForm1),并且为这个新类 创造一个实例。你对这个窗体的改动(添加构件和代码之类),无非是为这个派生类加入一些新的特性而 已;你再也用不着自己去处理最大化、最小化、改变大小这一类的情况,因为这些代码都在基类中被实现 ,而被派生类所继承了。这就是Delphi的伟大之处。当然,Delphi的VCL也绝不比MFC或OWL逊色(事实上 它是由后者演变而来)。 
    (可能有人会问起VB的情况。VB不支持继承,因此并没有什么复杂的类库,它自己的控件也少得 可怜,主要是使用ActiveX控件。)。 
    也许你已经若有所悟,为你的发现而心痒难骚了吧。但是,我们要讨论的东西当然不会仅仅这么 简单。 
    在1.1部分(“数据封装”),我们讲到了“Create方法是每一个Class都具有隐含的方法”。其 实,这种说法是不准确的。事实是,在Delphi中,所有的类都默认继承自一个最基础的类TOject,甚至在 你并未指定继承的类名也是如此。Create方法是TObject类具有的方法,因此理所当然,所有的类都自动 获得了Create方法,不管你是否实现过它。想想看就知道了:如果没有Create方法的话,怎样建立一个对 象呢? 
    你可能注意到了Create方法是一个特殊的方法。不错,Create方法的确非常特殊,甚至于它的“ 头衔”不再是function或procedure,而是Constructor(构造器)。你可以在VCL的源码中见到这样一些 例子: Constructor Create; 构造器不仅是一个Delphi关键字,而且是一个OOP方法学的名词。与之相对应的,还有 Destructor(毁坏器)。前者负责完成创建一个对象的工作,为它分配内存,后者负责释放这个对象,回 收它的内存。要注意的一点是,Constructor的名字一般是Create,但Destructor的名字却不是Free,而 是Destroy。例如: Destructor Destroy; 那么,在以前的代码,为什么又使用Free来释放对象呢?二者的区别是,Destroy会直接释放对 象,而Free会事实检查该对象是否存在,如果对象存在,或者对象不为nil,它才会调用Destroy。因此, 程序中应该尽量使用free来释放对象,这样更加安全一些。(但要注意,free也不会自动将对象置为nil ,所以在调用free之后,最好是再手动将对象置为nil。) 
    象对待一般的函数或过程那样,也可以向构造器传递参数: type 
       TDate = class 
       private 
    Mouth,day,Year:Integer; 
       Public 
    function LeapYear:Boolean; 
    function GetText:String; 
    Constructor Create(m,d,y:Integer); 
       end; procedure TDate.Create(m,d,y):Integer; 
    begin 
    Mouth := m; 
    Day := d; 
    Year := y;   
    end; 调用它:    ADay: TDate; 
    begin 
    ADay := TDate.create(1,1,2000); 
    if ADay.LeapYear then  
       ShowMessage('闰年:' + Inttostr(ADay.year)); 
    ADay.free; 
    end; 这样,在Create方法里就完成了对数据的初始化,而无须再调用SetValue方法了。 接下来,我们将要涉及到另一个重要的、也是很有趣的问题:方法的虚拟与重载。 
    可能你已经有点晕了吧……还是先看一个新的例子: type 
       TMyClass = class 
    procedure One;virtual; 
       end; type 
       TNewClass = class(TMyClass) 
    procedure One;override; 
       end; procedure TMyclass.One;virtual; 
    begin 
    ShowMessage('调用了TMyclass的方法!'); 
    end; procedure TNewClass.One; override; 
    begin 
    Inherited; 
    ShowMessage('调用了TNewClass的方法!'); 
    end; 可以看到,从TMyClass派生了一个新类TNewClass。这两个类都声明了一个名字相同的方法One。 
      

  4.   

    所不同的是,在TMyClass中,One方法后面多了一个Virtual关键字,表示这个方法是一个虚拟方法( Virtual Method)。而在TNewClass中,One方法后面多了一个Override关键字,表示该方法进行了重载( Override)。重载技术能够实现许多特殊的功能。 
    让我们来仔细分析它们的实现部分。在TMyclass.One方法的实现部分,调用ShowMessage过程弹 出一个对话框,说明该方法已被调用;这里没有任何特别的地方。在TNewClass.One方法中,出现了一条 以前从未出现过的语句: Inherited; 这个词的中文意思是“继承”。我们暂时不要去涉及到太过复杂的OOP概念,只要知道这条语句 的功能就是了。它的功能是调用基类中相当的虚拟方法中的代码。例如,你如果使用以下代码: var 
       AObject: TNewClass; 
    begin 
    AObject := TNewClass.create; 
    AObject.One; 
    AObject.free; 
    end; 那么程序将弹出两次对话框,第一次是调用TMyclass类中的One方法,第二次才是TNewClass.One方法中的 代码。 
    重载技术使得我们不但可以在派生类中添加基类没有的数据和方法,而且可以非常方便地继承基 类中原有方法的代码,只需要简单地加入Inherited就可以了。如果你不加入Inherited语句,那么基类的 相应方法将被新的方法覆盖掉。但是必须注意,重载只有在基类的方法被标志为Virtual时才能进行,而 且重载的方法必须具有和虚拟方法完全相同的参数类型。 
    虚拟方法还有一种特例,即抽象方法: procedure One;override;abstract; 在One方法后面,不但有override关键字,还多了一个abstract关键字(意为抽象)。这种方法 称为抽象方法(在C++中称为纯虚拟函数)。含有抽象方法的类称为抽象类。抽象方法的独特之处在于, 它只有声明,而根本没有实现部分,如果你企图调用一个对象的抽象方法,你将得到一个异常。只有当这 个类的派生类重载并实现了该方法之后,它才能够被调用。(在C++中,甚至根本就不能建立一个抽象类的 实例。) 
    既然如此,那么这种抽象方法又有什么用呢?这个问题我们将在接下来的“多态”部分进行讨论 。 
    1.3 多态 多态相对来说比较复杂一点。不过不要担心,它的内容比较少,而且如果以前的知识掌握得比较 稳固的话,多态的概念是水到渠成的。 
    先来讨论一下类型的兼容性问题。下面是一个例子: type 
       TAnimal = Class 
    Procedure Voice;virtual; 
    ... 
       end;    TDog = Class(TAnimal) 
    Procedure Voice;Override; 
    ... 
       end; implementation Procedure TAnimal.Voice;virtual; 
    Begin 
    PlaySound('Anim.wav',0,snd_Async); 
    End; Procedure TDog.Voice;virtual; 
    Begin 
    PlaySound('Dog.wav',0,snd_Async); 
    End; TDog类继承了TAnimal类,并重载了其中的Voice方法。PlaySound是一个WIN API函数,可以播放 指定的wav文件。(这个函数的定义在MMSystem.pas文件中可以找到。) 
    先看这段代码: var 
       MyAnimal1, MyAnimal2: TAnimal; 
    Begin 
    MyAnimal1 := TAnimal.Create; 
    MyAnimal2 := TDog.Create; 
    ... 在实现部分的第一行中,建立了一个TAnimal类型的对象,并将其赋予TAnimal类型的变量 MyAnimal1。这是很正常的事。但在第二行中,建立了一个TDog类型的对象,并将其赋予了TAnimal类型的 变量MyAnimal2。这看上去令人吃惊,但这些代码是完全合法的。 
    众所周知,Pascal以及Object Pascal是一种类型定义严格的语言,你不能将某个类型的值赋予 不同类型的变量,例如将一个整型值赋予布尔型变量,将会导致出错。但是,这个规则在涉及到OOP领域 时,出现了一个重要的例外,那就是:可以将一个子类的值赋予一个父类类型的变量。但倒过来却是不行 的,一个父类的值决不能赋予一个子类类型的变量。 
    如果将这个原则放到现实世界中,那就很容易理解了:“狗”继承自“动物”,因为狗也是一种 动物。所以可以将一个“狗”类型的值赋予“动物”类型的变量,因为“狗”具有“动物”的一切特征。 但反过来,“动物”不具有“狗”的所有特征,因此反向赋值是不行的。 
    那么,这种兼容规则在编程中究竟有什么用处呢? 
    请注意下面这段代码: var 
       MyAnimal1, MyAnimal2: TAnimal; 
    Begin 
    MyAnimal1 := TAnimal.Create; 
    MyAnimal2 := TDog.Create; 
    MyAnimal1.Sound; 
    MyAnimal2.Sound; 
    ... MyAnimal1和MyAnimal2都是TAnimal的变量,而且都调用了Sound方法。但是,执行的结果是完全 不同的:前者执行的是TAnimal.Voice的代码,而后者执行的是TDog.Voice的代码!其原因很简单,因为 MyAnimal1被赋予了TAnimal类型的对象,而MyAnimal2被赋予了TDog类型的对象。也就是说,一个TAnimal 类型的变量,当它调用Sound方法时,所执行的代码是不确定的:可能执行TAnimal.Voice的代码,也可能 执行的是TDog.Voice的代码,取决于它当时引用的是一个什么样的对象。 
    再看: MyAnimal1 := TAnimal.Create; 
    MyAnimal1.Sound; 
    MyAnimal1.free; 
    MyAnimal1 := TDog.Create; 
    MyAnimal1.Sound; 
    ... 同一个变量MyAnimal1,在第一次调用Sound方法时,执行的是TAnimal.Voice的代码,在第二次 时执行的是TDog.Voice的代码。MyAnimal1.Sound这行代码不需要变化,程序可以根据不同的情况赋予该 变量不同的对象,从而使它执行不同的代码。这就是多态的定义。 
    这个非常重要的特点大大地增加了代码的可复用性。如前所述,只需要简单地写下一行代码,就 可以让程序执行不同的功能,因为这个虚拟方法同TAnimal的任何派生类都是兼容的,甚至连那些还没有 编写出来的类也是一样。而程序员并不需要了解这些派生类的细节。利用多态性写出来代码,还具有简洁 和维护性好的特点。 
    现在我们可以回到本文的1.2节结尾处的问题了。抽象方法本身不能够做任何事情,必须在子类 中被重载并实现,才能够完成有意义的工作。但抽象方法的存在,相当于为父类留下了一个接口,当程序 将一个子类的对象赋予父类的变量时,父类的变量就可以调用这个方法,当然此时它运行的是相应的子类 中重载该方法的代码。如果没有这个抽象方法,父类的变量就不能调用它,因为它不能调用一个只在子类 中存在、而在父类中不存在的方法! 
      

  5.   

    多谢你能说这么多,如:
      type TOne = class 
         procedure show(s);virtual; 
      type TTwo = Class(TOne);
         prodedure show(s);override;virtual; 
    如果AA 是Two  的对象那它在执行 TOne 里的Show 时指针是如何指向它的, 运行的虚似表的情况, 而不是这单单只是说说皮毛的.    
      

  6.   

    Delphi帮助里有很清楚的解释!
    Virtual versus dynamic
    Virtual and dynamic methods are semantically equivalent. They differ only in the implementation of method-call dispatching at runtime. Virtual methods optimize for speed, while dynamic methods optimize for code size.
    In general, virtual methods are the most efficient way to implement polymorphic behavior. Dynamic methods are useful when a base class declares many overridable methods which are inherited by many descendant classes in an application, but only occasionally overridden.