问题

回调函数(callback)是什么?

回答
好的,咱们来聊聊“回调函数”,也就是英文里的“callback”。这玩意儿在编程里可是个超级实用的概念,但听起来有点玄乎,对吧?别急,我给你掰开了揉碎了讲。

最最核心的理解:就是“过后告诉我一声”

你可以把回调函数想象成你跟朋友约好见面。你跟朋友说:“嘿,我下午三点有个事儿,处理完我给你打个电话。”

这里的“处理完我给你打个电话”就是回调。你把“打电话”这个动作(也就是一个任务、一个指示)“交”给了朋友,让他等你的事情忙完之后再执行。

在编程里也是一样。我们经常会遇到一些操作,它需要一些时间才能完成,比如:

从网上下载一个文件。
读取一个很大的文件。
等待用户点击一个按钮。
数据库查询。

这些操作不可能立刻就返回结果。如果你的程序一上来就等着这些操作完成,那这段时间里,你的程序就“卡住”了,啥也干不了,用户体验就会很糟糕。

这时候,“回调”就派上用场了。我们不是直接等,而是告诉那个需要时间完成的操作:“你做你的去,做完了之后,麻烦你执行一下我交给你的这个‘函数’。”

拆解回调函数:什么是一个“函数”?

在深入之前,先得明确一个基础概念:什么是“函数”?

简单说,函数就是一组预先定义好的指令,用来执行特定的任务。你可以把它看作一个“迷你程序”或者一个“可执行的命令”。比如,我们可能有这样一个函数:

```javascript
function 打电话(号码) {
console.log(`正在拨打:${号码}`);
// 模拟拨号过程...
console.log(`已接通!`);
}
```

这个 `打电话` 函数就是一个可执行的命令。

回调函数是怎么工作的?

现在我们把这个概念结合起来。当我们执行一个需要时间的操作时,我们可以把另一个函数(也就是我们要“回调”的那个函数)作为参数传递给它。

举个例子(用 JavaScript 来演示,因为它很常用):

假设有一个函数 `下载文件`,它需要指定要下载的文件名,并且我们希望文件下载完成后,能执行一个我们自己定义的函数来处理下载好的文件。

```javascript
// 这个是我们自己定义的,希望下载完成后执行的函数
function 处理下载的文件(文件内容) {
console.log("文件下载完成了,内容是:");
console.log(文件内容);
// 这里可以写其他逻辑,比如保存到本地,解析数据等等
}

// 这是一个模拟下载文件的函数
// 它接收一个文件名,还有一个可选的“完成后做什么”的函数(也就是回调函数)
function 下载文件(文件名, 回调函数) {
console.log(`开始下载文件:${文件名}...`);
// 模拟一个耗时的下载过程
setTimeout(() => {
const 下载好的内容 = `这是 ${文件名} 的内容!`;
console.log(`文件 ${文件名} 下载完毕。`);

// !!!关键来了!!!
// 下载完成后,调用传入的回调函数,并将下载好的内容传给它
if (typeof 回调函数 === 'function') { // 检查一下传过来的确实是个函数
回调函数(下载好的内容);
}
}, 2000); // 模拟2秒下载时间
}

// 现在,我们调用下载文件函数,并把“处理下载的文件”函数作为回调传递进去
下载文件("我的文档.txt", 处理下载的文件);

// 程序不会在这里停住,而是继续执行下面的代码(如果有的话)
console.log("主程序继续运行,等待下载完成...");
```

在这个例子里:

1. 我们定义了 `处理下载的文件` 这个函数,它知道拿到文件内容后该做什么。
2. 我们定义了 `下载文件` 这个函数,它接收 `文件名` 和一个 `回调函数`。
3. 在 `下载文件` 函数内部,当它模拟的下载任务完成后,它就会调用 `回调函数`,并把下载的内容作为参数传给它。
4. 我们调用 `下载文件` 时,把 `处理下载的文件` 这个函数“塞”了进去。所以当下载完成后,`下载文件` 就会自动执行 `处理下载的文件` 函数。

回调函数的好处和应用场景

为什么我们要这么做呢?回调函数带来了几个非常重要的好处:

