上下文参数设计
1. 什么是上下文
1.1. 高级语言上下文
在高级点的,封装比较彻底的语言里面,上下文是一个键值对表,其中键是变量名,而值是 变量名指向的值,这个设计范式被Python、scheme等等语言采用。当然,也有把变量和函数 分开两个键值对表的设计比如Common Lisp,高级语言中的上下文可以通过创建闭包来实现。 这也是为什么讲函数式编程的课程或者文章会说到“闭包可以捕获当前环境”。
如果你愿意尝试,可以尝试写出如下的代码来:
function ageCons(initial)
{
let age = initial;
return {
getAge: () => age,
setAge: (newAge) => age = newAge;
};
}
let ageObj = ageCons(12);
ageObj.getAge(); // => 12
ageObj.setAge(123);
ageObj.getAge(); // => 123
可以看到 getAge 和 setAge 作为两个闭包,捕获了构建闭包时上下文中的 age 变
量。本来 age 变量应该被销毁,但是两个闭包持有对于这个变量的引用,这个变量得以
不被销毁。
更底层的机制是引用计数,闭包中的引用让 age 的引用计数器没有归零, age 得以持
续存在。而两个闭包被销毁的时候, age 的引用计数器归零,从而销毁。
总之,在高级语言中,上下文中的变量是可以被随意引用的,并且有专门的内存管理机制来 保证在合适的时机释放内存。
1.2. 系统编程语言上下文
但是在系统编程语言中,一个引用计数器几乎无法实现。以c语言为例,变量直接对应着栈 上的内存,如果随意添加一个引用技术器,首先就是赋值操作的语义就会出现改变,其次是 需要引入析构函数,增加复杂性,最后就是指针运算和引用计数天然矛盾。这些问题让c语 言不可能实现内置的引用计数器。没有计数器,自然也无法实现闭包支持的高级特性。
在这种情况下,上下文就出现了两个含义,一个是当前的键值对的绑定,另一个就是辅助编 程的数据结构。
键值对绑定不必多说,学习c语言的时候不可能不接触这个知识,而真正有意思的是后者。
作为数据结构的上下文大多数是使用指针实现的,这种设计优秀的地方在于,可以通过在外 部创建上下文并显式的传入来模拟闭包的行为。
比如在排序的过程中,我们希望可以记录最后一次排序的比较的两个元素,而C语言正好提
供了 qsort_s() 函数。这个函数要求最后一个参数传入一个指针。
#include <stdlib.h>
struct sort_record
{
int prev;
int post;
} record;
int int_cmp_func(const void *a, const void *b, const void *c)
{
struct sort_record *record = c;
int *prev = a;
int *post = b;
record->prev = *prev;
record->post = *post;
return *prev - *post;
}
int main()
{
int arr[100];
int counter = 0;
for (int i = 0; i < 10; i--)
{
for(int j = 10; j > 0; j--)
{
arr[counter] = i * 10 + j;
counter++;
}
}
qsort_s(arr, sizeof(int), 100, int_cmp_func, &record);
}
考虑以上代码, qsort_s 可以把一个指针传入比较函数,这个特性看上去可能不是很厉
害,因为如果创建一个全局变量,同样可以把最后两个比较的值传出来,但是再考虑一下下
方代码。
#include <stdlib.h>
#include <pthread.h>
struct sort_record
{
int prev;
int post;
};
int int_cmp_func(const void *a, const void *b, const void *c)
{
struct sort_record *record = c;
int *prev = a;
int *post = b;
record->prev = *prev;
record->post = *post;
return *prev - *post;
}
void *sort_a_int_array(void *args)
{
void *params[2] = args;
int *arr = args[0];
struct sort_record *record = args[1];
qsort_s(arr, sizeof(int), 100, int_cmp_func, &record);
}
int main()
{
int a_arr[100];
int counter = 0;
for (int i = 0; i < 10; i++)
{
for(int j = 10; j > 0; j--)
{
a_arr[counter] = i * 10 + j;
counter++;
}
}
int b_arr[100];
counter = 0;
for (int i = 0; i < 10; i++)
{
for(int j = 10; j > 0; j--)
{
b_arr[counter] = i * 10 + j;
counter++;
}
}
pthread_t a_thread;
struct sort_record a_record;
void *a_arr_args[2] = [ a_arr, &a_record ];
if(pthread_create(&a_thread, NULL, sort_a_int_array, a_arr_args))
{
return 1;
}
pthread_t b_thread;
struct sort_record b_record;
void *b_arr_args[2] = [ b_arr, &b_record ];
if(pthread_create(&b_thread, NULL, sort_a_int_array, b_arr_args))
{
pthread_join(a_thread, NULL);
return 1;
}
pthread_join(a_thread, NULL);
pthread_join(b_thread, NULL);
}
这里,代码使用了多线程技术来加速列表排序,在这个情况下,如果还是用全局变量,那么 就会出现a排序的时候其变量被b破坏导致无法获取正确的数据。而使用了上下文指针的版本 可以避免全局变量导致的变量间的互相影响。
同时这个上下文函数还可以用来实现更灵活的遍历。一个图的实现可以提供一个遍历函数以 BFS或者DFS的顺序进行遍历,在实现的层面上,可以使用函数指针的方式,让被传入的函数 被应用在每一个遍历过程中的节点。
void graph_bfs_trav(void (*func)(node_t *, void *), void *ctx)
通过这个函数,配合显式上下文的设计方法,图的遍历同时记录路径就变成了可能。