回调函数

作者:桥头堡
链接:http://www.zhihu.com/question/19801131/answer/27459821
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

什么是回调函数?

我们绕点远路来回答这个问题。

编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。

当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。

打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):

可以看到,回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。(我认为)这应该是回调最早的应用之处,也是其得名如此的原因。

回调机制的优势

从上面的例子可以看出,回调机制提供了非常大的灵活性。请注意,从现在开始,我们把图中的库函数改称为中间函数了,这是因为回调并不仅仅用在应用和库之间。任何时候,只要想获得类似于上面情况的灵活性,都可以利用回调。

这种灵活性是怎么实现的呢?乍看起来,回调似乎只是函数间的调用,但仔细一琢磨,可以发现两者之间的一个关键的不同:在回调中,我们利用某种方式,把回调函数像参数一样传入中间函数。可以这么理解,在传入一个回调函数之前,中间函数是不完整的。换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。这就比简单的函数调用要灵活太多了。请看下面这段Python写成的回调的简单示例:

even.py

#回调函数1
#生成一个2k形式的偶数
def double(x):
    return x * 2
#回调函数2
#生成一个4k形式的偶数
def quadruple(x):
    return x * 4

callback_demo.py

from even import *

#中间函数
#接受一个生成偶数的函数作为参数
#返回一个奇数
def getOddNumber(k, getEvenNumber):
    return 1 + getEvenNumber(k)

#起始函数,这里是程序的主函数
def main():    
    k = 1
    #当需要生成一个2k+1形式的奇数时
    i = getOddNumber(k, double)
    print(i)
    #当需要一个4k+1形式的奇数时
    i = getOddNumber(k, quadruple)
    print(i)
    #当需要一个8k+1形式的奇数时
    i = getOddNumber(k, lambda x: x * 8)
    print(i)

if __name__ == "__main__":
    main()

运行callback_demp.py,输出如下:
3
5
9

上面的代码里,给getOddNumber传入不同的回调函数,它的表现也不同,这就是回调机制的优势所在。值得一提的是,上面的第三个回调函数是一个匿名函数。

易被忽略的第三方

通过上面的论述可知,中间函数和回调函数是回调的两个必要部分,不过人们往往忽略了回调里的第三位要角,就是中间函数的调用者。绝大多数情况下,这个调用者可以和程序的主函数等同起来,但为了表示区别,我这里把它称为起始函数(如上面的代码中注释所示)。

之所以特意强调这个第三方,是因为我在网上读相关文章时得到一种印象,很多人把它简单地理解为两个个体之间的来回调用。譬如,很多中文网页在解释“回调”(callback)时,都会提到这么一句话:“If you call me, I will call you back.”我没有查到这句英文的出处。我个人揣测,很多人把起始函数和回调函数看作为一体,大概有两个原因:第一,可能是“回调”这一名字的误导;第二,给中间函数传入什么样的回调函数,是在起始函数里决定的。实际上,回调并不是“你我”两方的互动,而是ABC的三方联动。有了这个清楚的概念,在自己的代码里实现回调时才不容易混淆出错。

另外,回调实际上有两种:阻塞式回调和延迟式回调。两者的区别在于:阻塞式回调里,回调函数的调用一定发生在起始函数返回之前;而延迟式回调里,回调函数的调用有可能是在起始函数返回之后。这里不打算对这两个概率做更深入的讨论,之所以把它们提出来,也是为了说明强调起始函数的重要性。网上的很多文章,提到这两个概念时,只是笼统地说阻塞式回调发生在主调函数返回之前,却没有明确这个主调函数到底是起始函数还是中间函数,不免让人糊涂,所以这里特意说明一下。另外还请注意,本文中所举的示例均为阻塞式回调。延迟式回调通常牵扯到多线程,我自己还没有完全搞明白,所以这里就不多说了。
编辑于 2014-07-01 37 条评论 感谢 分享 收藏 • 没有帮助 • 举报 • 作者保留权利
黄兢成iOS 开发话题优秀回答者iOS 和 C++ 开发者,https://github.com/hjcapple
266 人赞同
回调函数就是你写好一个函数,让预先写好的系统来调用。你去调用系统的函数,是直调。让系统调用你的函数,就是回调。但假如满足于这种一句话结论,是不会真正明白的。