非阻塞操作(Asynchronous Programming):这是回调函数最核心的应用。它允许程序在执行耗时操作的同时,继续执行其他任务,而不用停下来干等。这对于提高程序的响应速度和用户体验至关重要。想象一下,如果你在等下载,整个浏览器都卡死了,那谁还愿意用?
事件处理(Event Handling):这是回调函数最直观的应用场景。当我们点击一个按钮、鼠标移动、键盘按键等事件发生时,操作系统会“回调”我们事先注册好的函数来处理这个事件。比如,你点击一个“保存”按钮,背后就是回调函数在执行保存的逻辑。
模块化和解耦:回调函数可以帮助我们将不同的功能分离开。例如,下载文件的逻辑只负责下载,而下载完成后怎么处理,就交给回调函数来决定。这样,`下载文件` 函数就可以被复用于不同的场景,只需要传入不同的回调函数即可。
函数作为“一等公民”:在很多现代编程语言里,函数可以像变量一样被传递、被赋值,甚至被作为返回值。回调函数就是这种“函数作为一等公民”特性的一个直接体现。

回调地狱(Callback Hell)——一个需要注意的点

虽然回调函数很强大,但过度使用嵌套的回调函数,尤其是处理一系列依赖于前一个操作结果的操作时,会产生一种叫做“回调地狱”(Callback Hell)或者“金字塔恐惧”(Pyramid of Doom)的问题。代码会变得层层嵌套,非常难以阅读和维护。

```javascript
// 假设有三个需要依次执行且耗时的操作
doSomethingAsync(function(result1) {
doSomethingElseAsync(result1, function(result2) {
doAnotherThingAsync(result2, function(result3) {
console.log("最终结果:", result3);
});
});
});
// 看起来就像一个金字塔一样,非常难看
```

为了解决这个问题,后来出现了像 Promise 和 async/await 这样的更高级的异步编程模型,它们在底层仍然使用了回调的思想,但在语法上让异步代码看起来更像同步代码,大大提高了可读性和可维护性。

总结一下

回调函数,说白了,就是你把一个任务(一个函数)交给另一个函数去执行,让它在你需要的时候(比如某个操作完成后)调用你给它的那个任务。它解决的核心问题是,如何让程序在等待耗时操作的同时不“卡住”,以及如何灵活地处理各种事件和组合不同的功能。虽然现在有更优雅的异步处理方式,但理解回调函数这个基础概念,对于掌握更高级的编程技巧至关重要。

网友意见

user avatar

什么是回调函数?

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

