前言

上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题。虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复。这不,就有一个园友提出了这样一个代码,这段代码如果被编译成CIL代码的话,对虚函数的调用会使用call而非callvirt:

override string ToString()
{
return Base.ToString();
}

至于为何是这样,匹夫在回复中也做了解释,因为如果CIL使用callvirt指令,那么上面那段代码其实相当于是这样的:

override string ToString()
{
return this.ToString();
}

所以如果使用callvirt的话,会产生无限递归的情况。那是为什么呢?因为callvirt主要会做两件事,首先它会检查实例是否为null。其次,如果实例不为空,则它会根据运行时类型寻找最恰当的方法去调用。当然,关于CIL代码中的call和callvirt的讨论是上一篇文章的内容,在本篇文章,匹夫还是想就这个例子作为引子,聊一聊C#中的虚函数机制。

假如只有静态函数

上一篇文章中,匹夫举了一个使用“call”来调用对象为null的实例函数的例子,通过那个例子我们发现了原来实例函数需要将当前实例的引用作为参数传入。由于是上一篇文章《用CIL写程序:从“call vs callvirt”看方法调用》中的园友回复才让匹夫有了写这篇文章的想法,所以这里匹夫还使用上一篇文章中的例子,只不过把那篇文章中的CIL代码换成C#。

现在假设我们有了以下3个类。

public class People
{
private string name = "People";
public virtual void Introduce()
{
string str = "我是People类,我叫" + this.name;
System.Console.WriteLine(str);
}
} public class Murong : People
{
private string name = "慕容小匹夫";
public override void Introduce()
{
string str = "我是Murong类,我叫" + this.name;
System.Console.WriteLine(str);
}
} public class ChenJD : People
{
private string name = "陈嘉栋";
public void Introduce()
{
string str = "我是ChenJD类,我叫" + this.name;
System.Console.WriteLine(str);
} }

还记得前一篇文章中,匹夫提到过的编译时类型和运行时类型吗?简单回顾一下,对编译器来说,变量的类型就是你声明它时的类型,也就是编译时类型,假设为TypeA。但是,往往有这种情况,就是你实例化了另一个类型,假设为TypeB,并且将这个实例的引用赋值给了你之前声明的那个变量。这就是说,在这段程序运行的时候,编译阶段被定义为TypeA类型的变量所指向的是一块存储了类型TypeB的实例的内存。这里,TypeB便是运行时类型。搞清楚这一点,我们才能继续下文的内容。那就是我们声明一个People类型的变量,然后再将它的派生类实例的引用赋值给这个变量,看看会有一些什么有趣的事情发生。

public class Test1
{ static void Main()
{
//编译时类型是People,运行时类型是People
People person = new People();
person.Introduce();
//编译时类型是People,运行时类型是Murong
person = new Murong();
person.Introduce();
//编译时类型是People,运行时类型是ChenJD
person = new ChenJD();
person.Introduce();
//编译时类型是ChenJD,运行时类型是ChenJD
ChenJD chen = new ChenJD();
chen.Introduce();
}
}

这组实现其实在上一篇文章中,匹夫使用CIL代码实现过。那么这里我们再重新用C#来做一遍。老套路,编译运行。

这四条输出结果,前2条都十分正常,没有什么可奇怪的。但是在ChenJD这个类中,并没有使用override关键字去重写基类People中的虚方法Introduce,而是直接实现了一个自己的实例函数Introduce。

而奇怪的事情也恰恰发生在和ChenJD这个类相关的操作中,那就是当变量声明为People类时,即使将ChenJD类的实例引用赋值给这个变量,调用Introduce方法,但输出的却不是ChenJD中重新定义的那个Introduce方法,反而很奇怪的调用起了基类People的Introduce方法。

与此同时,声明为ChenJD类的变量chen,在调用Introduce方法时,的确是选择了ChenJD类重新定义的Introduce。

