如何用简单的英语解释回调?它们与从一个函数调用另一个函数从调用函数获取上下文有什么不同?如何向新手程序员解释它们的强大功能?
当前回答
为了教授回调,你必须先教授指针。一旦学生理解了指向变量的指针的概念,回调的概念就会变得更容易。假设您使用的是C/ c++,可以遵循这些步骤。
First show your students how to use and manipulate variables using pointers alongside using the normal variable identifiers. Then teach them there are things that can be done only with pointers(like passing a variable by reference). Then tell them how executable code or functions are just like some other data(or variables) in the memory. So, functions also have addresses or pointers. Then show them how functions can be called with function pointers and tell these are called callbacks. Now, the question is, why all these hassle for calling some functions? What is the benefit? Like data pointers, function pointer aka callbacks has some advantages over using normal identifiers. The first one is, function identifiers or function names cannot be used as normal data. I mean, you cannot make a data structure with functions(like an array or a linked list of functions). But with callbacks, you can make an array, a linked list or use them with other data like in dictionary of key-value pairs or trees, or any other things. This is a powerful benefit. And other benefits are actually child of this one. The most common use of callbacks is seen in event driver programming. Where one or more functions are executed based on some incoming signal. With callbacks, a dictionary can be maintained to map signals with callbacks. Then the input signal resolution and execution of corresponding code become much easier. The second use of callbacks coming in my mind is higher order functions. The functions which takes other functions as input arguments. And to send functions as arguments, we need callbacks. An example can be a function which take an array and a callback. Then it performs the callback on each of the item of the array and return the results in another array. If we pass the function a doubling callback, we get a doubled valued array. If we pass a squaring callback, we get squares. For square roots, just send appropriate callback. This cannot be done with normal functions.
可能还有更多的事情。让学生参与进来,他们就会发现。希望这能有所帮助。
其他回答
从一个例子开始总是更好的:)。
假设有两个模块A和B。
你希望模块A在模块B中发生某些事件/条件时得到通知。然而,模块B对模块A一无所知。它只知道模块A提供给它的函数指针指向模块A的特定函数的地址。
因此,所有B现在必须做的是,当一个特定的事件/条件发生时,使用函数指针“回调”到模块A。A可以在回调函数内部进行进一步处理。
这里一个明显的优点是,你从模块B中抽象出了模块A的所有内容。模块B不必关心模块A是谁/什么。
在非程序员的术语中,回调是程序中的填空。
许多纸质表格上常见的一项是“紧急情况下的电话号码”。这里有一个空行。你写上某人的名字和电话号码。如果发生紧急情况,就会打电话给那个人。
每个人都有相同的空白表格,但是 每个人都可以写不同的紧急联系电话。
这是关键。您不需要更改表单(代码,通常是其他人的)。不过,你可以填写缺失的信息(你的电话号码)。
示例1:
回调被用作自定义方法,可能用于添加/更改程序的行为。例如,一些C代码执行一个函数,但不知道如何打印输出。它所能做的就是创造一个字符串。当它试图弄清楚该如何处理字符串时,它看到了一个空行。但是,程序员给了你写回调的空白!
在这个例子中,你不用铅笔在纸上填空白,你使用函数set_print_callback(the_callback)。
模块/代码中的空白变量是空行, Set_print_callback是铅笔, the_callback是你要填写的信息。
现在您已经在程序中填充了这一行空白。当它需要打印输出时,它将查看空白行,并遵循那里的指示(即调用您放在那里的函数)。实际上,这允许打印到屏幕、日志文件、打印机、通过网络连接或它们的任何组合。你已经填满了你想做的事情。
示例2:
当你被告知需要拨打一个紧急电话号码时,你去阅读纸质表格上写的内容,然后拨打你读到的号码。如果这一行是空的,什么也不会做。
Gui编程的工作原理与此大致相同。当一个按钮被点击时,程序需要弄清楚下一步要做什么。它去寻找回调。这个回调恰好在一个空白中,标签是"点击Button1时你所做的事情"
大多数ide会在你要求它(例如button1_clicked)时自动为你填充空白(编写基本方法)。不过那个空白可以有任何方法你都可以补好。你可以调用方法run_computices或butter_the_biscuits,只要你把那个回调的名字放在适当的空格里。你可以把“555-555-1212”写在紧急号码栏里。这没什么意义,但这是允许的。
最后提示:用回调填充的空行?它可以被随意擦除和重写。(你是否应该是另一个问题,但这是他们权力的一部分)
现实生活中的例子
这里有一个真实的例子,嗯,我自己的生活。
当我下午5点结束我的工作时,我的待办事项清单上有很多事情:
打电话给兽医要我的狗的检查结果。 遛狗。 处理我的税务问题。 洗碗。 回复私人邮件。 洗衣服。
当我打电话给兽医时,我接到了一个接待员的电话。接待员告诉我,我需要等待兽医,这样兽医就可以向我解释测试结果。接待员想让我等一下,直到兽医准备好。
你对此有什么反应?我知道我的工作多么低效!所以我向接待员提议,当兽医准备好谈话时,他让兽医给我回个电话。这样,我就不用等电话了,我可以做其他的事情。等兽医准备好了,我就可以把其他的事情暂时搁置,和她谈谈。
它和软件有什么关系
我是单螺纹的。我一次只能做一件事。如果我是多线程的,我将能够并行处理多个任务,但不幸的是,我不能这样做。
如果回调不是一个东西,当我遇到异步任务时,它会阻塞。如。当我打电话给兽医时,兽医需要大约15分钟来完成她正在做的事情,然后她才能和我说话。如果没有回调,我在这15分钟内就会被屏蔽。我就只能坐着等,而不能做其他的工作。
下面是没有回调的代码的样子:
function main() {
callVet();
// blocked for 15 minutes
walkDog();
doTaxes();
doDishes();
answerPeronalEmails();
doLaundry();
}
现在用回调:
function main() {
callVet(function vetCallback(vetOnThePhoneReadyToSpeakWithMe) {
talkToVetAboutTestResults(vetOnThePhoneReadyToSpeakWithMe);
});
walkDog();
doTaxes();
doDishes();
answerPeronalEmails();
doLaundry();
}
更一般地说,当您处于单线程执行环境中,并且有某种异步任务时,您可以使用回调以更合乎逻辑的顺序执行事情,而不是让该任务阻塞您的单线程。
一个很好的例子是,如果您有一些前端代码需要发出ajax请求。如。如果您有一个显示用户信息的仪表板。下面是它如何在没有回调的情况下工作。用户将立即看到导航栏,但他们必须等待一段时间才能看到边栏和页脚,因为ajax请求getUser需要一段时间(作为经验法则,网络被认为是很慢的)。
function main() {
displayNavbar();
const user = getUser();
// wait a few seconds for response...
displayUserDashboard(user);
displaySidebar();
displayFooter();
}
现在用回调:
function main() {
displayNavbar();
getUser(function (user) {
displayUserDashboard(user);
});
displaySidebar();
displayFooter();
}
通过利用回调,我们现在可以在ajax请求的响应返回给我们之前显示边栏和页脚。这就好比我对接待员说:“我不想在电话上等15分钟。兽医准备好和我谈谈的时候给我回电话,与此同时,我会继续做我待办事项清单上的其他事情。”在现实生活中,你可能应该更优雅一些,但在编写软件时,你可以对CPU非常粗鲁。
A callback is a function that will be called by a second function. This second function doesn't know in advance what function it will call. So the identity of the callback function is stored somewhere, or passed to the second function as a parameter. This "identity," depending on the programming language, might be the address of the callback, or some other sort of pointer, or it might be the name of the function. The principal is the same, we store or pass some information that unambiguously identifies the function.
当时间到来时,第二个函数可以调用回调,根据当时的情况提供参数。它甚至可以从一组可能的回调中选择回调。编程语言必须提供某种语法,允许第二个函数调用回调函数,并知道它的“标识”。
这种机制有许多可能的用途。通过回调,函数的设计者可以通过调用所提供的任何回调来定制函数。例如,排序函数可能将回调函数作为参数,而这个回调函数可能是用于比较两个元素以决定哪个元素先出现的函数。
顺便说一下,根据编程语言的不同,上面讨论中的“函数”一词可能会被“块”、“闭包”、“lambda”等取代。
在计算机编程中,回调是对可执行代码或一段可执行代码的引用,它作为参数传递给其他代码。这允许较低级别的软件层调用较高级别的软件层定义的子例程(或函数)。——维基百科
在C语言中使用函数指针进行回调
在C语言中,回调是使用函数指针实现的。函数指针——顾名思义,是一个指向函数的指针。
例如,int (*ptrFunc) ();
这里,ptrFunc是一个指向不带参数并返回整数的函数的指针。不要忘记加上圆括号,否则编译器会认为ptrFunc是一个普通的函数名,它不接受任何参数,只返回一个指向整数的指针。
下面是演示函数指针的一些代码。
#include<stdio.h>
int func(int, int);
int main(void)
{
int result1,result2;
/* declaring a pointer to a function which takes
two int arguments and returns an integer as result */
int (*ptrFunc)(int,int);
/* assigning ptrFunc to func's address */
ptrFunc=func;
/* calling func() through explicit dereference */
result1 = (*ptrFunc)(10,20);
/* calling func() through implicit dereference */
result2 = ptrFunc(10,20);
printf("result1 = %d result2 = %d\n",result1,result2);
return 0;
}
int func(int x, int y)
{
return x+y;
}
现在让我们尝试理解C语言中使用函数指针的回调概念。
完整的程序有三个文件:callback.c, reg_callback.h和reg_callback.c。
/* callback.c */
#include<stdio.h>
#include"reg_callback.h"
/* callback function definition goes here */
void my_callback(void)
{
printf("inside my_callback\n");
}
int main(void)
{
/* initialize function pointer to
my_callback */
callback ptr_my_callback=my_callback;
printf("This is a program demonstrating function callback\n");
/* register our callback function */
register_callback(ptr_my_callback);
printf("back inside main program\n");
return 0;
}
/* reg_callback.h */
typedef void (*callback)(void);
void register_callback(callback ptr_reg_callback);
/* reg_callback.c */
#include<stdio.h>
#include"reg_callback.h"
/* registration goes here */
void register_callback(callback ptr_reg_callback)
{
printf("inside register_callback\n");
/* calling our callback function my_callback */
(*ptr_reg_callback)();
}
如果我们运行这个程序,输出将是
这是一个演示函数回调的程序 内部register_callback 内部my_callback 回到主程序
上层函数像正常调用一样调用下层函数,而回调机制允许下层函数通过指向回调函数的指针调用上层函数。
Java中使用接口的回调
Java没有函数指针的概念 它通过接口机制实现回调机制 在这里,我们声明了一个接口,而不是函数指针,它有一个方法,当被调用方完成其任务时将被调用
让我通过一个例子来说明:
回调接口
public interface Callback
{
public void notify(Result result);
}
调用者或更高级别的类
public Class Caller implements Callback
{
Callee ce = new Callee(this); //pass self to the callee
//Other functionality
//Call the Asynctask
ce.doAsynctask();
public void notify(Result result){
//Got the result after the callee has finished the task
//Can do whatever i want with the result
}
}
被调用者或底层函数
public Class Callee {
Callback cb;
Callee(Callback cb){
this.cb = cb;
}
doAsynctask(){
//do the long running task
//get the result
cb.notify(result);//after the task is completed, notify the caller
}
}
使用EventListener模式
列表项
此模式用于通知0到n个观察者/监听器某个特定任务已经完成
列表项
回调机制和EventListener/Observer机制之间的区别在于,在回调中,被调用方通知单个调用方,而在Eventlisener/Observer中,被调用方可以通知任何对该事件感兴趣的人(通知可能会到应用程序中尚未触发任务的其他部分)。
让我通过一个例子来解释。
事件接口
public interface Events {
public void clickEvent();
public void longClickEvent();
}
类部件
package com.som_itsolutions.training.java.exampleeventlistener;
import java.util.ArrayList;
import java.util.Iterator;
public class Widget implements Events{
ArrayList<OnClickEventListener> mClickEventListener = new ArrayList<OnClickEventListener>();
ArrayList<OnLongClickEventListener> mLongClickEventListener = new ArrayList<OnLongClickEventListener>();
@Override
public void clickEvent() {
// TODO Auto-generated method stub
Iterator<OnClickEventListener> it = mClickEventListener.iterator();
while(it.hasNext()){
OnClickEventListener li = it.next();
li.onClick(this);
}
}
@Override
public void longClickEvent() {
// TODO Auto-generated method stub
Iterator<OnLongClickEventListener> it = mLongClickEventListener.iterator();
while(it.hasNext()){
OnLongClickEventListener li = it.next();
li.onLongClick(this);
}
}
public interface OnClickEventListener
{
public void onClick (Widget source);
}
public interface OnLongClickEventListener
{
public void onLongClick (Widget source);
}
public void setOnClickEventListner(OnClickEventListener li){
mClickEventListener.add(li);
}
public void setOnLongClickEventListner(OnLongClickEventListener li){
mLongClickEventListener.add(li);
}
}
类按钮
public class Button extends Widget{
private String mButtonText;
public Button (){
}
public String getButtonText() {
return mButtonText;
}
public void setButtonText(String buttonText) {
this.mButtonText = buttonText;
}
}
类复选框
public class CheckBox extends Widget{
private boolean checked;
public CheckBox() {
checked = false;
}
public boolean isChecked(){
return (checked == true);
}
public void setCheck(boolean checked){
this.checked = checked;
}
}
Activity类
包com.som_itsolutions.training.java.exampleeventlistener;
public class Activity implements Widget.OnClickEventListener
{
public Button mButton;
public CheckBox mCheckBox;
private static Activity mActivityHandler;
public static Activity getActivityHandle(){
return mActivityHandler;
}
public Activity ()
{
mActivityHandler = this;
mButton = new Button();
mButton.setOnClickEventListner(this);
mCheckBox = new CheckBox();
mCheckBox.setOnClickEventListner(this);
}
public void onClick (Widget source)
{
if(source == mButton){
mButton.setButtonText("Thank you for clicking me...");
System.out.println(((Button) mButton).getButtonText());
}
if(source == mCheckBox){
if(mCheckBox.isChecked()==false){
mCheckBox.setCheck(true);
System.out.println("The checkbox is checked...");
}
else{
mCheckBox.setCheck(false);
System.out.println("The checkbox is not checked...");
}
}
}
public void doSomeWork(Widget source){
source.clickEvent();
}
}
其他类
public class OtherClass implements Widget.OnClickEventListener{
Button mButton;
public OtherClass(){
mButton = Activity.getActivityHandle().mButton;
mButton.setOnClickEventListner(this);//interested in the click event //of the button
}
@Override
public void onClick(Widget source) {
if(source == mButton){
System.out.println("Other Class has also received the event notification...");
}
}
主类
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Activity a = new Activity();
OtherClass o = new OtherClass();
a.doSomeWork(a.mButton);
a.doSomeWork(a.mCheckBox);
}
}
从上面的代码中可以看到,我们有一个名为events的接口,它基本上列出了应用程序可能发生的所有事件。Widget类是所有UI组件(如按钮、复选框)的基类。这些UI组件是实际从框架代码接收事件的对象。Widget类实现了Events接口,它也有两个嵌套接口,即OnClickEventListener和OnLongClickEventListener
These two interfaces are responsible for listening to events that may occur on the Widget derived UI components like Button or Checkbox. So if we compare this example with the earlier Callback example using Java Interface, these two interfaces work as the Callback interface. So the higher level code (Here Activity) implements these two interfaces. And whenever an event occurs to a widget, the higher level code (or the method of these interfaces implemented in the higher level code, which is here Activity) will be called.
Now let me discuss the basic difference between Callback and Eventlistener pattern. As we have mentioned that using Callback, the Callee can notify only a single Caller. But in the case of EventListener pattern, any other part or class of the Application can register for the events that may occur on the Button or Checkbox. The example of this kind of class is the OtherClass. If you see the code of the OtherClass, you will find that it has registered itself as a listener to the ClickEvent that may occur in the Button defined in the Activity. Interesting part is that, besides the Activity ( the Caller), this OtherClass will also be notified whenever the click event occurs on the Button.