回调函数可以看成,让别人做事,传进去的额外信息。

比如,A 让 B 做事,根据粒度不同,可以理解成 A 函数调用 B 函数,或者 A 类使用 B 类,或者 A 组件使用 B 组件等等。反正就是 A 叫 B 做事。

当 B 做这件事情的时候,自身的需要的信息不够,而 A 又有。就需要 A 从外面传进来,或者 B 做着做着主动向外面申请。对于 B 来说,一种被动得到信息,一种是主动去得到信息,有人给这两种方式术语,叫信息的 push,和信息的 pull。

A 调用 B,A 需要向 B 传参数。如简单的函数:
int max(int a, int b);
要使用这函数,得到两者最大的值, 外面就要传进来 a, b。这个很好理解。

void qsort(void , size_t, size_t, int ()(const void , const void ));
而这个函数用于排序,最后一个参数就是回调函数,似乎就比较难以理解了。这是因为人为割裂了代码和数据。

我们暂停一下,看看计算机中比较诡异的地方,也就是代码(code)和数据(data)的统一。这是一个槛,如果不打通这个,很多概念就不清楚。我们常常说计算机程序分成 code 和 data 两部分。很多人就会觉得,code 是会运行的,是动的,data 是给 code 使用,是静态的,这是两种完全不同的东西。

其实 code 只是对行为的一种描述,比如有个机器人可以开灯,关灯,扫地。如果跟机器人约定好,0 表示开灯,1 表示关灯,2 表示扫地。我发出指令串,0 1 2,就可以控制机器人开灯,关灯,扫地。再约定用二进制表示,两位一个指令,就有一个数字串,000111,这个时候 000111 这串数字就描述了机器人的一系列动作,这个就是从一方面理解是 code,它可以控制机器人的行为。但另一方面,它可以传递,可以记录,可以修改,也就是数据。只要大家都协商好,code 就可以编码成 data, 将 data 解释运行的时候,也变成了 code。

code 和 data 可以不用区分,统一称为信息。既然 int max(int a, int b) 中 int,double 等表示普通 data 的东西可以传递进去,凭什么表示 code 的函数就不可以传进去了。有些语言确实是不区分的,它的 function(表示code)跟 int, double 的地位是一样的。这种语言就为函数是第一类值。

而有些语言是不能存储函数,不能动态创建函数,不能动态销毁函数。只能存储一个指向函数的指针,这种语言称为函数是第二类值。

另外有些语言不单可以传递函数,函数里面又用到一些外部信息(包括code, data)。那些语言可以将函数跟函数所用到的信息一起传递存储。这种将函数和它所用的信息作为一个整体,就为闭包。

将代码和数据打通,统一起来。过了这个槛,很多难以理解的概念就会清晰很多。

现在我们再回头看看回调函数。回调函数也就是是 A 让 B 做事,B 做着做着,信息不够,不知道怎么做了,就再让外面处理。

比如上述排序例子,A 让 B 排序,B 会做排序,但排序需要知道哪个比哪个大,这点 B 自己不知道,就需要 A 告诉它。而这种判断那个大,本身是一种动作,既然 C 语言中不可以传进第一值的函数,就设计成传递第二值的函数指针,这个函数指针就是 A 传向 B 的信息,用来表示一个行为。这里本来 A 调用 B 的,结果 B 又调用了 A 告诉它的信息,也就叫 callback。

再比如 A 让 B 监听系统的某个消息,比如敲了哪个键。跟着 B 监听到了,但它不知道怎么去处理这个消息,就给外面关心这个消息,又知道怎么去处理这个消息的人去处理,这个处理过程本身是个行为,既然这个语言不可以传递函数,又只能传一个函数指针了。假如我将函数指针存储下来,以后就可以随时调用。代码和数据都是信息,数据可以存储下来,用来表示行为的函数自然也可以存储下来。

