感谢刘铁猛老师的《C#入门详解》和擅码网Monkey老师的《C#面向对象基础》
本专栏的委托与事件部分已经更新完毕,跳转链接如下:
第一篇:感性认识委托
感性认识委托 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/146341073
第二篇:函数指针:委托的由来
函数指针:委托的由来 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/146637091
第三篇:委托的用法
委托的用法 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147242231
第四篇:感性认识事件
闹钟响了我起床——感性认识事件 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147932169
第五篇:事件的调用
事件的调用 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/148561855
第六篇:事件的完整声明,触发和事件的本质
事件的完整声明,触发和事件的本质 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/150967817
第七篇:为什么我们需要事件&补充和总结
为什么我们需要事件&补充和总结 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/162065756
第八篇:用委托事件机制模拟游戏场景
浅谈C#委托事件机制:开阔地机枪兵对射问题(8) - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/166465013
每个程序员都会对自己掌握的第一门语言怀有特殊感情,对我来说,这种语言正是C#;希望我的文字能为大家带来一点帮助,还请多多指教~
作为系列的第二篇文章;我们接着上一篇,说说委托的由来,这有助于我们学习下一课:委托的用法
委托的用法 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147242231
本文和主线课程的联系并不太紧密,更接近背景知识,如果你不敢兴趣请直接移步到下一课:
上一篇我们有提到委托这种语法实体之所以存在,除了现实的语法逻辑需要之外,还有历史传承的原因。
这种历史传承就是C语言和C++语言的函数指针[Function Pointer]
众所周知,指针系统是C语言语法中最难以理解的部分;我想这种难以理解有90%的原因都是失败的教材或老师导致的。(我在学习计算机程序的很长时间里都面临着这样的困惑:当我看到一个难以理解的概念时,教材总是用更多更加不知所谓的名词来解释第一个概念)
所以为了方便说明委托和函数指针的关系,这里有必要简单给没有接触过C/C++的同学解释一下函数指针是什么,如果你已经掌握这部分知识的话请直接移步下一篇专栏文章
语法糖与“一切皆地址”
所有出于方便编程的目的而简化的语法都是“语法糖”。
在讲函数指针之前,我们得先了解一些更基本的概念,比如,众所周知,计算机中所有的数据都以0和1的形式存储,而0和1本身是集成电路的“开”或“关”的状态。
在计算机这种东西刚被发明的时候,程序是以程序纸带的形式输入到机器中去的,机器读取纸袋上不同位置是否被打了孔来决定这里是0还是1,这种一大堆0和1组成的编码虽然对人类来说会造成san值狂跌,但却是唯一一种可以被计算机识别的语言(我们称为机器码)
因为解读和写作机器码的工作实在太反人类了,“指令”作为一种更先进的编程方式被发明了出来。
指令本身是对一串机器码的封装(比如把一串代表加法运算操作的0和1封装成一个单词“ADD”),只要输入封装指令的单词就能调用这串机器码,虽然指令有着泛用性差(只能运行于特定平台),并且语法依然反人类等种种问题;但毕竟还是比直接看0和1要友好多了。
指令就是一种封装了机器码的语法糖。
实际上,所有高级编程语言都是封装了机器码的语法糖(只不过封装的程度非常高罢了);而大家经常听说的“C语言是介于底层语言和高级语言之间的语言”,正是因为C语言没有把所有东西都包裹在语法糖衣里面,C语言允许直接操作内存的功能“指针”正是一种被拨开了糖衣的存在。
我们知道,程序实际上是在内存中运行的,而内存是一片连续的空间,这片空间就像一片一望无际的整齐田野,每片田里都种着一个0或者一个1,而这些0和1的组合构成了我们的数据世界:
有一些连续的0和1存储着数据,另一些则存储着逻辑;但0和1本身是没有意义的,只有明确知道从哪片田到哪片田里种的东西是什么,并且知道正确的解码方式,我们才能从无意义的0和1中解读出意义。
事实上,每片田都有一个独一无二的物理地址(这个电路开关的位置),而一组这样的信息规定了我们根据物理地址去检索信息的范围:
组成待检索信息第一位(这里的位指第一个0或1)的位置,以及待检索的信息长度(总共包括多少位)
现在,C语言中“一切皆地址”的概念也就不难理解了:
变量(数据)是以某个地址为起点的一段内存中存储的值——变量是用于寻找数据的地址
函数(算法)是以某个地址为起点的一段内存中存储的一系列机器指令——函数是用于寻找算法的地址
既然程序是由数据+算法组成的,而数据和算法都是地址,那当然“一切皆地址”了。
函数指针
现在,你大概已经猜到,所谓的“函数指针”就是函数所存储的算法在电脑中所实际存储的位置信息(这种信息包括了第一位地址,所占位数和解码方式);下面我们来详细说一说函数指针:
函数指针:手动定义一种数据类型来描述某种类型的函数,随后可以将符合描述之函数的堆空间地址(机器码第一位的地址)存入给定的引用变量。
之所以要这样做,是因为一切数据在内存中都以0和1来存储,要单纯用0和1来存储有效的数据或指令需要两个条件,一是给定一个连续的范围,将此范围的所有位作为待解析的数据;二是给定一个合理的解析方式(比如同样是32位数据,int和float都用32位0和1来存储,但将数据解码成对人类程序员友好的语言时,前者只需要将2进制数转换为10进制数,后者则必须按照指定的规则将32位机器码作为“原料”进行单精度浮点运算)
通过定义函数指针来规定数据类型以描述某些函数,也一样是出于满足上述两种条件的目的:
其一是定义从给定堆空间地址往后究竟数多少位存储着我们需要的函数,其二是定义计算机要怎样对这些位存储的机器码进行解码才能得到我们需要的结果
(对于堆空间是什么暂时不用去纠结,可以暂时理解为内存分为堆空间和栈空间,后者执行实际运算,总面积小,而前相对较大,被用于存放一些比较大的数据,而函数作为需要很多0和1才能存储的“肥胖”数据,一般都是放在堆空间里的——栈空间里仅仅用一个引用变量存储函数实际的堆空间位置)
而指针的危险性正在于此:如果我们去找某个人的唯一依据是此人居所的门牌号码,很可能走到目的地却发现里面住的根本不是要找的人——而计算机仍然会按照既定的位数和既定的解码规则去进行解析,这很容易出现意料之外的严重错误(比如一段存储着这样机器码的内存:000000000000000000000000000000000000000000000000000000000001000,如果从第一位开始按int解码,解出来是0,但如果这实际上是个long类型的数据“8”——计算机会自然的无视后面的“00000000000000000000000000001000”,理所当然的认为这就是0)
现在我们通过代码实例来看看函数指针的用法,大家看看能不能看出和委托有什么相似性:
先看一个不用函数指针的例子,和C#很像:
#include <stdio.h>
//定义一个加法函数
int Add(int a, int b) {
return a + b;
}
//定义一个减法函数
int Sub(int a, int b) {
return a - b;
}
//Main函数,可以类比为C#的Main方法
int main()
{
int x = 100;
int y = 200;
int z = 0;
printf("%d+%d=%dn", x, y, z);
//用变量z来接收Sub(x,y)的返回值
z = Sub(x, y);
printf("%d+%d=%dn", x, y, z);
//return 0的意思是告诉电脑程序已经结束了
return 0;
}
对于上面的Add和Sub函数,我们通常使用函数名+函数调用操作符"f(x)"的方式进行直接调用,但在C/C++语言中允许通过直接访问函数存储位置对函数进行调用,这种功能就是函数指针
下面是用函数指针实现调用的例子:
#include <stdio.h>
//定义一种函数指针数据类型Calc
//Calc类型是一种包含两个int类型传值参数且返回int类型数据的函数的指针,*Calc是函数指针的意思
typedef int(*Calc)(int a, int b);
int Add(int a, int b) {
return a + b;
}
int Sub(int a, int b) {
return a - b;
}
int main()
{
int x = 100;
int y = 200;
int z = 0;
//声明两个Calc类型变量,分别存储Add函数和Sub函数的地址
Calc funPoint1 = &Add;
Calc funPoint2 = ⋐
//直接调用函数地址来调用函数
z = funPoint1(x,y);
printf("%d+%d=%dn", x, y, z);
z = funPoint2(x, y);
printf("%d+%d=%dn", x, y, z);
return 0;
注意定义函数指针那句代码的语法:
//Calc类型是一种包含两个int类型传值参数且返回int类型数据的函数的指针,*Calc是函数指针的意思
typedef int(*Calc)(int a,int b);
有没有发现和C#语言中的委托很相似?
我们再回顾一下上一篇文章中定义委托的语法:
委托的标准声明格式为:
delegate method-return-type delegate-name (params-list);
其中,委托的返回值类型必须与method-return-type一致,而参数列表也必须与params-list一致
由此,我们可以看出,委托这一语法实体其实是C/C++函数指针功能在面向对象时代的自然延续,但是,千万不要认为委托和函数指针是同一种东西!
委托不是函数指针
C#中保留了真正的指针,但对指针的使用具有双保险:一是在编译器中打开“允许不安全代码”选项,二是指针必须处于unsafe语句块中(语法和C/C++是一样的,就不演示了)
C#的委托是一种普通的语句,并不需要放置在unsafe语句块中,可见delegate[委托]并非真正的函数指针,只是与函数指针有一些相似性:
委托是一个类,它定义了方法的数据类型(比如可以将符合以下描述的方法定义为一种数据类型:“具有一个int返回值和两个int参数的方法”),使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性
从这个定义,我们可以看出委托虽然发展自函数指针,但不像函数指针那样直接记录某函数的“门牌号码”和解析规则以供间接调用函数;而是以面向对象的方式定义一个类,这个类描述的是符合某种特征的方法,而这个类的对象可以作为参数被其它方法调用;委托当然也可以被直接调用。(因为委托的实例封装着方法,委托实例就是一种特殊的方法)
至少,相比于函数指针来说委托要安全的多——委托虽然具有危险性,但危险性来自委托造成的紧密耦合关系,而不是像函数指针那样具备编译层面的危险性
现在,我们对委托已经有了感性的认识,对委托是怎么来的也有了了解,下面一篇文章就可以正式说说委托的用法了~