用实际例子理解回调函数(Calback)

来源:互联网 发布:数学天才学编程难嘛 编辑:程序博客网 时间:2024/06/05 08:03

用实际例子理解回调函数(Calback)

在我们编码的过程中,调用和回调几乎无处不在,但是我对回调函数到底是怎样一回事并没有一个真正透彻的理解,最近我查找学习了一些资料,学到了很多。
我参考了一些知乎上的分享,很不错。
https://www.zhihu.com/question/19801131

1 调用和回调的定义

1.1 调用

对于调用,相信大家都不陌生,在一个应用系统中,模块之间必然存在调用。
什么是调用?
如图1.1所示,类A中的方法a()调用类B的方法b(),方法b()执行完毕会传回结果给方法a(),方法a()会用到这个结果继续往下执行。
调用

下面通过一个场景来体会下调用,以Java为例。
有两个类Employee类和Boss类。
1.Employee类:可以看作一个工具类,它的doAnswer()方法,能够对传进来的任务参数“1+1=?”计算出答案。

public class Employee{    public String doAnswer(String work){        if ("1+1=?".equals(work)) {            return 2;        }        else{            return null;    }}

2.Boss类:他有一项任务“1+1=?”需要完成,包括计算出答案,然后提交答案。
Boss类调用Employee类的doAnswer()方法来计算出答案。
boss得到employee给出的答案,然后用这个答案完成自己的doWork()方法(也就是提交答案)。

public class Boss{    public static void main(String[] args) {        Boss boss=new Boss();        String aWork="1+1=?";//任务        Employee employee=new Employee();         String theAnswer=employee.doAnswer(aWork);        boss.doWork(aWork,theAnswer);    }    public void doWork(String work,String answer){        if (answer!=null)) {            System.out.println("答案:"+answer);        }        else{            System.out.println("答案:"+"空白");        }    }}

1.2 回调

那么什么是回调呢?
回调:如图类A的a()方法调用类B的b()方法,类B的b()方法执行完毕后,主动调用类A的callback()方法。这就是回调。
回调是一种双向的调用方式。
回调

知道了回调,我们再分析一下上边提到的那个调用的应用场景。
对于上边的调用,我们可以看出有些麻烦,employee计算出答案后,boss需要将答案抄写过来,然后提交答案。
为什么不能让employee计算出答案后,直接帮boss提交答案呢?所以这就要用到回调。

依旧是两个类Boss类和Employee类。
1.Boss类:boss依旧是这项任务“1+1=?”需要完成,包括计算出答案,然后提交答案。
Boss类调用Employee类的doAnswer()方法来完成这项任务。这次不仅将任务传给doAnswer()方法,而且将自己的引用也传给它,让doAnswer()方法计算出答案后,直接帮boss提交答案。
这就相当于boss将自己的任务和账号都告诉employee,直接让employee计算出答案后用boss的账号提交答案。

public class Boss{    public static void main(String[] args) {        Boss boss=new Boss();        String aWork="1+1=?";        Employee employee=new Employee();        employee.doAnswer(aWork,boss);    }    public void doWork(String work,String answer){        if (answer!=null)) {            System.out.println("答案:"+answer);        }        else{            System.out.println("答案:"+"空白");        }    }}

2.Employee类:依旧是个工具类,但是它的doAnswer()方法有两个传入的参数了,包括任务参数和身份参数(Boss的引用)。
当计算出答案后,employee用boss的引用调用boss的doWork()方法,这就相当于employee帮boss提交了答案。
其中,boss的doWork()方法就是回调函数。

public class Employee{    public String doAnswer(String work,Boss boss){        if ("1+1=?".equals(work)) {            boss.doWork(work,"2");//doWork是回调函数        }        else{            boss.doWork(work,"空白");    }}

总结一下这个例子的流程:
(1)Boss完成任务的doWork()方法有两个入参:①任务内容,②任务答案。
(2)Employee为了帮Boss完成任务提供了一个doAnswer()方法,该方法有两个入参:①任务内容②Boss的引用。
(3)程序执行时,Boss只要调用Employee的doAnswer()方法就行了。因为一旦Employee计算出答案,会直接根据Boss的引用调用Boss的doWork()方法提交好答案。
抽象点描述:
类A调用类B的方法b(传入相关信息)完成一定的功能,类B的方法无法实现全部功能,需要反过头来调用类A的方法a来完成。
其中,方法a就是传说中的回调函数。
再抽象点描述:
调用(A调用B),回调(B调用A)。

通过上边逐步递进的思考,大家应该都应该知道回调函数是怎样一回事了。总结一下回调函数的实现原理:
(1)定义一个函数a;
(2)提供函数实现的一方A在初始化的时候,将函数a的函数指针注册给调用者B;
(3)当特定的事件或条件发生的时候,调用者B使用函数指针调用函数a对事件进行处理。
即把我要执行的这个任务写成一个函数,将这个函数和某一时间或者事件或者中断建立关联。当这个关联完成的时候,这个函数华丽的从普通函数变身成为回调函数。
所以我们可以知道:
①函数可以理解为一个功能体,执行它可以完成一个任务。回调函数就是一个函数,只是执行时间和执行主体与普通的函数稍有区别。
②使用回调函数本质上就是一种利用函数指针进行函数调用的过程。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
2 回调函数的改进——接口方式
我们分析一下上边回调中举的Boss和Employee的例子,可以看出有两点不妥:
1.常常使用回调函数的同学可能会说,没怎么见过在调用函数中直接把对象的引用写到第一次调用的方法里面的。确实,只是调用Employee类的方法完成任务,用得着把自己的全部暴露给别人吗?这样做是极其不安全的。
2.Employee类是工具类,用来解决Boss提出的任务,通过回调Boss的doWork()方法完成任务。如果有个工人也有相同的任务想让Empoloyee来解决,应该怎么办呢?难道再开个方法,专门接收工人的引用作为传参?如果还有警察也有相同任务,该怎么办?处理任务的就Employee,而任务提出者可以是各种各样的,难道对不同的角色都单独开一个方法吗?
所有的对象要有同一个方法,这也就引出了接口的概念。
一般的做法是将回调方法doWork()做成一个接口,不同的任务提出者去实现该接口,并把自己的接口实现类的对象在调用任务处理者时传给它。
那么不管你是老板、工人、警察等,都可以直接调用Employee类的doAnswer()方法来解决你的任务。

下面来看一下接口方式的回调函数的应用场景:
1.将回调函数抽象出来作为接口。这个接口的作用,仅仅是用来规定doWork()方法长什么样。

public interface DoWork{    void  doWork(String work,String answer);}

2.创建一个工人Worker类,在该类中实现上边的回调函数接口,那么就默认继承的doWork()方法。

public class Worker implements DoWork{    public static void main(String[] args) {        Worker worker=new Worker();        String work="1+1=?";        Employee employee=new Employee();        employee.doAnswer(work,worker);    }    @override    public void doWork(String work,String answer){        if (answer!=null) {            System.out.println("答案:"+answer);        }        else{            System.out.println("答案:"+"空白");        }    }}

3.Employee工具类
Empolyee不需要改动任何代码,就可以直接完成任务了。

public class Employee{    public String doAnswer(String work,DoWork someone){        if ("1+1=?".equals(work)) {            someone.doWork(work,"2");        }        else{            someone.doWork(work,"空白");    }}

3 回调函数的优缺点
3.1 优点
1.很多时候回调函数可以用来执行条件驱动的任务。即当该回调函数关心的那个条件被触发时,回调函数将被执行。条件触发可以是某一时间到了或者某一事件发生或者某一中断触发。

回调函数不是由该函数的实现方直接调用,而是在特定的条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

  1. 使程序设计更灵活。允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。而且这一设计允许了底层代码调用在高层定义的子程序。

3.回调函数最大的优势应用就是异步回调。
程序变成异步的了,也就是你不必再调用这个函数的时候一直等待这个时间的到达、事件的发生或中断的发生(万一一直不发生,你的程序会怎么样?)
在此期间你可以做别的事情,或者四处逛逛。当回调函数被执行时,你的程序重新得到执行的机会,此时你可以继续做必要的事情了。
3.2 缺点
1.学习成本会比普通函数高,需要有一定的抽象思维能力,需要对应用场景的理解。
2.回调函数很多情况下会附带有跨线程操作甚至于跨进程的操作,这些都是异步带来的成本。