跟着有些人有会引申成,什么注册啊,通知啊等等等。假如 B 做监听,C, D, E, F, G, H 告诉 B 自己有兴趣知道这消息,那 B 监听到了就去告诉 C,D,E,F,G等人了,这样通知多人了,就叫广播。

其实你理解了,根本不用去关心术语的。术语是别人要告诉你啊,或者你去告诉人啊,使用的一套约定的词语。本质上就这个东西,结果会有很多术语的。

跟着再将回调的概念泛化,比如某人同时关心 A, B, C, D, E, F 事件,并且这些事件是一组的,比如敲键盘,鼠标移动,鼠标点击等一组。将一组事件结合起来。在有些语言就映射成接口,接口有 N 个函数。有些语言就映射成一个结构,里面放着 N 个函数指针。跟着就不是将单个函数指针传进去,而是将接口,或者函数指针的结构传进去。根据不同的用途,有些人叫它为代理,监听者,观察者等等。

实际上也是将某种行为存储下来,以后有需要再进行调用。跟回调函数在更深层次是没有区别的。

以下是对提供的参考资料的总结,按照要求结构化多个要点分条输出: 4G/5G无线网络优化与网规案例分析: NSA站点下终端掉4G问题:部分用户反馈NSA终端频繁掉4G,主要因终端主动发起SCGfail导致。分析显示,在信号较好的环境下,终端可能因节能、过热保护等原因主动释放连接。解决方案建议终端侧进行分析处理,尝试关闭节电开关等。 RSSI算法识别天馈遮挡:通过计算RSSI平均值及差值识别天馈遮挡,差值大于3dB则认定有遮挡。不同设备分组规则不同,如64T和32T。此方法可有效帮助现场人员识别因环境变化引起的网络问题。 5G 160M组网小区CA不生效:某5G站点开启100M+60M CA功能后,测试发现UE无法正常使用CA功能。问题原因在于CA频点集标识配置错误,修正后测试正常。 5G网络优化与策略: CCE映射方式优化:针对诺基亚站点覆盖农村区域,通过优化CCE资源映射方式(交织、非交织),提升RRC连接建立成功率和无线接通率。非交织方式相比交织方式有显著提升。 5G AAU两扇区组网:与三扇区组网相比,AAU两扇区组网在RSRP、SINR、下载速率和上传速率上表现不同,需根据具体场景选择适合的组网方式。 5G语音解决方案:包括沿用4G语音解决方案、EPS Fallback方案和VoNR方案。不同方案适用于不同的5G组网策略,如NSA和SA,并影响语音连续性和网络覆盖。 4G网络优化与资源利用: 4G室分设备利旧:面对4G网络投资压减与资源需求矛盾,提出利旧多维度调优策略,包括资源整合、统筹调配既有资源,以满足新增需求和提质增效。 宏站RRU设备1托N射灯:针对5G深度覆盖需求,研究使用宏站AAU结合1托N射灯方案,快速便捷地开通5G站点,提升深度覆盖能力。 基站与流程管理: 爱立信LTE基站邻区添加流程:未提供具体内容,但通常涉及邻区规划、参数配置、测试验证等步骤,以确保基站间顺畅切换和覆盖连续性。 网络规划与策略: 新高铁跨海大桥覆盖方案试点:虽未提供详细内容,但可推测涉及高铁跨海大桥区域的4G/5G网络覆盖规划,需考虑信号穿透、移动性管理、网络容量等因素。 总结: 提供的参考资料涵盖了4G/5G无线网络优化、网规案例分析、网络优化策略、资源利用、基站管理等多个方面。 通过具体案例分析,展示了无线网络优化中的常见问题及解决方案,如NSA终端掉4G、RSSI识别天馈遮挡、CA不生效等。 强调了5G网络优化与策略的重要性,包括CCE映射方式优化、5G语音解决方案、AAU扇区组网选择等。 提出了4G网络优化与资源利用的策略,如室分设备利旧、宏站RRU设备1托N射灯等。 基站与流程管理方面,提到了爱立信LTE基站邻区添加流程,但未给出具体细节。 新高铁跨海大桥覆盖方案试点展示了特殊场景下的网络规划需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值