高软lab:深入理解Callback函数

sunfa_jasme 2023-04-08 16:31:33

回调函数的使用

 

回调函数:把一段可执行的代码作为参数(例如:函数指针)传递给其他代码,这段代码不一定会被立即执行。如果立即执行,则为同步回调;如果执行了后续的一些代码后再执行,则为异步回调。在异步回调中,我们不必关心回调函数何时完成,我们可以交给同一台机器上的一个线程、进程甚至交给另一台机器去完成,而main函数仍然保持继续执行。这样的特性,在运行一些耗时较长的功能时是非常实用的,例如文件读写、接受网络数据等。

menu.c是一个命令行下的菜单程序,在linklist.h中定义了所有键入的命令信息DataNode的数据结构——一个简单的单链表。除了必要命令信息和链表指针外,还有一个int型的函数指针,该指针被调用时将执行handler函数。可以发现,这个调用以面向过程的语法实现了类似面向对象编程的功能,handler就好像是OOP里的DataNode对象的一个方法。

// linktableInternal.h
typedef struct DataNode
{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;

通常的函数调用的过程是,程序员填写一个函数调用的所有参数,然后等待该函数执行完成以后得到想要的结果,形如以下的代码:

// 自己写的普通函数调用
char* getDesc(tDataNode* head, char* cmd);
int main() 
{
    // ...
    char* res = getDesc(head, "version");
    // ...
}

过程的图示如下:

 

 

 

 

 

在实际的开发过程中,当main和getDesc由不同的开发人员完成时,可能无法在一开始确定getDesc需要多少个参数,从而可能产生后续的修改,增加工作量。但是,当我们仅仅是调用函数指针时,这样的问题就不会发生。图示如下:

 

 

 

 

 

 

另外,当多个开发团队实现类似但不完全相同的功能时,menu.c为我们展现了另一个技巧:

// linktable.c
int InitMenuData(tLinkTable ** ppLinktable)
{
    *ppLinktable = CreateLinkTable();
    tDataNode* pNode = (tDataNode *)malloc(sizeof(tDataNode));
    pNode->cmd = "help";
    pNode->desc = "Menu List:";
    pNode->handler = Help;
    AddLinkTableNode(*ppLinktable, (tLinkTableNode *)pNode);
 
    pNode = (tDataNode *)malloc(sizeof(tDataNode));
    pNode->cmd = "version";
    pNode->desc = "Menu Program V1.0";
    pNode->handler = NULL;
    AddLinkTableNode(*ppLinktable, (tLinkTableNode *)pNode);
 
    pNode = (tDataNode *)malloc(sizeof(tDataNode));
    pNode->cmd = "quit";
    pNode->desc = "Quit from Menu Program V1.0";
    pNode->handler = Quit;
    AddLinkTableNode(*ppLinktable, (tLinkTableNode *)pNode);
 
    return 0;
}
int Help()
{
    ShowAllCmd(head);
    return 0; 
}
int Quit();    // 类似的写法
int ShowAllCmd(tDataNode * head);

可以发现,InitMenuData()中为不同的链表结点赋予了不同的函数指针,在这样的设计之下,同样是int型函数指针,却可以调用不同的函数,这样就能够为不同的用户/开发团队实现不同的功能。这在设计模式中被称为“模板方法模式”。在这样的设计方式下,我们为不同业务实现了耦合度较低的、通用的接口。不同层级的模块通过结构化的数据结构进行数据传递,规格约定是隐含的,这是一种“标记耦合”。

 

程序的抽象层次

 

 

 

 

 

 

 

 

 

 

 

 

 

 

从数据的角度看,从上到下,menu.c定义了基于DataNode结构体的多种抽象的业务,却不必关注底层实现;linktable.c定义了基于tLinkTable的多种数据操作,这些操作则需要考虑底层的实现,例如在高并发条件下需要实现的多线程。

// LinktableInternal.c
struct LinkTableNode
{
    struct LinkTableNode * pNext;
};
struct LinkTable
{
    struct LinkTableNode *pHead;
    struct LinkTableNode *pTail;
    int			SumOfNode;
    pthread_mutex_t mutex;
};
// menu.c
typedef struct DataNode
{
    tLinkTableNode head;
    char*   cmd;
    char*   desc;
    int     (*handler)();
} tDataNode;

从程序的角度看,从上到下,复杂的业务首先按照业务的项目分类:Help()、Quit()、findCmd(),接着我们将这些业务抽象为几个通用的操作ShowAllCmd();SearchCondition()等,更进一步地,通用的业务模块需要实现对底层数据结构。

以上模块化的设计降低了程序的耦合度,其中采用回调函数的部分由于采用了隐式的函数指针来实现模块间的共享,属于标记耦合;其余的模块间无法脱离对具体的DataNode或LinkTable数据结构的显式调用,因此属于公共耦合。前者的耦合度更低一些,这也是回调函数的一大优点。

 

参考资料

 

孟宁 / 软件工程

回调函数_wikipedia

C 语言回调函数详解

10张图让你彻底理解回调函数

行为型模式-模板方法模式

...全文
226 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