左耳听风_032_31_编程范式游记2-_泛型编程
你好,我是陈浩网名英,做耳朵house.在上一节课中呢,我们从c语言开始说起,聊了聊面向过程式的辩证范式。
那相信从代码的角度呢,你对这种类型的语言啊已经有了一些理解。
那作为一门高级语言呢,c语言啊它绝对是编程语言历史发展过程中的一个重要里程碑。
但是随着认知的升级呢,面向过程的c语言啊已经无法满足更高层次的编程的需要。
于是呢c加加就出现了。
那一九八零年ATT贝尔实验室的biononor straw strope创建的c加加语言横空出世,它既可以全面兼容c语言,又巧妙的柔和了一些面向对象的编程理念。
那现在来看,不得不佩服他的魄力。
那在这里呢我也向你推荐一本书,那书名是c加加语言的设计和演化。
那这本书呢系统介绍了c加加诞生的背景和初衷。
那书的作者呢就是stropfoop本人。
所以你可以非常详细的从语言创建者的角度啊,了解他的设计思路和创新之旅。
当然呢就是在今天啊c加加这门语言啊仍然还有很多争议。
那这里里呢我就细细说,那那果果感兴趣的话,可以看一看我几年前在库壳上发表的文章c加加的坑真的多么?那从语言的角度上来说呢,呃实际上早期的c加加的许多工作啊都是对c的强化和进化,并且呢把完全兼容c啊作为强制性的要求啊,这也是c加加复杂晦涩的原因。
在这一点上呢,java就干的比c加加彻底的多。
在c八九、c九九这两个c语言的标准中呢,有许多改进啊,都是从c加加中引进的。
可见呢c加加对c语言的贡献非常大。
是的,因为c加加很大程度上就是用来解决c语言中的各种问题和各种不方便的。
比如首先啊他用了引用来解决指针的问题。
那第二呢,他用了name space来解决命名空间充足的问题。
那第三呢,他通过try catch来解决检查返回值编程的问题。
那第四呢,他用class来解决对象的创建、复制和销毁的问题,从而可以解决在结构底嵌套的时候可以深度复制的内存安全问题。
那第五呢,他通过重载操作符来实现操作上的泛醒,比如消除了上一讲中提到的比较函数,compare function.那再比如用一位操作符来消除print f函数的数据类型,不够泛型的问题。
那第六呢,它通过模板template,虚函数的多态和运行式识别,来达到更高层次的泛型和多态。
那第七呢,他用RARI和智能指针的方式解决了c语言装啊,因为需要释放资源而出现的那些非常ugly啊,也很容易出错的代码的问题。
那第八点呢,他用STL解决了c语言中算法和数据结构的n多种抗衡。
那c加加呢是支持编程范式最多的一门语言。
它虽然解决了很多c语言的问题啊,但我个人觉得它最大的意义呢是解决了c语言泛型编程的问题。
因为我们可以看到一些c加加的标准规格,说明数量有一半以上都在说明STL的标准规格应该是什么样的这说明啊,泛型编程是c加加重点中的重点。
那在理想的情况下呢,算法应该是与数据结构和类型无关的那各种特殊的数据类型,理应做好自己分内的工作。
那算法呢只关心一个标准的实现。
而对于泛型的抽象呢,我们需要回答的问题是,如果我们的数据类型符合通用算法,那么对数据类型的最小需求又是什么呢?我们来看一看c加加它是如何有效解决程序泛型的问题的。
我认为呢有三点。
那第一呢它是通过类的方式来解决。
那类里面呢会有构造函数和析构函数,表示这个类的分配和释放,还有它的拷贝构造函数啊,表示对内存的复制,还有重在操作符啊,像我们呢要去比较大于等于和不等于。
那这样呢就可以让一个用户自定义的数据类型和内建的那些数据类型就很一致了。
那第二呢,它通过模板达到类型和算法的妥协。
那模板呢有点像DSL模板的特化呢,会根据使用者的类型在编缘时期啊生成那个模板的代码。
那模板呢还可以通过一个虚拟类型来做类型绑定。
那这样呢不会导致类型转换时的问题。
所以模板呢就很好的取代了c时代宏定义带来的问题。
第三呢,它引用了虚函数和运行时的类型识别。
虚函数带来的多肽在语义上呢可以支持同一类的类型泛型。
而运行时类型识别技术呢可以做到在泛行时啊,对具体类型的特殊处理。
那这样一来呢就可以写出基于抽象接口的泛型了。
那拥有这些c加加引入的技术呢,我们就可以做到c语言很难做到的泛型编程了。
那正如前面所说过的,一个良好的泛型编程呢,需要解决算法的泛型类型的泛型,属据结构的泛型啊,这几个泛型编程的问题,就像前面介绍过的函函,我拿它啊,举个例子里面的searfor int i等于零,i小于line i加加。
这样的遍历方式呢只能适用于顺序型的数据结构的迭代啊,比如array、 set、 hill list,还有link等等。
它并不适用于非顺序型的数据结构啊,比如哈希表啊、二叉树啊,还有图啊等等。
数据不是按顺序存放的数据结构。
所以呢如果找不到一种泛型的数据结构的操作方式,包括遍历啊查找啊,增加删除和改等等。
那么任何的算法或者程序啊都不可能做到真正意义上的泛型。
那除了lserge函数这样的遍历操作之外呢,还有serge函数的返回值,而是一个整型的索引结构。
它并个整形的下标呢,对于顺序型的数据结构啊是没有问题的那是对于非顺序型的数据结构呢在语义上都存在问题。
比如我要在一个harsh table中查找一个key,那应该返回什么呢?一定不是返回索引下标。
因为在hash table这样的数据结构中呢,数据的存放位置啊不是顺序的。
而且呢还会因为容量不够的问题被重新哈希后改变,所以呢返回数组下标啊是没有意义的。
对此呢我们要把这个事儿做的泛型和通用一些。
如果找到了这个元素呢,就返回它的一个指针啊,会更靠谱一些。
所以呢为了解决泛型的问题,我们需要动用以下两个c加加的技术。
第一呢是使用模板技术来抽象类型。
那这样呢就可以写出类型无关的数据结构。
那第二呢是使用一个迭代器来便利或者操作数据结构内的元素。
那这样呢我们就把c语言的设置函数啊改造成了c加加泛型版本的设置函数啊,你可以去文中看一下。
在c加加的泛型版本中呢,我们可以看到它使用了type name t抽象了数据结构中存储数据的类型。
它还使用了type name eter.那这个呢是不同数据结构需要自己实现的迭代器。
那这样呢也就抽象掉了不同类型的数据结构。
然后呢我们对数据容器的便利啊,使用了特尔中的加加方法。
那这个呢是数据容器,需要自己重载的操作符,这样通过操作符重载啊,也就泛型叫做便利。
同时呢在函数的入参上,它使用了p start和p and来表示遍历的起止。
另外呢它还使用新eter来取得这个指针的内容。
那这个呢也是通过重载星号啊,也就是取值操作符来达到的泛型。
当然啊你可能会问,为什么我们不用标准接口,点点eanext来取代加加,用eter点get value来取代星号,而是要重载操作符呢?那其实这样做呢是为了兼容原有c语言的编程习惯。
那在这里呢我要说明一下,所谓的eter呢,在实际的代码中啊,就是像vector类里面的iterator啊,或者是map类里面的initerator这样的东西。
那这个呢是由相应的数据容器来实现和提供的。
我们可以对比一下CI加STL标准库中find的这个函数的代码实现。
也许你觉得到这一步,我们的泛型设计就完成了。
其实呢这还远远不够,四sh函数呢只是一个开始,我们还有别的算法会让问题啊变得更加复杂。
我们再来看一个sum求和函数。
那同样呢我们在文中呢把c语言版本的sum实现改造成了c加加泛型版本的实现。
那从代码中你看到什么问题了吗?这个代码中最大的问题呢就是t result等于零这套语句。
那这个零呢它假设的返回值的类型啊是个int.这个t呢它假设了英特尔中出来的类型是个t那这样的假设呢是有问题的,如果类型不一样,就会导致转型的问题。
那这样呢会带来非常八个里的代码,那么我们应该怎么解决呢?那这个呢就需要用到c加加泛型编程的重要技术啊,也就是迭代器了。
我们知道itter在实际调用者那里啊,会是一个具体的啊像vector、 int、 iterator这样的东西。
那在这个声明中呢,int它已经被传入eter中了。
所以呢定义result的t应该可以从eter中来。
那这样呢就可以保证类型是一样的,而且啊不会有被转型的问题。
所以呢我们需要精心的实现一个迭代器。
因为c加加STL标准库里面的迭代器啊太过复杂,所以我在文中呢写了一个精简版的迭代器,我没有把所有的代码列出来,而是把它的一些基本思路列了出来。
那这里呢我说明一下几个关键点。
首先呢一个迭代器需要和一个容器在一起,因为这里面啊是对这个容器的具体的代码实现。
那其次呢,它需要重载一些操作符,比如取值操作、成员操作,还有比较操作,还有便利操作等等。
那还有呢它还有type dif一些类型,比如value type啊,告诉我们容器里数据的实际类型是什么样子。
那还有一些比如begin和and的基本操作,我们还可以看到其中呢有一个point ter PTR的内部指针来指向当前的数据。
好了,有了这个迭代期之后呢,我们还要解决t result等于零啊,后面的这个零的问题。
那这个事儿呢算法没有办法搞定,最好由用户来传入。
于是呢就出现了文中这个最终版本的泛型sum函数,我们可以看到type name eter value, type result t等于unit啊,这条语句啊是关键。
那这样呢我们就解决了所有的问题。
那这个萨姆求和函数呢,我们可以像文中这样来使用它。
那以上呢就是整个STL的泛型方法。
其中呢包括泛型的数据容器和泛型数据容器的迭代器。
那有了这些之后呢,泛型的算法就很容易写了。
但是呢还能不能做到更加抽象,更为泛型呢?比如我们有一个数据结构,employee里面有一个vacation成员变量啊,表示休假了多少天,还有工资、salary.那现在呢我想计算员工的总薪水或者总休假天数,那这样呢我们的萨玛就完全不知道该怎么搞了,因为要累加的是employee中的不同字段。
那即便我们的in葡OE重载了,加号操作,那也不知道要加哪个字段。
那另外呢我们还可能会求平均值average,求最小值min,求最大值max,求中位数mean等等。
你会发现算法写出来啊基本上都是一样的啊,只是其中的累加操作变成了另外一个操作。
就这个例子来说呢,我想计算员工薪水里面最高的和休假最少的,或者呢我想计算全部员工总共休假了多少天。
那么面对这么多的需求,我们是否可以泛醒一些呢?怎么解决这些问题呢?那要解决这个问题呢,我希望我的这个算法只管遍地。
那具体要干什么呢?那是业务逻辑,由外面的调用方来定义就好了,和我无关。
那这样一来呢,代码的通用度啊就更高了。
那这样呢就有了一个抽象程度更高的版本。
那代码呢我放到了文章里,那这个版本呢再叫萨m就不太合适了。
那这个版本呢应该是reduce用于把一个数组啊reduce成一个值。
在这个版本的代码中呢,我们需要传一个函数进来。
在STL中呢,它是一个函数对象啊,我们还是这套算法。
但是result呢不是像前面那样去加,而是把整个迭代器值啊给你一个operation,然后由它来做。
那我把这个方法又拿出去了,所以呢就会变成这个样子。
在c加加STL中呢,与我这个reduce函数对应的函数呢名字叫cumulate.那它的实际代码呢有两个版本,那这两个版本啊我在文中都给你写了出来。
那其中第一个版本呢,只不过是把well语句换成了for语气。
而第二个版本呢更为抽象,因为我需要传入一个二元操作函数、finanary、 operation OP来做accumulate, accumulate的语义啊,比sam更抽象了。
那这样呢我就可以像文中这样用这个radio参数来获取员工的总薪水和最高薪水了。
呃,需要注意啊,我在这里呢用了c加加的兰姆达表达式,你可以很清楚的看到reduce这个函数呢就更通用了。
具体要干什么样的事情呢?放在匿名函数里面,那它会定义我要做什么?我呢只做一个reduce,那更抽象一点来说呢,我只是把一个数组一个集合变成一个值。
那么怎么变成一个值呢?由这个函数啊来决定我们来看一看啊如何使用reduce和其他的函数,完成一个更为复杂的功能。
再来看文中这一段示例代码,我先定义了一个函数对象counter.那这个函数对象呢需要一个叫count的函数对象,呃,它是一个条件判断函数,如果满足条件呢则加一,否则呢就加零。
然后呢,我用上面的counter函数对象和reduce函数共同来打造一个counter if算法,你可以看到只需要一行代码就可以实现了。
那至于是什么样的条件呢?这属于业务逻辑啊,不是我的流程控制,所以呢这个应该交给使用方。
于是呢当我需要统计薪资超过一万块的员工数量的时候呢,调用这个counter if函数同样只需要一行代码就可以了。
调用reduce的时候呢,可以只对结构体中的某些值做reduce.比如说只对salary大于一万的人做啊,只选出这个里面的值。
那他用reduce啊就可以达到这步。
只要传不同的函数给它啊,你就可以又造出一个新的东西出来。
那说着说着呢,就到了函数式编程。
在函数式编程里面呢,我们可以用很多的像reduce这样的函数来完成更多的像STL里面counter if这样有具体意义的函数。
那关于函数式编程呢,我们会在后面继续具体聊一聊。
好了,这节课到这里呢就结束了。
我们来总结一下今天所讲的内容。
那在这一讲中呢,我们聊到c加加语言是如何通过泛型来解决c语言所遇到的问题。
那其实这里面呢主要就是泛型编程和函数式编程的基本方法相关的细节。
那虽然解决编程语言中类型带来的问题啊,可能有多种方式啊,不一定就是c加加这种方式。
而我之所以从c一和c加加开始,目的呢只是因为c和c加加啊都是比较偏底层的编程语言。
从底层的原理上呢,我们可以更透彻的了解从c到c加的演进,这一过程中带来的编程方式的变化。
这样可以让你看到,在静态类型语言方面,解决泛型编程的一些技术和方法,从而感受到其中的奥妙和原理。
因为形式是多样的,但是呢原理是相通的。
所以呢这个过程啊会非常有助于你更深刻的了解后面会谈到的更多的变成范式。
那文末呢是编程范式游记一系列文章的目录,方便你了解这一系列内容的全貌。