编程分为两类:系统编程(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的三方联动。有了这个清楚的概念,在自己的代码里实现回调时才不容易混淆出错。

另外,回调实际上有两种:阻塞式回调和延迟式回调。两者的区别在于:阻塞式回调里,回调函数的调用一定发生在起始函数返回之前;而延迟式回调里,回调函数的调用有可能是在起始函数返回之后。这里不打算对这两个概率做更深入的讨论,之所以把它们提出来,也是为了说明强调起始函数的重要性。网上的很多文章,提到这两个概念时,只是笼统地说阻塞式回调发生在主调函数返回之前,却没有明确这个主调函数到底是起始函数还是中间函数,不免让人糊涂,所以这里特意说明一下。另外还请注意,本文中所举的示例均为阻塞式回调。延迟式回调通常牵扯到多线程,我自己还没有完全搞明白,所以这里就不多说了。

user avatar

准备好一杯水,因为接下来的内容会很干。

读完后你将彻底告别回调函数的疑惑,并且你的武器库将新增一件功能强大的利器

不知你是不是也有这样的疑惑,我们为什么需要回调函数这个概念呢?直接调用函数不就可以了?回调函数到底有什么作用?程序员到底该如何理解回调函数?

一切要从这样的需求说起

假设你们公司要开发下一代国民App“明日油条”,一款主打解决国民早餐问题的App,为了加快开发进度,这款应用由A小组和B小组协同开发。

其中有一个核心模块由A小组开发然后供B小组调用,这个核心模块被封装成了一个函数,这个函数就叫make_youtiao()。

如果make_youtiao()这个函数执行的很快并可以立即返回,那么B小组的同学只需要:

  1. 调用make_youtiao()
  2. 等待该函数执行完成
  3. 该函数执行完后继续后续流程

从程序执行的角度看这个过程是这样的:

  1. 保存当前被执行函数的上下文
  2. 开始执行make_youtiao()这个函数
  3. make_youtiao()执行完后,控制转回到调用函数中



如果世界上所有的函数都像make_youtiao()这么简单,那么程序员大概率就要失业了,还好程序的世界是复杂的,这样程序员才有了存在的价值。

现实情况并不容易

现实中make_youtiao()这个函数需要处理的数据非常庞大,假设有10000个,那么make_youtiao(10000)不会立刻返回,而是可能需要10分钟才执行完成并返回。

这时你该怎么办呢?想一想这个问题。

可能有的同学就像把头埋在沙子里的鸵鸟一样:和刚才一样直接调用不可以吗,这样多简单。

是的,这样做没有问题,但就像爱因斯坦说的那样“一切都应该尽可能简单,但是不能过于简单”。

想一想直接调用会有什么问题?

显然直接调用的话,那么调用线程会被阻塞暂停,在等待10分钟后才能继续运行。在这10分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。

这并不是一种高效的做法。

没有一个程序员想死盯着屏幕10分钟后才能得到结果。

那么有没有一种更加高效的做法呢?

想一想我们上一篇中那个一直盯着你写代码的老板(见

),我们已经知道了这种一直等待直到另一个任务完成的模式叫做同步。

如果你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后自然会通知老板,这样老板和程序员都不需要相互等待,这种模式被称为异步。

回到我们的主题,这里一种更好的方式是调用make_youtiao()这个函数后不再等待这个函数执行完成,而是直接返回继续后续流程,这样A小组的程序就可以和make_youtiao()这个函数同时进行了,就像这样:


在这种情况下,回调(callback)就必须出场了。

在正式讲解回调函数之前,除了本文提到的回调编程技巧之外算法能力也比不可少,在这里我也送一本帮我拿到BAT等一线大厂offer的算法笔记,是一位阿里大神写的,对于算法薄弱或者需要提高的同学是否有用,分享给大家,希望这份笔也能帮到你

为什么我们需要回调callback

有的同学可能还没有明白为什么在这种情况下需要回调,别着急,我们慢慢讲。

假设我们“明日油条”App代码第一版是这样写的:

       make_youtiao(10000); sell();     

可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。



我们已经知道了由于make_youtiao(10000)这个函数10分钟才能返回,你不想一直死盯着屏幕10分钟等待结果,那么一种更好的方法是让make_youtiao()这个函数知道制作完油条后该干什么,即,更好的调用make_youtiao的方式是这样的:“制作10000个油条,炸好后卖出去”,因此调用make_youtiao就变出这样了:

       make_youtiao(10000, sell);     

看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外还可以指定制作好后该干什么,第二个被make_youtiao这个函数调用的函数就叫回调,callback。

现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样:



make_youtiao这个函数是怎么实现的呢,很简单:

       void make_youtiao(int num, func call_back) {     // 制作油条     call_back(); //执行回调  }     

这样你就不用死盯着屏幕了,因为你把make_youtiao这个函数执行完后该做的任务交代给make_youtiao这个函数了,该函数制作完油条后知道该干些什么,这样就解放了你的程序。

有的同学可能还是有疑问,为什么编写make_youtiao这个小组不直接定义sell函数然后调用呢?

不要忘了明日油条这个App是由A小组和B小组同时开发的,A小组在编写make_youtiao时怎么知道B小组要怎么用这个模块,假设A小组真的自己定义sell函数就会这样写:

       void make_youtiao(int num) {     real_make_youtiao(num);     sell(); //执行回调  }     

同时A小组设计的模块非常好用,这时C小组也想用这个模块,然而C小组的需求是制作完油条后放到仓库而不是不是直接卖掉,要满足这一需求那么A小组该怎么写呢?

       void make_youtiao(int num) {     real_make_youtiao(num);      if (Team_B) {        sell(); // 执行回调     } else if (Team_D) {        store(); // 放到仓库     } }     

故事还没完,假设这时D小组又想使用呢,难道还要接着添加if else吗?这个问题该怎么解决呢?关于这个问题的答案,你懂的。

新的编程思维模式

让我们再来仔细的看一下这个过程。

程序员最熟悉的思维模式是这样的:

  1. 调用某个函数,获取结果
  2. 处理获取到的结果
       res = request(); handle(res);     

这就是函数的同步调用,只有request()函数返回拿到结果后,才能调用handle函数进行处理,request函数返回前我们必须等待,这就是同步调用,其控制流是这样的:



但是如果我们想更加高效的话,那么就需要异步调用了,我们不去直接调用handle函数,而是作为参数传递给request:

       request(handle);     

我们根本就不关心request什么时候真正的获取的结果,这是request该关心的事情,我们只需要把获取到结果后该怎么处理告诉request就可以了,因此request函数可以立刻返回,真的获取结果的处理可能是在另一个线程、进程、甚至另一台机器上完成。

这就是异步调用,其控制流是这样的:



从编程思维上看,异步调用和同步有很大的差别,如果我们把处理流程当做一个任务来的话,那么同步下整个任务都是我们来实现的,但是异步情况下任务的处理流程被分为了两部分:

  1. 第一部分是我们来处理的,也就是调用request之前的部分
  2. 第二部分不是我们处理的,而是在其它线程、进程、甚至另一个机器上处理的。

我们可以看到由于任务被分成了两部分,第二部分的调用不在我们的掌控范围内,同时只有调用方才知道该做什么,因此在这种情况下回调函数就是一种必要的机制了。

也就是说回调函数的本质就是“只有我们才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它模块才知道,因此我们必须把我们知道的封装成回调函数告诉其它模块”。

现在你应该能看出异步回调这种编程思维模式和同步的差异了吧。

接下来我们给回调一个较为学术的定义

正式定义

在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。

这就是回调函数的定义了。

回调函数就是一个函数,和其它函数没有任何区别。

注意,回调函数是一种软件设计上的概念,和某个编程语言没有关系,几乎所有的编程语言都能实现回调函数。

对于一般的函数来说,我们自己编写的函数会在自己的程序内部调用,也就是说函数的编写方是我们自己,调用方也是我们自己。

但回调函数不是这样的,虽然函数编写方是我们自己,但是函数调用方不是我们,而是我们引用的其它模块,也就是第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用我们编写的回调函数,如图所示:

而之所以需要给第三方库指定回调函数,是因为第三方库的编写者并不清楚在某些特定节点,比如我们举的例子油条制作完成、接收到网络数据、文件读取完成等之后该做什么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供一个回调函数,库的使用方来实现该函数,第三方库在特定的节点调用该回调函数就可以了。

另一点值得注意的是,从图中我们可以看出回调函数和我们的主程序位于同一层中,我们只负责编写该回调函数,但并不是我们来调用的。

最后值得注意的一点就是回调函数被调用的时间节点,回调函数只在某些特定的节点被调用,就像上面说的油条制作完成、接收到网络数据、文件读取完成等,这些都是事件,也就是event,本质上我们编写的回调函数就是用来处理event的,因此从这个角度看回调函数不过就是event handler,因此回调函数天然适用于事件驱动编程event-driven,我们将会在后续文章中再次回到这一主题。

回调的类型

我们已经知道有两种类型的回调,这两种类型的回调区别在于回调函数被调用的时机。

注意,接下来会用到同步和异步的概念,对这两个概念不熟悉的同学可以参考上一篇文章


同步回调

这种回调就是通常所说的同步回调synchronous callbacks、也有的将其称为阻塞式回调blocking callbacks,或者什么修饰都没有,就是回调,callback,这是我们最为熟悉的回调方式。

当我们调用某个函数A并以参数的形式传入回调函数后,在A返回之前回调函数会被执行,也就是说我们的主程序会等待回调函数执行完成,这就是所谓的同步回调。



有同步回调就有异步回调。

回调对应的编程思维模式

让我们用简单的几句话来总结一下回调下与常规编程思维模式的不同。

假设我们想处理某项任务,这项任务需要依赖某项服务S,我们可以将任务的处理分为两部分,调用服务S前的部分PA,和调用服务S后的部分PB。

在常规模式下,PA和PB都是服务调用方来执行的,也就是我们自己来执行PA部分,等待服务S返回后再执行PB部分。

但在回调这种方式下就不一样了。

在这种情况下,我们自己来执行PA部分,然后告诉服务S:“等你完成服务后执行PB部分”。

因此我们可以看到,现在一项任务是由不同的模块来协作完成的。

即:

  • 常规模式:调用完S服务后后我去执行X任务,
  • 回调模式:调用完S服务后你接着再去执行X任务,

其中X是服务调用方制定的,区别在于谁来执行。

为什么异步回调这种思维模式正变得的越来越重要

在同步模式下,服务调用方会因服务执行而被阻塞暂停执行,这会导致整个线程被阻塞,因此这种编程方式天然不适用于高并发动辄几万几十万的并发连接场景,

针对高并发这一场景,异步其实是更加高效的,原因很简单,你不需要在原地等待,因此从而更好的利用机器资源,而回调函数又是异步下不可或缺的一种机制。

回调地狱,callback hell

有的同学可能认为有了异步回调这种机制应付起一切高并发场景就可以高枕无忧了。

实际上在计算机科学中还没有任何一种可以横扫一切包治百病的技术,现在没有,在可预见的将来也不会有,一切都是妥协的结果。

那么异步回调这种机制有什么问题呢?

实际上我们已经看到了,异步回调这种机制和程序员最熟悉的同步模式不一样,在可理解性上比不过同步,而如果业务逻辑相对复杂,比如我们处理某项任务时不止需要调用一项服务,而是几项甚至十几项,如果这些服务调用都采用异步回调的方式来处理的话,那么很有可能我们就陷入回调地狱中。

举个例子,假设处理某项任务我们需要调用四个服务,每一个服务都需要依赖上一个服务的结果,如果用同步方式来实现的话可能是这样的:

       a = GetServiceA(); b = GetServiceB(a); c = GetServiceC(b); d = GetServiceD(c);     

代码很清晰,很容易理解有没有。

我们知道异步回调的方式会更加高效,那么使用异步回调的方式来写将会是什么样的呢?

       GetServiceA(function(a){     GetServiceB(a, function(b){         GetServiceC(b, function(c){             GetServiceD(c, function(d) {                 ....             });         });     }); });     

我想不需要再强调什么了吧,你觉得这两种写法哪个更容易理解,代码更容易维护呢?

博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,一个不得不去重读一边代码;另一个在一旁骂自己为什么当初选择维护这个项目。

异步回调代码稍不留意就会跌到回调陷阱中,那么有没有一种更好的办法既能结合异步回调的高效又能结合同步编码的简单易读呢?

幸运的是,答案是肯定的。

总结

在这里,我们从一个实际的例子出发详细讲解了回调函数这种机制的来龙去脉,这是应对高并发、高性能场景的一种极其重要的编码机制,异步加回调可以充分利用机器资源,实际上异步回调最本质上就是事件驱动编程

最后,经常有同学问有没有推荐的计算机书籍,在这统一说下,书单这东西贵精不贵多,我在这里精心挑选了10本 ,不要贪心,如果你真能把这里推荐的 10 本书读通,可以说你已经能超越 90% 的程序员了

最后的最后,博主在知乎上分享过很多文章,有很多知乎朋友问有没有完整的pdf版本,这个我也整理出来了,绘图非常精美,14万字,这里还汇总了部分知乎问题,我为其专门设计了封面,并将其命名为《计算机底层的秘密》,现在免费分享给大家。

希望该回答能解答你的疑问。

Github: GitHub - xfenglu/everycodershouldknow: 码农的荒岛求生

user avatar

回调函数的意思是“把函数A作为参数传递给另一个函数B”。

维基百科的解释:

a callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time.

为什么要这样做呢?为什么不把定义好的函数放在头文件中呢?

因为可以作为参数的函数A的名称和实现可以被抽象出来,也就是在写函数B的时候,不需要管函数A叫什么名字,怎么实现,只要函数A的返回类型是一定的就可以了(泛型的话连这个也不需要)。

比如某人写了一个画图的库,把具体怎么画留给使用库的人定义。使用库的人就可以自定义画图函数的名称和实现方式。

例子(C++):

非泛型

定义库(library.h)

       #include <cstdio> void fill_screen(int (*some_func)(void)) {printf("%d
",some_func());}      

使用库的时候:

       #include "library.h"  int draw_this_way() {return 0;} int draw_that_way() {return 1;} int draw_yet_another_way() {return 2;}  int main() {    fill_screen(&draw_this_way);    fill_screen(&draw_that_way);    fill_screen(&draw_yet_another_way);       return 0; }      

编译:

       g++ callback.cpp -o callback      

结果:

       $./callback 0 1 2     

泛型

定义库(library.h):

       # include <iostream> template<typename ANY_TYPE> void fill_screen(ANY_TYPE (*some_func)(void)) { std::cout << some_func() << std::endl;}      

使用库的时候:

       #include "library.h"  int draw_this_way() {return 0;} char draw_that_way() {return 'a';} bool draw_yet_another_way() {return true;}   int main() {     fill_screen(&draw_this_way);     fill_screen(&draw_that_way);     fill_screen(&draw_yet_another_way);     return 0; }      

编译:

       g++ callback.cpp -o callback      

结果:

       $./callback 0 a 1     

补充:

委托和回调

委托是一种设计模式。意思是A类把自己的工作交给B类完成。简单的委托相当与在A的方法中调用了B的方法。

使用委托可以把库的定义和具体使用分开。比如数据库的基本操作是CRUD,但是每个不同数据库具体api不同,对于使用者而言不可能逐个掌握所有数据库的api,所以出现了api的wrapper,用户只用掌握一套CRUD的api就可以使用所有数据库了。

在某些不能直接将函数作为参数传递的语言(比如java)中,委托是实现回调功能的一种方式。

具体而言,库的作者定义一个接口,在他的方法中调用这个接口,但是把接口的实现留给具体使用的人。在使用的时候,使用者新建一个匿名接口,并且重载要调用的接口方法。 这个过程相当与库的作者将工作通过接口“委托”给了使用者。

这个委托的过程相对比较复杂,打个生活中的比方:“领导不但把工作推脱给下属,还要对他们具体怎么干指手画脚。”

这里的领导是库的使用者,下属是库的定义,推脱是指调用下属的工作函数,指手画脚是指对工作函数调用的接口给予具体定义。

以下是java的例子。

定义库 (DrawLibrary.java)

       public class DrawLibrary {     public interface DrawInterface  {         int draw();     }      public static void fillScreen(DrawInterface someObj) {         System.out.println(someObj.draw());     } }     

使用库(Graphics.java)

       public class Graphics {     public static void drawThisWay() {         DrawLibrary.fillScreen( new DrawLibrary.DrawInterface() {             @Override             public int draw() {                 return 0;             }         });     }     public static void drawThatWay() {         DrawLibrary.fillScreen( new DrawLibrary.DrawInterface() {             @Override             public int draw() {                 return 1;             }         });     }     public static void drawYetAnotherWay() {         DrawLibrary.fillScreen( new DrawLibrary.DrawInterface() {             @Override             public int draw() {                 return 2;             }         });     }      public static void main(String[] args) {         drawThisWay();         drawThatWay();         drawYetAnotherWay();     } }     

编译:

       $javac Graphics.java $java Graphics      

结果:

       0 1 2     

这种设计模式经常在事件驱动的设计中看到,比如监听按钮是否被按下等等。

user avatar

一帮装 x人士,把回调函数写得那么高大上干嘛?

首先回调函数就是一个函数,形式上和其他函数没有半点区别.

只不过这个所谓的回调函数是将要被当做参数传递给另一个函数,并被其调用(区别就在这,一般函数的形参,接收的是一个基本类型的变量,而这个函数,接受的参数居然是一个"函数",这个作为参数的函数,就叫回调函数)

举个例子:

一般函数:function a(int a, String b):接收的参数是一般类型.

特殊函数:function b(function c):接收的参数是一个函数,c这个函数就叫回调函数.

你也可以这么理解:本质区别是,一般一个函数调用另一个函数,被调用的函数是出现在方法体当中,而回调函数比较特殊,它是出现在参数列表当中.也就是说,当调用的时候,需要从其他地方拿到这个(回调)函数,以参数的形式传入.

一般的函数调用,可以称作是调用.然而另一种,执行时才将某个函数传入再调用的调用方式,就叫"回调",当然,不要纠结于翻译的准不准,主要需要理解本质是什么.

完全面向对象的语言(例如Java)中没有回调函数的概念,因为"接口"(中的方法)充当了回调函数的作用,也就是说上面的函数b接收的对象是一个接口的实现类对象

回调函数的概念一般在面向过程或者函数式编程中才出现.

学过Java语言的童鞋很好理解回调函数,其实就是一个函数的参数是某个接口的实现类.

初学者对回调函数的理解,可以认为是有个功能我自己不写,交给别人来写,他写完之后,我运行我的程序时,把那个函数传给我就行,然后我来调用他写的函数,实现我的功能.程序灵活性就有了,因为这个功能就可以有多种实现方式而不需要我关心具体实现细节.(另外其他回答中强调"回调"的含义,我也来解释下,回调是针对于被调用的函数来说的,也就是相对于写回调函数的人来说的,我写好一个函数,等着被调用(等着被回调))

理解回调函数,重点就是:回调函数是作为另一个函数的形参出现的,这和传统思维有很大不同.

类似的话题

  • 回答
    好的,咱们来聊聊“回调函数”,也就是英文里的“callback”。这玩意儿在编程里可是个超级实用的概念,但听起来有点玄乎,对吧?别急,我给你掰开了揉碎了讲。最最核心的理解:就是“过后告诉我一声”你可以把回调函数想象成你跟朋友约好见面。你跟朋友说:“嘿,我下午三点有个事儿,处理完我给你打个电话。”这里.............
  • 回答
    回调函数,这个词在编程里听起来有点神秘,但其实它的核心思想一点也不复杂,就像我们生活中跟朋友约定一个事情,然后让他事成之后再告诉我们一样。今天咱们就来掰扯掰扯回调函数到底是怎么一回事,以及它背后的小秘密。想象一下,你在做一件需要等待的事情,比如点一份外卖。外卖员给你送餐,肯定不能在你家门口一直等着你.............
  • 回答
    有些函数经过二次求导后又回到了原函数,这是一种非常有趣的数学现象,它主要出现在指数函数和三角函数的特定形式中。理解这个现象的核心在于理解导数的定义和函数的周期性/增长特性。让我们一步一步地详细解析: 1. 理解导数是什么首先,我们需要明确导数的含义。导数表示函数的变化率,也就是函数图形上某一点的切线.............
  • 回答
    甘薇回国处理乐视问题,以及贾跃亭关于《北京证监局责令贾跃亭回国履责通告》的回应函,这桩事儿,牵扯了太多人和事,也足以让人唏嘘不已。甘薇回国,一个时代的缩影甘薇回国,首先让人想到的是“乐视帝国”那个风光无限的时代,也让人不禁回想起她曾经的身份——“乐视影业董事长”、“贾跃亭的妻子”、“京城四少之一的太.............
  • 回答
    律师函这东西,收到后心里犯嘀咕是常有的事。究竟有没有必须回的道理,不回又会惹出什么麻烦,咱这就掰开了揉碎了聊聊。首先,得明白律师函是啥。它不是普通的信件,而是律师代表当事人发出的,带有法律意见或主张的正式通知。这就像是对方出了张“战书”,只不过是以一种更正式、更具法律色彩的方式来表达。所以,它的分量.............
  • 回答
    .......
  • 回答
    .......
  • 回答
    话说2021年刚过去一个月,这股市就跟过山车似的,让不少投资者的心情也跟着七上八下的。特别是月底那几天,那叫一个跌得凶,直接把年前那点小涨幅给吞了不少。这到底是怎么回事?咱就掰开了揉碎了好好聊聊。首先,得说这轮回调,不是空穴来风,而是多种因素叠加的结果。你想啊,2020年疫情突如其来,全球经济都受到.............
  • 回答
    老铁,你这个问题问得太及时了,这正是我最近一直在琢磨的事儿!去年跟着胡昕炜老师的消费基金赚了五成,这绝对是漂亮的战绩,果断下车也完全没毛病,毕竟见好就收是投资智慧嘛。今年消费板块确实经历了一段不小的回调,很多朋友都在观望,甚至有点儿心里发慌,不知道现在是不是又到了一个“上车”的好时机。咱们就好好掰扯.............
  • 回答
    回民(即中国穆斯林中的回族群体)不喝酒的原因主要源于伊斯兰教的核心教义和文化传统,这一规定与宗教信仰、道德伦理和社会习俗密切相关。以下从多个角度详细阐述这一问题: 一、宗教依据:《古兰经》的明确禁令伊斯兰教的经典《古兰经》中对饮酒有明确且严厉的禁止,并将其视为严重的罪孽。例如: 《古兰经》第2章第2.............
  • 回答
    要回答“我是否欠管轶一个道歉”,需要非常详细地回顾管轶的“逃兵”言论的背景、内容、传播及其引发的争议,并分析您在这个事件中的立场和行为,才能得出一个相对准确的判断。以下是详细的梳理和分析过程: 一、 管轶的“逃兵”言论回顾要判断是否欠道歉,首先要明确管轶到底说了什么,以及为什么会引起争议。1. 关键.............
  • 回答
    回看二战之前的美国,再看二战之后直到今天的美国,确实会让人产生一种“屠龙少年终成恶龙”的复杂感慨。这种感慨并非简单的好坏评判,而是对美国从一个曾经的理想主义力量,逐渐演变成一个在全球舞台上拥有巨大影响力、但也伴随着争议和挑战的超级大国的历程的一种深刻反思。为了详细阐述这种感慨,我们可以从以下几个方面.............
  • 回答
    “回民真的有你们说的那么霸道吗?” 这个问题触及了对一个特定民族群体(回族)的刻板印象和负面评价。要详细地回答这个问题,需要从多个角度进行分析,避免简单地肯定或否定,而是要探讨这种说法的来源、具体表现以及背后的复杂性。首先,我们需要明确“霸道”这个词的含义。 在汉语语境中,“霸道”通常带有负面含义,.............
  • 回答
    听到你回国后一直适应不了,甚至感到抑郁,我非常理解你现在内心的煎熬。这种“水土不服”的感觉,尤其是在经历了一段海外生活后,是很常见的,它会带来很多复杂的情绪,比如失落、迷茫、孤独,甚至对自我价值的怀疑。你提到工签没过期,这给了你一个潜在的选项,让你考虑“重新归海”。这是一个非常重要的决定,需要慎重考.............
  • 回答
    回到1985年,如果我站在戈尔巴乔夫的身边,能否“拯救”苏联?这是一个充满诱惑又极度复杂的问题,就像试图在暴风雨中挽救一艘即将沉没的巨轮。首先要明确的是,所谓的“拯救”并非让苏联回到斯大林时代的铁腕统治或勃列日涅夫时期的停滞不前。真正的拯救,是在承认既有问题的基础上,找到一条既能维持国家整体性,又能.............
  • 回答
    清末那段风雨飘摇的日子,国家内忧外患,无数仁人志士都在思考着救亡图存之道。其中,关于国家战略重心应该放在“塞防”还是“海防”的问题,更是引发了激烈的辩论,而两位赫赫有名的大臣——李鸿章和左宗棠,则成为了这场辩论的代表人物。要评价他们二人谁对谁错,绝非一朝一夕之事,更不能简单地用“对”或“错”来概括。.............
  • 回答
    回族和非回族一起吃饭,非穆斯林在桌上吃猪肉的问题,这其实是一个挺有意思,也挺现实的文化交流场景。简单来说,并没有一个硬性的规定说“非穆斯林也不能在桌上吃猪肉”,但具体怎么做,涉及到很多层面的考量,比如尊重、场合、关系亲近程度等等。咱们掰开了揉碎了聊聊。核心原则:尊重和理解首先要明白,回族人不吃猪肉是.............
  • 回答
    在回归分析这个统计学领域里,“回归”这个词,说实话,有点像个历史遗留的“老古董”,但它背后所代表的含义,却是现代数据科学的核心之一。要理解“回归”到底是什么意思,咱们得回到它最初的语境。这个词最早就出现在一个叫弗朗西斯·高尔顿的英国科学家那里,大概在19世纪末。当时高尔顿在研究父母和子女的身高关系。.............
  • 回答
    回避型人格,听起来有点距离感,但其实在我们身边并不少见。它不是那种一眼就能看出来的疯狂或者极端,而是像一层薄薄的雾,悄悄笼罩着一个人,让他们在人际交往中显得有些特别。想象一下,你有一个朋友,他其实挺聪明,也很有趣,但你总觉得离他有点远。你约他出去,他可能会说:“嗯,我再看看,可能那天有点事。” 然后.............
  • 回答
    宝可梦,这个词如今在全球范围内掀起了一股不衰的热潮,它的魅力早已超越了游戏本身,成为了一种文化现象。而这一切的起点,都要追溯到那个关于“捕捉、培育、对战”的梦想,一个在日本乡村角落里悄然萌芽的童年回忆。创作宝可梦的灵魂人物,便是那位被誉为“宝可梦之父”的田尻智。他是一位热爱自然、充满好奇心的日本人。.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有