那么我们能直观的发现一些什么(从最直观的角度看)?

  • 即便将变量声明为People,换言之变量的编译时类型是People。但是将不同的实例引用赋值给它,它也会根据运行时类型寻找正确的被重写的方法去调用。例如Murong中使用override关键字重写的方法Introduce。
  • 将变量声明为People,和将变量声明为ChenJD(也就是说编译时类型一个是People,一个是ChenJD),即使它们的运行时类型都是ChenJD,但是调用的Introduce方法显然不同。

的确有点意思了,是吗?

那么现在假设我们的手中只有静态方法,换言之上面例子中的虚方法,实例方法其实全部是静态方法变化而来的,那么我们应该如何通过静态方法来实现实例方法和虚方法的功能呢?

先从实例方法下手

实例方法和静态方法有什么不同呢?大概你会说一个目标是实例,一个目标是类。不错,但除此之外它们还有什么本质的区别吗?貌似没有了。那么好,如果我们只有静态方法,如何去实现一个实例方法的功能呢?不错,把实例的引用当做这个静态方法的一个参数。

那么我们就以上面的ChenJD类中的Introduce方法入手,使用静态方法的形式去实现一个实例方法的功能。

    //用静态方法实现实例方法
public static void Introduce(ChenJD _this)
{
string str = "我是ChenJD类,我叫" + _this.name;
System.Console.WriteLine(str);
}

那么我们该如何调用呢?

很简单,直接调用ChenJD这个类的静态方法Introduce,同时将它的实例引用作为参数传入这个静态方法。

//调用静态函数实现的实例函数
ChenJD chen = new ChenJD();
ChenJD.Introduce(chen);

编译运行的结果和上面调用实例函数是一样的。

所以,实例函数的实现,其实就是靠将当前实例的引用作为参数_this传入一个静态函数中,只不过这个参数_this对我们不可见罢了。

OK,可为什么匹夫你饶了一大圈聊怎么用静态函数实现实例函数呢?这个和本文的主题有关系吗?当然有,因为如果所谓的虚函数也是用静态方法实现的呢?

从静态方法到虚函数

其实,实现c#的虚函数机制只需要静态函数和委托就够了。所以,进入下面的内容之前我们要先抛弃现有的一些现成的概念,比如实例函数。

此时,我们假设我们手中只有静态函数和委托,下面匹夫就带领各位一起去一探虚函数的究竟吧。

_this是谁很重要

虚函数有什么特点呢?嗯~,匹夫简单想了想,最大的特点可能就是需要具备在运行时选择正确的重写版本的能力。

假如没有现成的虚函数的存在,那么在运行时才决定要调用哪个函数的能力,会让你想到谁呢?

不错,前方一大波delegate仿佛就在眼前。

但是还是要注意啊,我们现在没有所谓的实例方法的存在,有的只是静态方法。那么我们所有的虚函数和重写方法,应该怎么表示呢?

不错,和刚刚才说过的实例方法的实现方式一样,将_this作为静态函数不可见的第一个参数传入。那么问题来了,_this到底应该是什么类型的呢?

这为什么是一个问题呢?

因为你可以有很多派生类,派生类中又可以重写虚函数,那么这个虚函数的第一个参数_this到底是谁就很重要了。所以,第一个参数_this 就是声明这个函数的那个类型实例。具体到刚刚的例子,声明为People类型的变量,即便被赋值为Murong的实例引用、ChenJD的实例引用却都是去最初声明了虚函数Introduce的People中去分派符合的重写方法的版本,当Murong使用了override关键字的时候,People能够找到Murong的重写版本,所以调用了Murong的重写版本。而由于ChenJD类中并没有重写基类的虚方法,而是重新定义了一个自己的Introduce方法,所以People找不到符合的重写版本,输出的就是最初定义的Introduce。而声明为ChenJD的变量,在调用Introduce方法时,_this已经变成了ChenJD类,和People已经没有关系了。

明白了这一点,我们探索C#的虚函数机制就完成了51.23198%了。

delegate有话说

