天天看点

编程范式学习总结-泛型编程泛型编程

泛型编程

程序 = 算法 + 数据

C

C 语言会有几个问题:

通用的算法:需要对所处理的数据的数据类型进行适配。但在适配数据类型的过程中,C 语言只能使用 void* 或 宏替换的方式,这两种方式导致了类型过于宽松。

适配数据类型:需要 C 语言在泛型中加入一个类型的 size,因为识别不了被泛型后的数据类型,C 语言没有运行时的类型识别。

适配数据结构:算法其实是在操作数据,而数据则是放到数据结构中的,所以,真正的泛型除了适配数据类型外,还要适配数据结构。比如容器内存的分配和释放、如对象之间的复制。

总体来说,C 语言设计目标是提供一种能以简易的方式编译、处理低层内存、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。C 语言也很适合搭配汇编语言来使用。C 语言把非常底层的控制权交给了程序员,它设计的理念是:

  • 相信程序员;
  • 不会阻止程序员做任何底层的事;
  • 保持语言的最小和最简的特性;
  • 保证 C 语言的最快的运行速度,虽然会牺牲移值性。

从某种角度上来说,C 语言的伟大之处在于——使用 C 语言的程序员在高级语言的特性之上还能简单地做任何底层上的微观控制。

C++

C++很大程度就是用来解决 C 语言中的各种问题和各种不方便的:

  • 用引用来解决指针问题。
  • 用 namespace 来解决名字空间冲突问题。
  • 通过 try-catch 来解决检查返回值编程问题。
  • 用 class 解决对象的创建、复制、销毁问题,解决在结构体嵌套时深度复制的内存安全问题。
  • 通过重载操作符来达到操作上的泛型。比如用>>操作符消除printf()的数据类型不够泛型的问题。
  • 通过模板 template 和虚函数的多态以及运行时识别来达到更高层次的泛型和多态。
  • 用 RAII(Resource Acquisition Is Initialization,资源创建即初始化)、智能指针的方式,解决了 C 语言中因为需要释放资源而出现的那些容易出错的代码的问题。
  • 用 STL 解决了 C 语言中算法和数据结构的 N 多种坑。

对于泛型的抽象,如果数据类型符合通用算法,那么对数据类型的最小需求是什么?

第一,它通过类的方式来解决。

  • 类里的构造函数、析构函数表示类的分配和释放。
  • 拷贝构造函数,表示了对内存的复制。
  • 重载操作符,像我们要去比较大于、等于、不等于。

    让一个用户自定义的数据类型和内建的数据类型很一致。

第二,通过模板达到类型和算法的妥协。

  • 模板有点像 DSL,模板的特化会根据使用者的类型在编译时期生成哪个模板的代码。
  • 模板可以通过一个虚拟类型来做类型绑定,这样不会导致类型转换时的问题。

    模板很好地解决了C 语言宏定义带来的问题。

第三,通过虚函数和运行时类型识别。

  • 虚函数带来的多态在语义上可以支持“同一类”的类型泛型。
  • 运行时类型识别技术可以做到在泛型时对具体类型的特殊处理。

    可以写出基于抽象接口的泛型。

一个良好的泛型编程需要解决如下几个泛型编程的问题:

  • 算法的泛型
  • 类型的泛型
  • 数据结构(数据容器)的泛型

类型系统

程序语言的类型系统主要提供如下的功能:

  • 程序语言的安全性。使用类型可以让编译器检查一些代码的错误。
  • 利于编译器的优化。 静态类型语言的类型声明,可以让编译器明确地知道代码的意图。因此,编译器就可以做很多代码优化工作。
  • 代码的可读性。有类型的编程语言,可以让代码更易读和更易维护,代码的语义也更清楚,代码模块的接口(如函数)也更丰富和清楚。
  • 抽象化。类型允许程序设计者对程序以较高层次的方式思考,而不是低层次实现。

一类是静态类型语言,如 C、C++、Java,一种是动态类型语言,如 Python、PHP、JavaScript 等

动态类型的语言,会以类型标记维持程序所有数值的“标记”,并在运算任何数值之前检查标记。一个变量的类型是由运行时的解释器来动态标记的,这样就可以动态地和底层的计算机指令或内存布局对应起来。

每个语言都需要一个类型检查系统:

静态类型检查 在编译器进行语义分析时进行的。如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),那么称此处理为强类型,反之称为弱类型。

动态类型检查 在运行时期做动态类型标记和相关检查。所以,动态类型的语言必然要给出一堆诸如:is_array(), is_int(), is_string() 或是 typeof() 这样的运行时类型检查函数。

泛型本质

要了解泛型的本质,就需要了解类型的本质。

  • 类型是对内存的一种抽象。不同的类型,会有不同的内存布局和内存分配的策略。
  • 不同的类型,有不同的操作。所以,对于特定的类型,也有特定的一组操作。

所以,要做到泛型,我们需要做下面的事情:

  • 标准化掉类型的内存分配、释放和访问。
  • 标准化掉类型的操作。比如:比较操作,I/O 操作,复制操作……
  • 标准化掉数据容器的操作。比如:查找算法、过滤算法、聚合算法……
  • 标准化掉类型上特有的操作。需要有标准化的接口来回调不同类型的具体操作……

C++ 动用了非常繁多和复杂的技术来达到泛型编程的目标。

  • 通过类中的构造、析构、拷贝构造,重载赋值操作符,标准化(隐藏)了类型的内存分配、释放和复制的操作。
  • 通过重载操作符,可以标准化类型的比较等操作。
  • 通过 iostream,标准化了类型的输入、输出控制。
  • 通过模板技术(包括模板的特化),来为不同的类型生成类型专属的代码。
  • 通过迭代器来标准化数据容器的遍历操作。
  • 通过面向对象的接口依赖(虚函数技术),来标准化了特定类型在特定算法上的操作。
  • 通过函数式(函数对象),来标准化对于不同类型的特定操作。

本质 —— 屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。

继续阅读