被回调的函数其实是作为参数传递给要执行的函数的。那如何才能将函数作为参数呢?当然是使用委托了,委托的知识这里就不讲解了。
这样的话我们就很好理解了,那我们就开始实现起来。首先定义一个委托:
delegate void DelegateDone(); //定义一个无返回值的委托
再定义一个实现函数,将委托作为参数传递进来:
void DoWork(DelegateDone callBack){ callBack(); }
看看是不是就实现了一个简单的回调函数了啊。
现在我们再回头来看看,是不是必须在函数入参的时候传递进去呢?那当然不是,那上面只是为了便于我们理解回调函数的实现。下面我就我工作中遇到的回调函数使用给大家做一个参考:
public delegate void GetHeartDataDelegate(HeartModel model);
//定义一个委托
public GetHeartDataDelegate GetHeartHandler;
//MQTT接收数据处理
void MQTTReceiver(object sender, MqttMsgPublishEventArgs e)
{
string receiveData = Encoding.Default.GetString(e.Message);
if(null != GetHeartHandler)
{ //省略转换
GetHeartHandler(receiverData); //调用委托函数
}
}
//外部调用
void GetHeartDataCallback(HeartModel model)
{
//省略实现代码
}
void Main()
{
GetHeartHandler=new GetHeartDataDelegate (GetHeartDataCallback);
//绑定需要回调的函数
}
看代码其实很简单,就是在底层我们检测到了数据的传入,回调给上层具体的函数实现。
总结
回调函数其实就是把某个方法当做参数传递给另外一个方法。在实际工作中,底层数据接收的回调,订单支付的回调等等都是基于这个思想实现的。
一切要从这样的需求说起
假设你们公司要开发下一代国民App“明日油条”,一款主打解决国民早餐问题的App。为了加快开发进度,这款应用由A小组和B小组协同开发
其中有一个核心模块由B小组开发然后供A小组调用,这个核心模块被封装成了一个函数,这个函数就叫make_youtiao()。
如果make_youtiao()这个函数执行的很快并可以立即返回,那么A小组的同学只需要
1.调用make_youtiao()
2.等待该函数执行完成
3.该函数执行完后继续后续流程
从程序执行的角度看这个过程是这样的
1.保存当前被执行函数的上下文
2.开始执行make_youtiao()这个函数
3.make_youtiao()执行完后,控制转回到调用函数中
如果世界上所有的函数都像make_youtiao()这么简单,那么程序员大概率就要失业了,还好程序的世界是复杂的,这样程序员才有了存在的价值。
现实情况并不容易
现实中make_youtiao()这个函数需要处理的数据非常庞大,假设有10000个,那么make_youtiao(10000)不会立刻返回,而是可能需要10分钟才执行完成并返回。
这时你该怎么办呢?想一想这个问题。
可能有的同学就像把头埋在沙子里的鸵鸟一样:和刚才一样直接调用不可以吗,这样多简单。
是的,这样做没有问题,但就像爱因斯坦说的那样“一切都应该尽可能简单,但是不能过于简单”。
想一想直接调用会有什么问题?
显然直接调用的话,那么调用线程会被阻塞暂停,在等待10分钟后才能继续运行。在这10分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。
这并不是一种高效的做法。
没有一个程序员想死盯着屏幕10分钟后才能得到结果。
那么有没有一种更加高效的做法呢?
想一想我们上一篇中那个一直盯着你写代码的老板.我们已经知道了这种一直等待直到另一个任务完成的模式叫做同步。
如果你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后自然会通知老板,这样老板和程序员都不需要相互等待,这种模式被称为异步。回到我们的主题,这里一种更好的方式是调用make_youtiao()这个函数后不再等待这个函数执行完成,而是直接返回继续后续流程,这样A小组的程序就可以和make_youtiao()这个函数同时进行了,就像这样:
在这种情况下,回调(callback)就必须出场了。
为什么我们需要回调callback
有的同学可能还没有明白为什么在这种情况下需要回调,别着急,我们慢慢讲。
假设我们“明日油条”App代码第一版是这样写的:
make_youtiao(10000);
sell();
可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。
我们已经知道了由于make_youtiao(10000)这个函数10分钟才能返回,你不想一直死盯着屏幕10分钟等待结果,那么一种更好的方法是让make_youtiao()这个函数知道制作完油条后该干什么,即,更好的调用make_youtiao的方式是这样的:“制作10000个油条,炸好后卖出去”,因此调用make_youtiao就变出这样了:
make_youtiao(10000, sell);
看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外还可以指定制作好后该干什么,第二个被make_youtiao这个函数调用的函数就叫回调,callback。
现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样:
void make_youtiao(int num, func call_back) {
// 制作油条
call_back(); //执行回调
}
这样你就不用死盯着屏幕了,因为你把make_youtiao这个函数执行完后该做的任务交代给make_youtiao这个函数了,该函数制作完油条后知道该干些什么,这样就解放了你的程序。
为什么要用回调?这里是根据业务场景用回调(同步调用方式:A小组调用B小组make_youtiao函数以后A小组再sell卖出去。改成同步回调方式:A小组调用B小组make_youtiao函数并委托B小组制作油条以后B小组再sell卖出去),但是对于程序没有性能提升,感觉要用回调的理由不是很充分。
如果改成异步回调(A小组委托B小组make_youtiao以后再B小组sell卖出去同时A小组不等待B小组,可以去做其他事情)。对程序有性能提升,这里用回调的理由很充分。
有的同学可能还是有疑问,
为什么编写make_youtiao这个小组不直接定义sell函数然后调用呢?
不要忘了明日油条这个App是由A小组和B小组同时开发的,B小组在编写make_youtiao时怎么知道A小组要怎么用这个模块,假设B小组真的自己定义sell函数就会这样写:
void make_youtiao(int num) {
real_make_youtiao(num);
sell(); //执行回调
}
同时B小组设计的模块非常好用,这时C小组也想用这个模块,然而C小组的需求是制作完油条后放到仓库而不是不是直接卖掉,要满足这一需求那么B小组该怎么写呢?
void make_youtiao(int num) {
real_make_youtiao(num);
if (Team_B) {
sell(); // 执行回调
} else if (Team_D) {
store(); // 放到仓库
}
故事还没完,假设这时D小组又想使用呢,难道还要接着添加if else吗?这个问题该怎么解决呢?关于这个问题的答案,你懂的。
在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。
这就是回调函数的定义了。
注意,回调函数是一种软件设计上的概念,和某个编程语言没有关系,几乎所有的编程语言都能实现回调函数。
对于一般的函数来说,我们自己编写的函数会在自己的程序内部调用,也就是说函数的编写方是我们自己,调用方也是我们自己。
但回调函数不是这样的,虽然函数编写方是我们自己,但是函数调用方不是我们,而是我们引用的其它模块,也就是第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用我们编写的回调函数,如图所示:
而之所以需要给第三方库指定回调函数,是因为第三方库的编写者并不清楚在某些特定节点,比如我们举的例子油条制作完成、接收到网络数据、文件读取完成等之后该做什么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供一个回调函数,库的使用方来实现该函数,第三方库在特定的节点调用该回调函数就可以了。
另一点值得注意的是,从图中我们可以看出回调函数和我们的主程序位于同一层中,我们只负责编写该回调函数,但并不是我们来调用的。
最后值得注意的一点就是回调函数被调用的时间节点,回调函数只在某些特定的节点被调用,就像上面说的油条制作完成、接收到网络数据、文件读取完成等,这些都是事件,也就是event,本质上我们编写的回调函数就是用来处理event的,因此从这个角度看回调函数不过就是eventhandler,因此回调函数天然适用于事件驱动编程event-driven,我们将会在后续文章中再次回到这一主题。
同步回调
这种回调就是通常所说的同步回调synchronous callbacks、也有的将其称为阻塞式回调blocking callbacks,或者什么修饰都没有,就是回调,callback,这是我们最为熟悉的回调方式。
当我们调用某个函数A并以参数的形式传入回调函数后,在A返回之前回调函数会被执行,也就是说我们的主程序会等待回调函数执行完成,这就是所谓的同步回调。
回调对应的编程思维模式
让我们用简单的几句话来总结一下回调下与常规编程思维模式的不同。
假设我们想处理某项任务,这项任务需要依赖某项服务S,我们可以将任务的处理分为两部分,调用服务S前的部分PA,和调用服务S后的部分PB。
在常规模式下,PA和PB都是服务调用方来执行的,也就是我们自己来执行PA部分,等待服务S返回后再执行PB部分。
但在回调这种方式下就不一样了。
在这种情况下,我们自己来执行PA部分,然后告诉服务S:“等你完成服务后执行PB部分”。
因此我们可以看到,现在一项任务是由不同的模块来协作完成的。
即:
常规模式:调用完S服务后后我去执行X任务,
回调模式:调用完S服务后你接着再去执行X任务,
其中X是服务调用方制定的,区别在于谁来执行。
为什么异步回调这种思维模式正变得的越来越重要
在同步模式下,服务调用方会因服务执行而被阻塞暂停执行,这会导致整个线程被阻塞,因此这种编程方式天然不适用于高并发动辄几万几十万的并发连接场景,针对高并发这一场景,异步其实是更加高效的,原因很简单,你不需要在原地等待,因此从而更好的利用机器资源,而回调函数又是异步下不可或缺的一种机制。
回调地狱,callback hell
有的同学可能认为有了异步回调这种机制应付起一切高并发场景就可以高枕无忧了。
实际上在计算机科学中还没有任何一种可以横扫一切包治百病的技术,现在没有,在可预见的将来也不会有,一切都是妥协的结果。
那么异步回调这种机制有什么问题呢?
实际上我们已经看到了,异步回调这种机制和程序员最熟悉的同步模式不一样,在可理解性上比不过同步,而如果业务逻辑相对复杂,比如我们处理某项任务时不止需要调用一项服务,而是几项甚至十几项,如果这些服务调用都采用异步回调的方式来处理的话,那么很有可能我们就陷入回调地狱中。
举个例子,假设处理某项任务我们需要调用四个服务,每一个服务都需要依赖上一个服务的结果,
如果用同步方式来实现的话可能是这样的:
a = GetServiceA();
b = GetServiceB(a);
c = GetServiceC(b);
d = GetServiceD(c);
代码很清晰,很容易理解有没有。
我们知道异步回调的方式会更加高效,那么使用异步回调的方式来写将会是什么样的呢?
GetServiceA(function(a){
GetServiceB(a, function(b){
GetServiceC(b, function(c){
GetServiceD(c, function(d) {
....
});
});
});
});
我想不需要再强调什么了吧,你觉得这两种写法哪个更容易理解,代码更容易维护呢?
博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,一个不得不去重读一边代码;另一个在一旁骂自己为什么当初选择维护这个项目。
异步回调代码稍不留意就会跌到回调陷阱中,那么有没有一种更好的办法既能结合异步回调的高效又能结合同步编码的简单易读呢?
幸运的是,答案是肯定的。
Promise, then, await。
本人理解:
执行顺序 : 主程序—库函数----回调函数
1.主程序为何不主动调用库函数,调用回调函数?
同步调用改成异步回调节省主程序线程等待的时间。
2.库函数为何不去实现回调函数逻辑?
①因为库函数不知道主程序调用完成以后需要做什么,库函数是不知道如何写回调函数逻辑。
②就算库函数知道如何写回调函数逻辑。如果还有其他程序调用库函数,那么库函数内部就会有很多if…else…。
如下代码所示:
void make_youtiao(int num)
{
real_make_youtiao(num);
if (Team_B) {
sell(); // 执行回调
} else if (Team_D) {
store(); // 放到仓库
}
3.业务逻辑层BLL层调用数据访问层DAL,如果某种情况下DAL层需要调用BLL层代码的话,这里可以用回调函数。C#项目互相依赖是不允许的,BLL层依赖于DAL层的话,DAL层是无法再调用BLL层。