定义

回调函数(callback function) 被定义为"作为参数传递的函数", 或者用更加接地气的方式描述:

将函数 A 作为参数传递给函数 B, B 在执行的某一刻调用 A, 此时称 A 为回调函数

作用

回调提供了一种灵活的方式来扩展或自定义函数的行为, 需要在某些特定的时刻执行特定的动作, 但这些动作可能因情境而异. 通过使用回调, 可以允许其他代码决定何时以及如何响应

简单来说, 回调函数保证了调用者对被调用者的控制权, 是一种灵活的函数调用方式

应用场景

  1. 事件驱动
    以 GUI 为例, 假如我们需要实现诸如"用户用鼠标点击按钮, 程序作出响应"的功能, "响应"就可以作为回调函数传递给按钮类
  2. 计时器
    考虑需求"每 1min 执行函数一次", 我们可以先定义计时器类, 再将函数作为回调函数传递给计时器的实例化, 避免了我们只能通过继承的方式创建大量无用的计时器类的子类
  3. 错误处理

实现

作为一种机制, 回调函数在很多语言中都有实现方法

C

在 C 中一般使用函数指针实现回调函数, 考虑这样的情况:

编写修饰函数 Modify, 使得给定输入数组 arr, 要求对数组 arr 遍历, 并对其中所有元素加上给定的同长数组 append 对应的值. 出于简单, 只考虑 arr 的长度为 5, append = [3, 2, 4, 5, 1] 的情况

#include<stdio.h>

typedef int (*Callback)(int);

const int n = 5;

int Append(int x){
static int append[5] = {3, 2, 4, 5, 1};
return append[x];
}

void Modify(int* arr, Callback ptr_callback){
for(int i = 0; i < n; ++i){
arr[i] += (*ptr_callback)(i);
}
}

int main(){
int arr[5];
for(int i = 0; i < n; ++i){
scanf("%d", arr + i);
}
Modify(arr, Append);
for(int i = 0; i < n; ++i){
printf("%d, ", arr[i]);
}
}

在这个程序中, 函数 Append 以函数指针的形式, 作为回调函数传递给了 Modify

C++

在 C++11 及以上, 我们通常使用函数对象, lambda匿名函数和 std::function 等方式实现回调函数

另见

下面是利用 lambda匿名函数实现的回调

#include<functional>
#include<iostream>
#include<vector>

using VectorInt = std::vector<int>;

void Modify(const VectorInt& vec, const std::function<void(int)>& callback){
for(auto i:vec){
callback(i);
}
}

int main(){
VectorInt vec = {1, 2, 3, 4, 5};
Modify(vec, [](int x){
std::cout << x << std::endl;
});
}

实例

计时器

实现一个计时器类, 接受一个回调函数, 在计时达到一定时间时调用回调函数

// inherit
class Timer{
public:
Timer() = default;
~Timer() = default;

void on_update(){
++cur_time;
if(cur_time == wait_time){
cur_time = 0;
callback();
}
}

protected:
virtual void callback() = 0;

private:
int cur_time = 0;
int wait_time = 0;
};

class Refresher:Timer{
void callback() override{
// function body...
}
};
// callback
class Timer{
public:
Timer() = default;
~Timer() = default;

void on_update(){
++cur_time;
if(cur_time == wait_time){
cur_time = 0;
callback();
}
}

void set_callback(std::function<void()> callback){
this->callback = callback;
}

private:
std::function<void()> callback;
int cur_time = 0;
int wait_time = 0;
};

int main(){
Timer refresher;
refresher.set_callback([](){
// function body...
});
}

通过对比我们可以发现, 回调版本的代码更加简洁. 同时假如我们需要多次复用 Timer, 回调版本可以省去很多多余的继承, 使代码更容易维护

总结

一般来说, 当我们需要扩展的基类满足以下条件, 回调函数的表现就会优于继承

  • 只需要扩展方法逻辑
  • 无需扩展成员数据

一些现代语言, 例如 Rust 已经完全舍弃了继承这一概念, 回调函数其实更加接近 OI(面向接口 oriented-interface) 而不是 OO