左耳听风_033_32_编程范式游记3_-_类型系统和泛型的本质
你好,我是陈浩网名做尔多house.在前面的课程中呢,我们讨论了从c到c加加的泛型编程方法,并且呢还初步探究了更为抽象的函数式编程。
那就像我在上一讲中所说的,泛型编程的方式呢,并不是只有c加加这一种类型。
我们呢只是通过这个过程了解一下底层静态类型语言的泛型编程原理。
那这样呢能够方便我们继续后面的历程。
是的,除了c加加那样的泛型之外呢,如果你了解其他的编程语言啊,就一定会发现在动态的一项语言,或者是在某些有语法堂知识的语言中呢,那个swap或者search函数的泛型啊,其实可以很简单的就实现了。
比如你甚至可以把swap函数简单的写成b逗号a等于a逗号b那在上一讲后面的reduce函数中呢,我们可以看到在编程世界中呢,我们需要处理好两件事儿。
一呢是编程语言中的类型问题。
二呢是对真实世界中业务代码的抽象、重用和拼装。
所以呢在这一讲中啊,我们还是要继续深入的讨论上面这两个问题,着重讨论一下编程语言中的类型系统和泛型编程的本质。
首先呢我们来谈一谈类型系统在计算机科学中呢,我们通过类型系统来定义如何将编程语言中的数值和表达式归类为许多不同的类型啊,以及如何操作这些类型。
那还有呢这些类型如何相互作用?那类型可以确认一个值或者一组值,有特定的意义和目的。
那一般来说呢编程语言啊会有两种类型,一种呢是内建类型啊,比如int flow,还有叉r等等。
那一种呢是抽象类型,比如struct class,还有function等等。
那抽象类型在程序运行时呢可能不表示为值。
那类型系统在各种语言之间啊都有非常大的不同。
也许呢最主要的差异啊存在于编译时期的语法,还有运行时期的操作实现方式。
那编译器呢可能会使用值的静态类型来最优化所需的存储区。
然后呢选取在数值运算时的最佳算法。
比如呢在许多c编译器中啊,浮点数类型呢就用来表示有三十二个比特位与i triple e七五四规格一致的单净度浮点数。
所以呢在数值运算上,c又应用了浮点数规范啊,比如浮点数、加法、乘法等等。
那类型的约束程度和评估方法影响了语言的类型。
那更进一步讲呢,编程语言可能就类型多态性部分,对每一个类型啊都对应了一个针对于这个类型的算法运算类型,理论研究类型系统。
那尽管实际编程语言的类型系统起源于计算机架构的实际问题,编译器实现还有语言设计。
那程序语言的类型系统呢主要提供这么几个功能。
那第一呢是程序语言的安全性,使用类型呢可以让编器侦测一些代码的错误啊,比如可以识别出一个错误无效的表达式,比如hello world加三这样的不同数据类型间操作的问题。
那相对来说呢,强烈性语言会提供更多的安全性,但是呢并不能保证绝对的安全。
那第二呢是有利于编器的优化。
对于静态类型语言呢,它们的类型声明可以让编器明确的知道程序员的意图。
所以呢编译器就可以利用这一信息啊,做很多代码优化的工作。
比如我们指定一个类型是int.那么编译器啊就会知道这个类型呢会以四个字节的倍数进行对齐。
那编译器呢就可以非常有效的利用更有效率的机器指令。
那第三呢是代码的可读性,有类型的编程语言呢可以让代码更易读,更容易维护。
那代码的语义呢也更清楚,代码模块的接口啊也更丰富和清楚。
那第四呢是抽象化,那类型呢允许程序设计者对程序用较高层次的方式来思考,而不是凡人的低层次实现。
比如我们啊就会使用整形和浮点型来取代底层的字节实现。
那我们呢也可以将字符串设计成一个值,而不是底层字节的数组。
那从高层上来说呢,类型可以用来定义不同模块间的交互协议,比如函数的入参类型和返回类型,从而让接口呢可以更有语义,而且呢不同模块间的数据交换也更为直观和易懂。
但是呢正如前面所说的类型带来的问题呢,就是我们作用于不同类型的代码啊,虽然看起来非常相似,但是由于类型的问题呢,就需要我们根据不同的版本啊写出不同的算法。
那如果要做的泛型呢,就需要涉及比较底层的玩法。
所以呢这个世界啊就出现了两类语言,一类呢是静态类型语言啊,比如CC、加加和java.那另一种呢是动态类型语言,如python、 PRP啊、javascript等等。
我们先来看一段动态类型语言的代码。
首先呢我让一个变量x等于五,然后呢我再让这个x啊等于一个字符串。
Hello,从从这个子子中呢,我们可以看到变量x一开始好像是一个整型,然后呢又成了字符串型。
那如果是在静态类型语言中啊写出这样的代码,那么就会在编译期出错。
而在动态类型的语言中呢,会以类型标记来维持程序所有数值的标记,并且会在运算任何数值之前啊检查标记。
所以呢一个变量的类型是由运行时的解释器来动态标记的那这样呢就可以动态的和底层的计算机指令或者内存布局对应起来。
我们再来看另外一个例子,我首先创建了一个javascriript数组。
然后呢,我在数组的第一项放了一个数字,第二项呢放了一个字符串,第三项呢放了一个字典。
那对于javascript这样的动态语言来说呢,是可以定义出这样的数据结构的。
但是呢对于静态类型的语啊,这种是很难做到的那另外呢在弱类型或者动态类型的语言中呢,我这段代码啊就是首先让x等于五,再让一个变量y等于字符串三十七,再让z呢等于x加y.那这段代码的执行啊会有不确定的结果。
有的语言像visual、 basic啊,给出的结果是四十二。
因为系统会把字符串三十七转换成数字散十七来匹配运算上的直觉。
而有的呢像javascript t语言,给出的结果呢是一个字符串五三七。
那这个是因为系统啊把数字五转换成了字符串五,然后把这两者啊串接起来。
那还有的像python这样的语言呢,则会产生一个运行式错误。
但是呢我们需要清楚的知道,无论哪种程序语言啊,都避免不了一个特定的类型系统,哪怕是那些可以随意改变变量类型的动态类型的语言。
我们在读代码的过程中呢,也需要脑补某个变量在运行时的类型。
所以呢每个语言啊都需要一个类型检查系统。
那静态类型检查呢是在编译器进行语义分析的时候进行的那如果一个语言是强制执行类型规则,也就是说它通常只允许以不丢失信息为前提的自动类型转换,那么呢我们就称这个处理为强类型,反之呢则称为弱类型。
而动态类型检查系统呢,更多的是在运行时期做动态类型的标记和相关的检查。
所以呢动态类型的语言必然要给出一堆。
比如is a ray, is int, is a string或者type of这样的运行时的类型检查函数。
总之呢类型有时候啊是一个有用的事儿,但是有时候呢又是一件很讨厌的事情。
因为类型是对底层内存布局的一个抽象,会让我们的代码要关注于这些非业务逻辑上的东西。
而且呢我们的代码需要在不同类型的数据间做处理。
那如果程序语言的类型检查的过于严格,那么我们写出来的代码就不能那么随意了。
所以呢即便是静态类型的语言啊,也开了一些小后门,比如类型转换呢还有c加加和java运行时期的类型测试啊等等。
那这些小号门啊也会带来相当讨厌的问题。
比如文中之前说过的这段代码,我先让x啊等于一个整数,让y等于一个字符串,再让z呢等于x加y.那这段代码的结果呢可能和你想的完全不一样,比如c源的底层特性。
那这里的z呢会指向一个超过y地址,五个字节的内存地址,相当于指向y字符串指针之后的两个空字符处,静态类型语言的支持者和动态类型自由形式的支持者经常发生争执。
那前者呢就主张在编译时期啊就可以较早发现错误,而且呢还可以增加运行时期的性能。
而后者主张使用更加动态的类型,系统分析代码更为简单,减少出错机会才能更加轻松、快速的编写程序。
那与此相关的是,后者呢还主张考虑到在类型推断的编程语言中呢,通常不需要手动宣告类型,那这部分额外开销也就自动降低了。
在本系列内容的前两篇文章呢,我们用了c一和c加加语言来做泛型编程的示例。
似乎动态类型语言呢就能够比较好的规避不同类型,导致需要出现多个版本代码的问题。
那这样呢可以让我们更好的关注业务。
但是呢我们需要清楚的明白,任何语言啊都有类型系统指动态类型语言是在运行时做类型检查的那动态语言的代码复杂度比较低,并且呢可以更容易关注业务。
那这个结论呢在某些场景下是对的。
但是有些情况下呢,却并不见得。
比如在javascript中啊,我们需要做一个变量转型的函数。
那这个呢就有可能像需要文中这样写上一堆的switch case判断在运行时判断变量的类型。
我相信啊你在动态类型语言的代码中啊,可以看到大量类似于type of这样的类型检查代码。
是的,这个呢就是动态类型带来的另一个问题啊,就是运行时识别。
如果你用过一段时间的动态类型语啊,那一旦代码量比较大了,我们就会发现代码中出现类型问题,而引发整个程序出错的情况实在是太多太多了。
而且这样的出错啊会让整个程序啊都崩溃掉啊,真是太恐怖了。
那这个时候呢我们就很希望提前发现这些类型的问题。
那静态语言的支持者呢就会说编译器能帮我们找到这些问题。
而动态语言的支持者则认为啊,静态语言的编译器啊也无法找到所有的问题,想真正提前找到问题啊,只有通过测试来解决。
那其实啊他们说的都对,那谈完了类型系统呢,我们接下来啊再来聊一聊泛型的本质。
要了解泛型的本质呢就需要了解类型的本质。
那首先呢类型其实是对内存的一种抽象,不同的类型呢会有不同的内存布局和内存分配的策略。
那其次呢不同的类型啊会有不同的操作。
所以呢对于特定的类型也有特定的一组操作。
所以呢要做到泛性,我们需要做这么几件事情。
那第一呢是要标准化掉类型的内存分配释放和访问。
那第二呢是要标准化掉类型的操作,比如比较操作、IO操作,还有复制操作等等。
那第三呢,标准化掉数据容器的操作,比如查找算法、过滤算法、聚合算法等等。
那第四呢我们要标准化掉类型上特有的操作需要有标准化的接口来回调,不同类型的具体操作。
所以呢c加加就动用了非常繁多和复杂的技术来达到泛型编程的目标。
那第一呢它通过类中的构造函数、析构函数、拷贝、构造函数、重载复制、操作符、标准化的类型的内存分配、释放和复制的操作。
那第二呢,它通过重载操作服务可以标准化类型的比较等操作。
那第三呢,它通过IO stream标准化的类型的输入和输出控制。
那第四呢CA加通过模板技术来给不同的类型生成类型专属的代码。
那第五点呢它通过迭代器来标准化数据容器的便利操作。
那第六点呢它通过面向对象的接口依赖,也就是虚函数技术来标准化的特定类型在特定算法上的操作。
最后呢第七个点c加加通过函数式来标准化,对于不同类型的特定操作。
那通过学习c加加呢,我们就可以看到一个比较完整的泛型编程里所涉及的编程范式。
那这些编程范式呢在其他语言中啊都会或多或少的体现着。
比如说JDK五引入的泛型类型啊,就源自c加加的模板。
我在文中呢给你展示了泛型编程,在一九八五年的论文generic programming中的定义。
你也可以打开这篇论文的链接啊,来详细看一看。
那我个人认为呢,泛型编程的本质就是屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者呢更多的关注算法的结构,而不是在算法中处理不同的数据类型。
好了,这节课到这里呢就结束了。
我们最后啊来简单总结一下,在编程语言中呢类型系统的出现主要是对容许混乱的操作加上了严格的限制来避免代码呢以无效的数据使用方式编译和运行。
比如整数运算呢就不可用于字符串指针的操作呢,不可用于整数上等等。
但是呢类型的产生和限制,虽然对底层代码来说是安全的,但是对于更高层次的抽象呢,却产生了一些负面因素。
比如在c加加语言里,为了同时满足静态类型和抽象啊,就导致了模板技术的出现,带来了语言的复杂性。
我们需要清楚的明白,编程语言呢本质上就是在帮助程序员啊屏蔽底层机器代码的实现。
让我们可以更关注于业务逻辑代码。
但是呢编程语言作为机器代码和业务逻辑的粘合层,是让程序员可以控制更多底层的灵活性呢?还是屏蔽掉底层的细节,让程序员呢可以更多的关注于业务逻辑呢,那这个呢是很难两全的事儿。
所以不同的语言呢在设计上都会有相应的取舍。
比如c语言呢就偏向于让程序员可以控制更多的底层细节。
而java和python呢则让程序员可以更多的关注业务功能的实现。
而c加加呢则是两者都想要啊,导致语言在设计上就非常复杂。
文末呢是编程范式游记系列文章的目录,方便你了解这一系列内容的全貌。