上文已经说了,为了实现虚函数能够在运行时选择正确重写版本的能力,我们可以考虑使用委托。将调用函数换个思路,变成对委托的调用,这样自然就实现了根据不同的情况,调用不同函数。

那么我们再来改写一下上文中的例子。

public class People
{
//新增的
public Action<People> DelegateIntroduce;
public string name = "People";
public static void Introduce(People _this)
{
string str = "我是People类,我叫" + _this.name;
System.Console.WriteLine(str);
}
} public class Murong : People
{
public string name = "慕容小匹夫";
public static void Introduce(People _this)
{
string str = "我是Murong类,我叫" + _this.name;
System.Console.WriteLine(str);
}
} public class ChenJD : People
{
public string name = "陈嘉栋";
public static void Introduce(People _this)
{
string str = "我是ChenJD类,我叫" + _this.name;
System.Console.WriteLine(str);
}
}

到此,匹夫将之前例子中的实例函数全部替换成了静态函数,而且还新增了一个委托Action<People> DelegateIntroduce。那么现在我们就利用这个委托,来实现我们的目标,将对具体函数的调用,转换成对委托的调用。那么首先我们显然需要一个方法,来对各个派生类的委托字段初始化赋值。

不过在此之前,匹夫查阅资料时发现了很有趣的一点,那就是现实的C#的虚函数槽(上一篇文章中提到过这个概念)其实是在类实例分配完内存之后,但是在实例构造器调用之前就被初始化了。所以,为了模拟这一点,我们不使用实例构造器(实例构造器主要负责实例的初始化,比如字段赋值等等),而引入一个静态Create方法,使用new来分配内存,之后初始化我们的DelegateIntroduce也就是委托字段,之后再做一些实例构造器做的事情。这里仅仅写出基类People的Create方法,它的派生类类似。

  //使用静态方法创建实例
public static People Create()
{
People people = new People();//仅仅分配内存
People.InitVirCall(people);//初始化我们的委托
//TODO
//之后实例构造器要做的事情
}

之后就到了我们实现委托字段初始化的阶段了。那么无非是将派生类各自的重写方法赋值给委托。所以,我们在此将虚函数和使用了override关键字的重写版本赋值给对应的委托,而没有使用override关键字的方法则不在此列,例如ChenJD类中的Introduce方法。在此需要注意,对于派生类来说,首先要调用基类中定义的为委托字段赋值的方法,将虚函数最初的定义首先赋值给委托,这其实也就是为何当没有正确的重写版本时,会调用在基类中最初定义的那个方法。

//基类,也就是Introduce方法的原始定义的类。
public static void InitVirCall(People people)
{
people.DelegateIntroduce = People.Introduce;//保证了最原始(定义)的Introduce方法赋值给委托
} //派生类Murong,重写了Introduce方法
public static void InitVirCall(Murong murong)
{
People.InitVirCall(murong);//首先保证最原始也就是定义的方法在赋值给委托。
murong.DelegateIntroduce = Murong.Introduce;//其次如果有重写版本,再赋值给委托。如没有重写版本则不赋值。
} //派生类ChenJD,没有重写Introduce方法,而是重新定义了该方法。
//因为没有重写基类的Introduce方法,所以不能赋值给DelegateIntroduce
public static void InitVirCall(ChenJD chen)
{
People.InitVirCall(chen);
//因为ChenJD类中的Introduce是重新定义的,所以不加到委托中。
}

到此。。。我们似乎又发现了一个新的问题。因为会涉及到实例的字段的问题,但是传入各个Introduce的都是People类的实例,那么字段的值就不能保证是派生类自己的了。比如编译一下上面的代码,输出的其实是:

所以为了能够匹配正确的类型,我们还需要进行一步转化。将People转化成对应的派生类。到此,我们就利用委托和静态方法实现了虚函数的机制。代码如下:

using System;

