定义
回调函数(callback function) 被定义为"作为参数传递的函数", 或者用更加接地气的方式描述:
将函数 A 作为参数传递给函数 B, B 在执行的某一刻调用 A, 此时称 A 为回调函数
作用
回调提供了一种灵活的方式来扩展或自定义函数的行为, 需要在某些特定的时刻执行特定的动作, 但这些动作可能因情境而异. 通过使用回调, 可以允许其他代码决定何时以及如何响应
简单来说, 回调函数保证了调用者对被调用者的控制权, 是一种灵活的函数调用方式
应用场景
- 事件驱动
以 GUI 为例, 假如我们需要实现诸如"用户用鼠标点击按钮, 程序作出响应"的功能, "响应"就可以作为回调函数传递给按钮类
- 计时器
考虑需求"每 1min 执行函数一次", 我们可以先定义计时器类, 再将函数作为回调函数传递给计时器的实例化, 避免了我们只能通过继承的方式创建大量无用的计时器类的子类
- 错误处理
实现
作为一种机制, 回调函数在很多语言中都有实现方法
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; }); }
|
实例
计时器
实现一个计时器类, 接受一个回调函数, 在计时达到一定时间时调用回调函数
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{ } };
|
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([](){ }); }
|
通过对比我们可以发现, 回调版本的代码更加简洁. 同时假如我们需要多次复用 Timer
, 回调版本可以省去很多多余的继承, 使代码更容易维护
总结
一般来说, 当我们需要扩展的基类满足以下条件, 回调函数的表现就会优于继承
一些现代语言, 例如 Rust 已经完全舍弃了继承这一概念, 回调函数其实更加接近 OI(面向接口 oriented-interface) 而不是 OO