1、自己之前在开发功能时候也常常封装很多函数。这种做法会造成重复的代码,让dlist的实现随着应用环境的变化而变化。
2、采样上一篇的回调函数法。这两个函数的实现和dlist_print的实现类似,无非是print那行代码要换成别的功能。在真正动手时,发现每个回调函数都要保存一些中间数据。大部分人选择了用全局变量来保存,这可以实现要求的功能(这也是我常常犯的毛病),但违背了禁用全局变量的原则。
(1).不要编写重复的代码。
写重复的代码很简单,甚至凭本能都可以写出来。但要想成为优秀的程序员,一定要克服自己的惰性,因为重复的代码造成很多问题:
重复的代码更容易出错。在写类似代码的时候,几乎所有人都会选择 Copy&Paste 的方法,这种方法很容易犯一些细节上的错误,如果某个地方修改不完整,那就留下了”不定时”的炸弹,说不定什么时候会暴露出来。
重复的代码经不起变化。无论是修改BUG,还是增加新特性,往往你要修改很多地方,如果忘掉其中之一,你同样得为此付出代价。请记住古惑仔的话,出来混迟早是要还的。大师们说过,在软件中欠下的BUG,你会为此还得更多。
去除重复代码往往不是件简单的事情,需要更多思考和更多精力,不过事实证明这是最值得的投资。在这里,我们要怎么抽取这些重复的代码呢?
这三个函数无非是要遍历双向链表并做一些事情,遍历双向链表我们可以提供一个dlist_foreach 函数,至于要做什么,这是千变万化的行为,可以通过回调函数让调用者去做。
(2)、任何回调函数都要有上下文
虽然混了这么多年嵌入式开发软件的饭,但是自己还是常常喜欢用全局变量;使用全局变量有很多坏处,按作者的说法列举如下:
(禁止全局变量
除了为使用单件模式(只允许一个实例存在)的情况外,任何时候都要禁止使用全局变量。这一点我反复的强调,但发现初学者还是屡禁不止,为了贪图方便而使用全局变量。请读者从现在开始就记住这一准则。
全局变量始终都会占用内存空间,共享库的全局变量是按页分配的,那怕只有一个字节的全局变量也占用一个page,所以这会造成不必要空间浪费。全局变量也会给程序并发造成困难,想把程序从单线程改为多线程将会遇到麻烦。重要的是,如果调用者直接访问这些全局变量,会造成调用者和实现者之间的耦合。)
在使用回调函数情况下可以这样避免使用全局变量:很简单,给回调函数传递额外的参数就行了。这个参数我们称为回调函数的上下文,变量名用ctx(context的缩写)。要在这个上下文中存放什么东西呢?那得根据具体的回调函数而定,为了能保存任何数据类型,我们选择void*表示这个上下文。
DListRet dlist_foreach(DList* list,DListDataVisitFunc visit,void* ctx)
{
DListNode* node = list->first;
while(node != NULL)
{
visit(ctx,node->data);
node = node->next;
}
return DLIST_RET_OK;
}
visit 是回调函数,ctx就是我们说的上下文。要特别强调的一点是,ctx应该作为回调函数的第一个参数。为什么呢?在前面我们讲过的面向对象的函数命名规则中,我们以thiz作为函数的第一个参数,而thiz通常也就是函数的上下文。如果在这里恰ctx==thiz,就不需要因为参数顺序不同而做转换了。
实现求和的回调函数:
DListRet sum_cx(void* ctx,void* data)
{
long* sum = ctx;
*sum += (int)data;
return DLIST_RET_OK;
}
调用foreach:
long sumx = 0;
dlist_foreach(dlist,sum_cx,&sumx);
同理,查找最大值的回调函数:
typedef struct _Max_Val
{
unsigned char first;
int max_val;
}Max_Val;
DListRet max_cx(void* ctx,void* data)
{
Max_Val* max = ctx;
if(ctx->first == 0)
{
ctx->first = 1;
max->max_val = (int)data;
}
else if(max->max_val < (int)data)
{
max->max_val = (int)data;
}
return DLIST_RET_OK;
}
调用foreach:
Max_Val maxval = {.first = 0,0};
dlistforeach(dlist,max_cx,&maxval);