public class Test1
{ static void Main()
{
People person = People.Create();
person.DelegateIntroduce(person); person = Murong.Create();
person.DelegateIntroduce(person); person = ChenJD.Create();
person.DelegateIntroduce(person); ChenJD chen = ChenJD.Create();
ChenJD.Introduce(chen); }
} public class People
{
//新增的
public Action<People> DelegateIntroduce;
public string name = "People";
public static void Introduce(People _this)
{
string str = "我是People类,我叫" + (_this as People).name;
System.Console.WriteLine(str);
} public static People Create()
{
People people = new People();//仅仅分配内存
People.InitVirCall(people);//初始化我们的委托
//TODO
return people;
} public static void InitVirCall(People people)
{
people.DelegateIntroduce = People.Introduce;//保证了最原始(定义)的Introduce方法赋值给委托
}
} public class Murong : People
{
public string name = "慕容小匹夫";
public static void Introduce(People _this)
{
string str = "我是Murong类,我叫" + (_this as Murong).name;
System.Console.WriteLine(str);
}
public static Murong Create()
{
Murong murong = new Murong();//仅仅分配内存
Murong.InitVirCall(murong);//初始化我们的委托
//TODO return murong;
} public static void InitVirCall(Murong murong)
{
People.InitVirCall(murong);//首先保证最原始也就是定义的方法在赋值给委托。
murong.DelegateIntroduce = Murong.Introduce;//其次如果有重写版本,再赋值给委托。如没有重写版本则不赋值。
}
} public class ChenJD : People
{
public string name = "陈嘉栋";
public static void Introduce(ChenJD _this)
{
string str = "我是ChenJD类,我叫" + _this.name;
System.Console.WriteLine(str);
} public static ChenJD Create()
{
ChenJD chen = new ChenJD();//仅仅分配内存
ChenJD.InitVirCall(chen);
//TODO return chen;
} //因为没有重写基类的Introduce方法,所以不能赋值给DelegateIntroduce
public static void InitVirCall(ChenJD chen)
{
People.InitVirCall(chen);
//因为ChenJD类中的Introduce是重新定义的,所以不加到委托中。
}
}

那么编译运行的结果如图,和本文开头时的结果一致:

到此,我们只使用委托和静态方法就基本实现了虚函数的机制。但是,似乎还缺点什么?假如我们有很多很多虚函数呢?是不是就意味着我们需要很多很多个委托字段呢?而且每个相同的类的实例的委托字段其实都是一样的,但是每一个实例本身都会包含一套委托字段。那样是不是太浪费,太任性了呢?

的确,所以c#虚函数的实现虽然也是这样的思路,但是具体的实现却要机智的多。

委托的集合---vtable

为了解决上文提到的空间浪费的问题,CLR其实是实现了一套所谓的虚函数分派表,或者叫做vtable。在c#中,一个vtable其实就是一套委托的集合。而这个虚表的作用就是使CLR具备了在运行时选择正确的虚函数重写版本的能力。

结合上文的例子实现一个vtable:

    sealed class VTable
{
public readonly Action<People> Introduce;
public VTable(Action<People> delegateIntroduce)
{
this.Introduce = delegateIntroduce;
}
}

当然这个并非是个很好的例子,因为我们只有一个虚函数,所以显得略微势单力薄,不过你应该可以想象的出有很多虚函数时的样子吧。那我们应该如何利用这个虚表类来构建上例那三个类的对应的vtable呢?很简单,使用override关键字重写了虚方法的类,在VTable的构建函数中传入重写后的方法,如果没有重写,则传入该方法的原始定义。

//基类People的vtable的构建
private static VTable PeopleTable = new VTable(People.Introduce); //派生类Murong的vtable的构建
//由于重写了Introduce,所以此处传入重写版本
private static VTable MurongTable = new VTable(Murong.Introduce); //派生类ChenJD的vtable的构建
//由于没有使用override重写,所以此处直接传入在people中定义的原始Introduce
private static VTable ChenJDTable = new VTable(People.Introduce);

这样,我们利用vtable就将原本在每个实例中的委托字段,作为类的静态字段放在对应的类中,而我们需要做的,就是为每个类构建正确的vtable,至于各个实例则只需要保留一份正确vtable的引用就可以了。

