golang拾遗:为什么我们需要泛型( 二 )


遗憾的是通用引用类型帮我们把具体的类型信息全部擦除了 。
写程序最重要的就是发散型的思维 , 如果你看到这里觉得本方案不行了的话你就太天真了 。 别的不说 , java能用Object实现泛用容器 , c也可以 。 秘诀很简单 , 既然我们不能准确创建类型的实例 , 那不创建不就行了嘛 。 队列本来就是负责存取数据的 , 创建这种工作外包给其他代码就行了:
typedef struct {unsigned int max_size;unsigned int current;void **data;} Queue;Queue* NewQueue(unsigned int size){Queue* q = (Queue*)malloc(sizeof(Queue));if (q == NULL) {return NULL;}q->max_size = size;q->size = 0;q->data = http://kandian.youth.cn/index/(void **)malloc(size*sizeof(void*));}bool QueuePush(Queue* q, void* value){if (q == NULL || value == NULL || q->current == q->max_size-1) {return false;}q->data[q->current++] = value;return true;}It works! 但是我们需要队列中的类型有特定操作呢?把操作抽象形成函数再传递给队列的方法就行了 , 可以参考c的qsort和bsearch:
#include void qsort(void *base, size_t nmemb, size_t size,int (*compar)(const void *, const void *));void *bsearch(const void *key, const void *base,size_t nmemb, size_t size,int (*compar)(const void *, const void *));更普遍的 , 你可以用链表去实现队列:
typedef struct node {int val;struct node *next;} node_t;void enqueue(node_t **head, int val) {node_t *new_node = malloc(sizeof(node_t));if (!new_node) return;new_node->val = val;new_node->next = *head;*head = new_node;}原理同样是将创建具体的数据的任务外包 , 只不过链表额外增加了一层node的包装罢了 。
那么这么做的好处和坏处是什么呢?
好处是我们可以遵守DRY原则了 , 同时还能专注于队列本身的实现 。
坏处那就有点多了:

  • 首先是类型擦除的同时没有任何类型检测的手段 , 因此类型安全无从保证 , 比如存进去的可以是int , 取出来的时候你可以转换成char* , 程序不会给出任何警告 , 等你准备从这个char*里取出某个位置上的字符的时候就会引发未定义行为 , 从而出现许许多多奇形怪状的bug
  • 只能存指针类型
  • 如何确定队列里存储数据的所有权?交给队列管理会增加队列实现的复杂性 , 不交给队列管理就需要手动追踪N个对象的生命周期 , 心智负担很沉重 , 并且如果我们是存入的局部变量的指针 , 那么交给队列管理就一定会导致free出现未定义行为 , 从代码层面我们是几乎不能区分一个指针是不是真的指向了堆上的内容的
  • 依旧不能避免书写类型代码 , 首先使用数据时要从void*转换为对应类型 , 其次我们需要书写如qsort例子里那样的帮助函数 。
动态类型语言的特例在真正进入本节的主题之前 , 我想先介绍下什么是动态类型 , 什么是静态类型 。
所谓静态类型 , 就是在编译期能够确定的变量、表达式的数据类型 , 换而言之 , 编译期如果就能确定某个类型的内存布局 , 那么它就是静态类型 。 举个c语言的例子:
int a = 0;const char *str = "hello generic";double values[] = {1., 2., 3.};上述代码中int、const char *、double[3]都是静态类型 , 其中int和const char *(指针类型不受底层类型的影响 , 大家有着相同的大小)标准中都给出了类型所需的最小内存大小 , 而数组类型是带有长度的 , 或者是在表达式和参数传递中退化(decay)为指针类型 , 因此编译器在编译这些代码的时候就能知道变量所需的内存大小 , 进而确定了其在内存中的布局 。 当然静态类型其中还有许多细节 , 这里暂时不必深究 。
回过来看动态类型就很好理解了 , 编译期间无法确定某个变量、表达式的具体类型 , 这种类型就是动态的 , 例如下面的python代码:
name = 'apocelipes'name = 12345name究竟是什么类型的变量?不知道 , 因为name实际上可以赋值任意的数据 , 我们只能在运行时的某个点做类型检测 , 然后断言name是xxx类型的 , 然而过了这个时间点之后name还可以赋值一个完全不同类型的数据 。
好了现在我们回到正题 , 可能你已经猜到了 , 我要说的特例是什么 。 没错 , 因为动态类型语言实际上不关心数据的具体类型是什么 , 所以即使没有泛型你也可以写出类似泛型的代码 , 而且通常它们工作得很好:
class Queue:def __init__(self):self.data = http://kandian.youth.cn/index/[]def push(self, value):self.data.append()def pop(self):self.data.pop()def take(self, index):return self.data[index]我们既能放字符串进Queue也能放整数和浮点数进去 。 然而这并不能称之为泛型 , 使用泛型除了因为可以少写重复的代码 , 更重要的一点是可以确保代码的类型安全 , 看如下例子 , 我们给Queue添加一个方法: