协慌网

登录 贡献 社区

什么是回调函数?

什么是回调函数?

答案

由于该死的东西的名字,开发人员常常对回调是什么感到困惑。

回调函数是一个函数,它是:

  • 可通过另一个功能访问,并且
  • 如果第一个函数完成,则在第一个函数之后调用

设想回调函数如何工作的一种好方法是,它是传递给函数的 “后面” 调用的函数。

也许更好的名字应该是“call after”功能。

这种构造对于异步行为非常有用,在异步行为中,我们希望每当前一个事件完成时就进行一次活动。

伪代码:

// A function which accepts another function as an argument
// (and will automatically invoke that function when it completes - note that there is no explicit call to callbackFunction)
funct printANumber(int number, funct callbackFunction) {
    printout("The number you provided is: " + number);
}

// a function which we will use in a driver function as a callback function
funct printFinishMessage() {
    printout("I have finished printing numbers.");
}

// Driver method
funct event() {
   printANumber(6, printFinishMessage);
}

如果调用 event()的结果:

The number you provided is: 6
I have finished printing numbers.

这里的输出顺序很重要。由于回调函数是在以后调用的,因此 “我已经完成打印号码” 的打印是最后而不是首先打印。

所谓的回调是由于其与指针语言一起使用。如果您不使用其中之一,请不要使用 “回调” 这个名称。只需了解一下,它只是描述一个方法的名称,该方法作为另一个方法的参数提供,这样,当调用父方法(无论条件如何,例如单击按钮,计时器滴答声等)且其方法主体完成时,然后调用回调函数。

某些语言支持以下结构,其中支持多个回调函数参数,并根据父函数的完成方式进行调用(即,在父函数成功完成的情况下调用一个回调,在父函数抛出具体错误等)。

不透明的定义

回调函数是您提供给另一段代码的功能,允许该代码调用该函数。

人为的例子

你为什么想做这个?假设有一个您需要调用的服务。如果服务立即返回,则您只需:

  1. 叫它
  2. 等待结果
  3. 结果输入后继续

例如,假设服务是factorial功能。当您想要5! ,您将调用factorial(5) ,并且将发生以下步骤:

  1. 您当前的执行位置已保存(在堆栈上,但这并不重要)

  2. 执行移交给factorial

  3. factorial完成时,它将结果放在您可以到达的位置

  4. 执行返回到 [1] 中的位置

现在,假设factorial花费了很长时间,因为您要给它提供大量数据,并且它需要在某些超级计算集群中运行。假设您预计需要 5 分钟才能返回结果。你可以:

  1. 保持设计并在晚上入睡时运行程序,这样就不会有一半时间盯着屏幕

  2. 设计您的程序以执行其他事情,而factorial正在执行它的事情

如果选择第二个选项,则回调可能对您有用。

端到端设计

为了利用回调模式,您想要的是能够通过以下方式factorial

factorial(really_big_number, what_to_do_with_the_result)

第二个参数what_to_do_with_the_result是您发送给factorial的函数,希望factorial在返回结果之前对其进行调用。

是的,这意味着factorial以支持回调。

现在假设您希望能够将参数传递给回调。现在您不能了,因为您不会再称呼它,所以factorial就是。因此factorial以允许您传入参数,并且它将在调用它时将它们交给您的回调。它可能看起来像这样:

factorial (number, callback, params)
{
    result = number!   // i can make up operators in my pseudocode
    callback (result, params)
}

现在, factorial允许这种模式,您的回调可能看起来像这样:

logIt (number, logger)
{
    logger.log(number)
}

而您打电话给factorial将是

factorial(42, logIt, logger)

如果您想从logIt返回值怎么办?好吧,你不能,因为factorial没有注意它。

好吧,为什么factorial不能只返回回调返回的内容呢?

使其无阻塞

由于执行是要在factorial完成时移交给回调,因此它实际上不应将任何内容返回给其调用方。理想情况下,它将以某种方式在另一个线程 / 进程 / 机器中启动其工作并立即返回,以便您可以继续执行,也许是这样的:

factorial(param_1, param_2, ...)
{
    new factorial_worker_task(param_1, param_2, ...);
    return;
}

现在,这是一个 “异步调用”,这意味着当您调用它时,它会立即返回,但尚未真正完成其工作。因此,您确实需要机制来对其进行检查,并在完成时获得其结果,并且您的程序在此过程中变得越来越复杂。

顺便说一句,使用这种模式, factorial_worker_task可以异步启动回调并立即返回。

所以你会怎么做?

答案是保持在回调模式之内。每当你想写

a = f()
g(a)

并且f被异步调用,您将改为

f(g)

其中g作为回调传递。

这从根本上改变了程序的流拓扑,并需要一些习惯。

您的编程语言可以为您提供即时创建函数的方式,对您有很大帮助。在紧接上面的代码中,函数g可能和print (2*a+1)一样小。如果您的语言要求您使用完全不必要的名称和签名将其定义为一个单独的函数,那么如果您频繁使用此模式,您的生活将变得不愉快。

另一方面,如果您的语言允许您创建 lambda,那么您的状态会好得多。然后,您将最终写出类似

f( func(a) { print(2*a+1); })

真是太好了。

如何传递回调

您如何将回调函数传递给factorial ?好吧,您可以通过多种方式来做到这一点。

  1. 如果被调用的函数在同一进程中运行,则可以传递一个函数指针

  2. 或者,也许您想在程序中维护fn name --> fn ptr ,在这种情况下,您可以传递名称

  3. 也许您的语言允许您就地定义函数,可能是 lambda!在内部,它正在创建某种对象并传递一个指针,但是您不必为此担心。

  4. 也许您正在调用的功能正在完全独立的机器上运行,并且您正在使用诸如 HTTP 之类的网络协议进行调用。您可以将回调作为 HTTP 可调用函数公开,并传递其 URL。

你明白了。

近期回调的兴起

在我们进入的这个网络时代,我们调用的服务通常是通过网络进行的。我们通常对这些服务没有任何控制权,即我们没有编写它们,不维护它们,无法确保它们正常运行或性能如何。

但是我们不能期望我们的程序在等待这些服务响应时会阻塞。意识到这一点,服务提供商经常使用回调模式来设计 API。

JavaScript 非常好地支持回调,例如使用 lambda 和闭包。在 JavaScript 世界中,浏览器和服务器上都有很多活动。甚至还有针对移动设备开发的 JavaScript 平台。

随着我们的前进,越来越多的人将在编写异步代码,对此的理解将是至关重要的。

维基百科上的 “回调”页面对此进行了很好的解释:

在计算机编程中,回调是对可执行代码或一段可执行代码的引用,该代码作为参数传递给其他代码。这允许较低层的软件层调用较高层中定义的子例程(或函数)。