而作为实现C#的虚函数机制的结果就如下所示了:

using System;

public class Test1
{ static void Main()
{
People person = People.Create();
person.VTable.Introduce(person); person = Murong.Create();
person.VTable.Introduce(person); person = ChenJD.Create();
person.VTable.Introduce(person); ChenJD chen = ChenJD.Create();
ChenJD.Introduce(chen); }
} public class People
{
//供实例引用
public VTable VTable;
//构建类的vtable
private static VTable PeopleTable = new VTable(People.Introduce);
public string name = "People";
public static void Introduce(People _this)
{
string str = "我是People类,我叫" + (_this as People).name;
System.Console.WriteLine(str);
} public static People Create()
{
People people = new People();//仅仅分配内存
people.VTable = People.PeopleTable;
//TODO
return people;
}
} public class Murong : People
{
private static VTable MurongTable = new VTable(Murong.Introduce);
public string name = "慕容小匹夫";
public static void Introduce(People _this)
{
string str = "我是Murong类,我叫" + (_this as Murong).name;
System.Console.WriteLine(str);
}
public static Murong Create()
{
Murong murong = new Murong();//仅仅分配内存
murong.VTable = Murong.MurongTable;
//TODO return murong;
}
} public class ChenJD : People
{
//由于没有使用override重写,所以此处直接传入在people中定义的原始Introduce
private static VTable ChenJDTable = new VTable(People.Introduce); public string name = "陈嘉栋";
public static void Introduce(ChenJD _this)
{
string str = "我是ChenJD类,我叫" + _this.name;
System.Console.WriteLine(str);
} public static ChenJD Create()
{
ChenJD chen = new ChenJD();//仅仅分配内存
chen.VTable = ChenJD.ChenJDTable;
//TODO return chen;
}
}
//虚表
public class VTable
{
public readonly Action<People> Introduce;
public VTable(Action<People> delegateIntroduce)
{
this.Introduce = delegateIntroduce;
}
}

编译运行,结果如图:

如果各位看官觉得文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~

装模作样的声明一下:本博文章若非特殊注明皆为原创,若需转载请保留原文链接http://www.cnblogs.com/murongxiaopifu/p/4357588.html)及作者信息慕容小匹夫

匹夫细说C#:从园友留言到动手实现C#虚函数机制的更多相关文章

  1. 匹夫细说C#:不是“栈类型”的值类型,从生命周期聊存储位置

    0x00 前言: 匹夫在日常和别人交流的时候,常常会发现一旦讨论涉及到“类型”,话题的热度就会立马升温,因为很多似是而非.或者片面的概念常常被人们当做是全面和正确的答案.加之最近在园子看到有人翻译的& ...

  2. 【基础概念】匹夫细说C#:不是“栈类型”的值类型,从生命周期聊存储位置

    转载地址 https://www.cnblogs.com/murongxiaopifu/p/4419040.html 0x00 前言: 匹夫在日常和别人交流的时候,常常会发现一旦讨论涉及到" ...

  3. 匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密

    0x00 前言 在匹夫的上一篇文章<匹夫细说C#:不是“栈类型”的值类型,从生命周期聊存储位置>的最后,匹夫以总结和后记的方式涉及到一部分迭代器的知识.但是觉得还是不够过瘾,很多需要说清楚 ...

  4. 匹夫细说C#:委托的简化语法,聊聊匿名方法和闭包

    0x00 前言 通过上一篇博客<匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的>的内容,我们实现了使用委托来构建我们自己的消息系统的过程.但是在日常的开发中,仍然有很多开发者因为 ...

  5. 发布园友设计的新款博客皮肤BlueSky

    园友#a为大家设计了一款“简单.纯粹,一点淡雅,一点宁静”的博客皮肤——BlueSky,欢迎您的享用!感谢#a的精心设计! 如果您有兴趣为大家设计博客皮肤,请将您设计的html/css/images文 ...

  6. 回复《我要阻止做java开发的男朋友去创业型公司工作吗?》园友问题

    真的非常开心能收到这么多园友的关心,看到这么多的回复顿感身边处处充满爱.也非常感谢大家踊跃的帮我出谋划策,小女子在此有礼了! 我先来回答一下性别的问题(前面已经暴露了……),我是前端程序媛.大三时和男 ...

  7. 用Xamarin 实现园友的 :Android浮动小球与开机自启动

    原文:用Xamarin 实现园友的 :Android浮动小球与开机自启动 前两天看园子里有筒子写了个 Android浮动小球与开机自启动  , 感觉这种被 360 玩烂的功能原来是如此的简单啊... ...

  8. 用C#开发的双色球走势图(原创)值得园友拥有(二)接上一篇

    昨晚由于时间的原因只写了一部分内容,今天将这一部分内容补充完毕,多谢各位园友的支持. 这是用C#开发的双色球走势图(原创)值得园友拥有 新的园友可以看昨晚写的内容,以免脱节.首先回复园友的评论,有说好 ...

  9. 用C#开发的双色球走势图(原创)值得园友拥有

    首先声明,个人纯粹无聊之作,不作商业用途. 我相信每个人都拥有一个梦想那就是有朝一日能中500W,这个也一直是我的梦想,并默默每一期双色球或多或少要贡献自己一点点力量,本人并不属于那种铁杆的彩票迷,每 ...

