如何用简单的英语解释回调?它们与从一个函数调用另一个函数从调用函数获取上下文有什么不同?如何向新手程序员解释它们的强大功能?


当前回答

简单明了:回调是你给另一个函数的函数,这样它就可以调用它。

通常在某个操作完成时调用它。由于在将回调函数交给其他函数之前创建了回调,因此可以使用调用站点的上下文信息初始化它。这就是为什么它被命名为call*back* -第一个函数从它被调用的地方回调到上下文。

其他回答

想象一下,一个朋友要离开你的家,你告诉她“到家后给我打个电话,好让我知道你已经安全到家了”;这是(字面上的)回电。这就是回调函数,与语言无关。您希望某个过程在完成某些任务后将控制传递回给您,因此您可以给它一个函数,用于回调。

例如,在Python中,

grabDBValue( (lambda x: passValueToGUIWindow(x) ))

grabDBValue可以被编写为只从数据库获取一个值,然后让您指定对该值实际做什么,因此它接受一个函数。您不知道grabDBValue何时或是否会返回,但是如果/当它返回时,您知道希望它做什么。在这里,我传入一个匿名函数(或lambda),它将值发送到GUI窗口。我可以通过这样做轻松地改变程序的行为:

grabDBValue( (lambda x: passToLogger(x) ))

回调在函数是第一类值的语言中工作得很好,就像通常的整数、字符串、布尔值等。在C语言中,你可以通过传递指向函数的指针来“传递”函数,调用者可以使用它;在Java中,调用者将请求具有特定方法名的特定类型的静态类,因为类之外没有函数(实际上是“方法”);在大多数其他动态语言中,您可以通过简单的语法传递函数。

Protip:

在具有词法作用域的语言(如Scheme或Perl)中,您可以使用这样的技巧:

my $var = 2;
my $val = someCallerBackFunction(sub callback { return $var * 3; });
# Perlistas note: I know the sub doesn't need a name, this is for illustration

在本例中$val将为6,因为回调可以访问定义它的词法环境中声明的变量。词法作用域和匿名回调是一个强大的组合,值得新手进一步研究。

在PHP中,它是这样的:

<?php

function string($string, $callback) {
     $results = array(
        'upper' => strtoupper($string),
        'lower' => strtolower($string),
        );

    if(is_callable($callback)) {
        call_user_func($callback, $results);
    }
}

string('Alex', function($name) {
    echo $name['lower'];
});

我要尽量让这个问题简单化。“回调”是由另一个以第一个函数为参数的函数调用的任何函数。很多时候,“回调”是在发生某些事情时调用的函数。在编程语言中,这可以被称为“事件”。

想象一下这样的场景:你几天后就会收到一个包裹。这个包裹是给邻居的礼物。因此,一旦你得到了包裹,你想把它带到邻居那里。你出城了,所以你给你的配偶留下了指示。

你可以让他们去拿包裹,然后送到邻居那里。如果你的配偶像电脑一样愚蠢,他们会坐在门口等包裹来(什么也不做),然后一旦它来了,他们就会把它带到邻居那里。但是有一个更好的办法。告诉你的配偶,一旦他们收到包裹,他们应该把它带到邻居那里。然后,他们可以正常生活,直到他们收到包裹。

在我们的例子中,包的接收是“事件”,而把它带给邻居是“回调”。你的配偶“执行”你的指示,只有当包裹到达时才把包裹带过来。更好的!

这种思维在日常生活中很明显,但计算机没有这种常识。考虑程序员通常如何写入文件:

fileObject = open(file)
# now that we have WAITED for the file to open, we can write to it
fileObject.write("We are writing to the file.")
# now we can continue doing the other, totally unrelated things our program does

在这里,我们等待文件打开,然后再写入它。这“阻塞”了执行流,我们的程序不能做它可能需要做的任何其他事情!如果我们可以这样做:

# we pass writeToFile (A CALLBACK FUNCTION!) to the open function
fileObject = open(file, writeToFile)
# execution continues flowing -- we don't wait for the file to be opened
# ONCE the file is opened we write to it, but while we wait WE CAN DO OTHER THINGS!

事实证明,我们可以通过一些语言和框架来实现这一点。太酷了!查看Node.js来获得一些这种思考的实际实践。

你觉得不舒服,所以去看医生。他检查了你的身体,认为你需要一些药物治疗。他开了一些药,并把处方打电话到你当地的药店。你回家吧。稍后,你的药房打电话告诉你,你的处方已经准备好了。你去捡吧。

让我们假设您要给我一个可能长期运行的任务:获取您遇到的前五个独特的人的名字。如果我在人口稀少的地区,这可能需要几天的时间。你真的不想在我忙来忙去的时候袖手旁观,所以你说:“你拿到名单后,打我手机给我读一遍。这是号码。”

您已经给了我一个回调引用——一个我应该执行以传递进一步处理的函数。

在JavaScript中,它可能是这样的:

var lottoNumbers = [];
var callback = function(theNames) {
  for (var i=0; i<theNames.length; i++) {
    lottoNumbers.push(theNames[i].length);
  }
};

db.executeQuery("SELECT name " +
                "FROM tblEveryOneInTheWholeWorld " +
                "ORDER BY proximity DESC " +
                "LIMIT 5", callback);

while (lottoNumbers.length < 5) {
  playGolf();
}
playLotto(lottoNumbers);

这可能有很多方面可以改进。例如,你可以提供第二次回拨:如果最终超过一个小时,打电话给红色电话,告诉接听电话的人你超时了。