随机推荐

  1. ASP.NET Aries 入门开发教程2:配置出一个简单的列表页面

    前言: 朋友们都期待我稳定地工作,但创业公司若要躺下,也非意念可控. 若人生注定了风雨飘摇,那就雨中前行了. 最机开始看聊新的工作机会,欢迎推荐,创业公司也可! 同时,趁着自由时间,抓紧把这系列教程给 ...

  2. xamarin DependencyService源码阅读

    xamarin在面对PCL无法实现的各平台特有功能时使用了一种叫[DependencyService]的方式来实现.它使得xamarin能像原生平台一样做平台能做到的事情!主要分四个部分 接口:定义功 ...

  3. nginx+iis+redis+Task.MainForm构建分布式架构 之 (redis存储分布式共享的session及共享session运作流程)

    本次要分享的是利用windows+nginx+iis+redis+Task.MainForm组建分布式架构,上一篇分享文章制作是在windows上使用的nginx,一般正式发布的时候是在linux来配 ...

  4. 使用蓝灯后,IE浏览器以及内置IE浏览器的程序不能使用的解决方案

    使用完蓝灯后,每次使用IE浏览器都不能正常使用,于是有了下面的这个方案 1.通过Win+R 打开注册表编辑器(regedit) 进入目录 HKEY_CURRENT_USER \ Software \ ...

  5. 深入.NET平台和C#编程总结大全

    对于初学者的你,等到你把这个看完之后就更清楚地认知.NET和C#编程了,好了废话不多说,开始吧!                                                     ...

  6. 开发者最爱的Firebug停止更新和维护

        近日,Firebug团队在其官网上宣布,Firebug将不再继续开发和维护,并邀请大家使用Firefox的内置开发工具.     Firebug最初是2006年1月由Joe Hewitt编写, ...

  7. java设计模式之单例模式(几种写法及比较)

    概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建 ...

  8. win7下利用ftp实现华为路由器的上传和下载

    win7下利用ftp实现华为路由器的上传和下载 1.  Win7下ftp的安装和配置 (1)开始->控制面板->程序->程序和功能->打开或关闭Windows功能 (2)在Wi ...

  9. 【SAP业务模式】之ICS(四):组织单元的配置

    SAP的ICS业务后台配置主要有以下几个配置点: 1.组织单元的配置(公司代码.销售组织.工厂.采购组织等): 2.主数据的部分: 3.订单和开票的定价过程: 4.开票输出类型: 5.公司间发票的配置 ...

  10. 深入理解 Android 之 View 的绘制流程

    概述 本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定 ...