200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 编程的本质--深入理解类型系统/泛型/函数式编程/面向对象编程

编程的本质--深入理解类型系统/泛型/函数式编程/面向对象编程

时间:2023-06-03 10:40:30

相关推荐

编程的本质--深入理解类型系统/泛型/函数式编程/面向对象编程

编程语言发展到今天,出现了好多不同的代码编写方式,但不同的方式解决的都是同一个问题,那就是如何写出更为通用、更具可重用性的代码或模块。

1. C语言

C 语言历史悠久,而几乎现在看到的所有编程语言都是以 C 语言为基础来拓展的,不管是 C++、Java、C#、Go、Python、PHP、Perl、JavaScript、Lua,还是 Shell。

自 C 语言问世 40 多年以来,其影响了太多太多的编程语言,到现在还一直被广泛使用,不得不佩服它的生命力。但是,我们也要清楚地知道,大多数 C Like 编程语言其实都是在改善 C 语言带来的问题。

1.1 C语言的特性

C 语言是一个静态弱类型语言,在使用变量时需要声明变量类型,但是类型间可以有隐式转换;不同的变量类型可以用结构体(struct)组合在一起,以此来声明新的数据类型;C 语言可以用 typedef 关键字来定义类型的别名,以此来达到变量类型的抽象;C 语言是一个有结构化程序设计、具有变量作用域以及递归功能的过程式语言;C 语言传递参数一般是以值传递,也可以传递指针;通过指针,C 语言可以容易地对内存进行低级控制,然而这加大了编程复杂度;编译预处理让 C 语言的编译更具有弹性,比如跨平台。

C 语言的这些特性,可以让程序员在微观层面写出非常精细和精确的编程操作,让程序员可以在底层和系统细节上非常自由、灵活和精准地控制代码。

1.2 C语言的问题

如果说,程序 = 算法 + 数据,我觉得 C 语言会有这几个问题:

一个通用的算法,需要对所处理的数据的数据类型进行适配。但在适配数据类型的过程中,C 语言只能使用 void* 或 宏替换的方式,这两种方式导致了类型过于宽松,并带来很多其它问题。适配数据类型,需要 C 语言在泛型中加入一个类型的 size,这是因为我们识别不了被泛型后的数据类型,而 C 语言没有运行时的类型识别,所以,只能将这个工作抛给调用泛型算法的程序员来做了。算法其实是在操作数据结构,而数据则是放到数据结构中的,所以,真正的泛型除了适配数据类型外,还要适配数据结构,最后这个事情导致泛型算法的复杂急剧上升。比如容器内存的分配和释放,不同的数据体可能有非常不一样的内存分配和释放模型;再比如对象之间的复制,要把它存进来我需要有一个复制,这其中又涉及到是深拷贝,还是浅拷贝。最后,在实现泛型算法的时候,你会发现自己在纠结哪些东西应该抛给调用者处理,哪些又是可以封装起来。如何平衡和选择,并没有定论,也不好解决。

1.3 C代码示例

int search(void* a, size_t size, void* target, size_t elem_size, int(*cmpFn)(void*, void*) ){for(int i=0; i<size; i++) {if ( cmpFn (a + elem_size * i, target) == 0 ) {return i;}}return -1;}

2. c++

2.1 C++针对C的改进

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

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

2.2 C++的特性

算法应是和数据结构以及类型无关的,各种特殊的数据类型理应做好自己分内的工作,算法只关心一个标准的实现。

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

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

C++是如何做到的?

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

类里面会有构造函数、析构函数表示这个类的分配和释放。还有它的拷贝构造函数,表示了对内存的复制。还有重载操作符,像我们要去比较大于、等于、不等于。这样可以让一个用户自定义的数据类型和内建的那些数据类型就很一致了。第二,通过模板达到类型和算法的妥协。

模板有点像 DSL,模板的特化会根据使用者的类型在编译时期生成那个模板的代码。模板可以通过一个虚拟类型来做类型绑定,这样不会导致类型转换时的问题。模板很好地取代了 C 时代宏定义带来的问题。第三,通过虚函数和运行时类型识别。

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

2.3 C++代码示例

C代码

int search(void* a, size_t size, void* target, size_t elem_size, int(*cmpFn)(void*, void*) ){for(int i=0; i<size; i++) {if ( cmpFn (a + elem_size * i, target) == 0 ) {return i;}}return -1;}

C++代码

template<typename T, typename Iter>Iter search(Iter pStart, Iter pEnd, T target) {for(Iter p = pStart; p != pEnd; p++) {if ( *p == target ) return p;}return NULL;}

使用typename T抽象了数据结构中存储数据的类型。使用typename Iter,这是不同的数据结构需要自己实现的“迭代器”,这样也就抽象掉了不同类型的数据结构。然后,我们对数据容器的遍历使用了Iter中的++方法,这是数据容器需要重载的操作符,这样通过操作符重载也就泛型掉了遍历。在函数的入参上使用了pStart和pEnd来表示遍历的起止。使用*Iter来取得这个“指针”的内容。这也是通过重载 * 取值操作符来达到的泛型。

3. 类型系统和泛型的本质

无论哪种程序语言,都避免不了一个特定的类型系统。

3.1 静态类型语言vs动态类型语言

类型带来的问题就是我们作用于不同类型的代码,虽然长得非常相似,但是由于类型的问题需要根据不同版本写出不同的算法,如果要做到泛型,就需要涉及比较底层的玩法。

对此,这个世界出现了两类语言,

一类是静态类型语言,如 C、C++、Java

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

一种是动态类型语言,如 Python、PHP、JavaScript 等。

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

3.2 类型的本质

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

3.3 类型的泛型化

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

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

3.4 C++的类型泛型化

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

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

3.5 泛型编程的本质

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

4. 函数式编程

C++ 很大程度上解决了 C 语言中的各种问题和不便,尤其是通过类、模板、虚函数和运行时识别等解决了 C 语言的泛型编程问题。

然而,如何做更为抽象的泛型呢?答案就是函数式编程(Functional Programming)。

4.1 数学函数

函数式编程,它的理念就来自于数学中的代数。

f(x)=5x^2+4x+3g(x)=2f(x)+5=10x^2+8x+11h(x)=f(x)+g(x)=15x^2+12x+14

通过函数式编程,我们可以更容易理解递归的概念

f(x)=f(x-1)+f(x-2)

对于函数式编程来说,它只关心定义输入数据和输出数据相关的关系。

4.2 函数式编程特点

stateless:函数不维护任何状态。

函数式编程的核心精神是 stateless,简而言之就是它不能存在状态,打个比方,你给我数据我处理完扔出来。里面的数据是不变的。immutable:输入数据是不能动的。

动了输入数据就有危险,所以要返回新的数据集。

4.3 函数式编程优势

没有状态就没有伤害。并行执行无伤害。Copy-Paste 重构代码无伤害。函数的执行没有顺序上的问题。

4.4 函数式编程劣势

数据复制比较严重。

4.5 函数式编程用到的一些技术

first class function(头等函数) :这个技术可以让你的函数就像变量一样来使用。也就是说,你的函数可以像变量一样被创建、修改,并当成变量一样传递、返回,或是在函数中嵌套函数。map & reduce :这个技术不用多说了,函数式编程最常见的技术就是对一个集合做 Map 和 Reduce 操作。这比起过程式的语言来说,在代码上要更容易阅读。(传统过程式的语言需要使用 for/while 循环,然后在各种变量中把数据倒过来倒过去的)pipeline(管道):这个技术的意思是,将函数实例成一个一个的 action,然后将一组 action 放到一个数组或是列表中,再把数据传给这个 action list,数据就像一个 pipeline 一样顺序地被各个函数所操作,最终得到我们想要的结果。recursing(递归) :递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。

4.6 函数是编程思维-声明式编程

函数式编程关注的是:describe what to do, rather than how to do it。

于是,我们把以前的过程式编程范式叫做 Imperative Programming – 指令式编程。

把函数式编程范式叫做 Declarative Programming – 声明式编程。

即使使用非函数式语言,函数式编程的思维也有助于我们编写更好看的代码,更灵活的代码。

比如函数不维护任何状态比如输入数据是不能动的

这两个特点如果应用在过程式语言的方法里,我们的方法就可以随便复制粘贴调用。

5. 面向对象编程

几乎所有的主流语言都支持面向对象,比如:Common Lisp、Python、C++、Objective-C、Smalltalk、Delphi、Java、Swift、C#、Perl、Ruby 与 PHP 等。

5.1 面向对象的概念

传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。

面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。

目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。

5.2 设计模式(两个核心理念)

面向接口而非实现(“Program to an ‘interface’, not an ‘implementation’.”) 使用者不需要知道数据类型、结构、算法的细节。使用者不需要知道实现细节,只需要知道提供的接口。 钟情组合而非集成(“Favor ‘object composition’ over ‘class inheritance’.”) 继承需要给子类暴露一些父类的设计和实现细节。父类实现的改变会造成子类也需要改变。我们以为继承主要是为了代码重用,但实际上在子类中需要重新实现很多父类的方法。

5.3 面向对象的优点

能和真实的世界交相辉映,符合人的直觉。强调于“名词”而不是“动词”,更多地关注对象和对象间的接口。根据业务的特征形成一个个高内聚的对象,有效地分离了抽象和具体实现,增强了可重用性和可扩展性。拥有大量非常优秀的设计原则和设计模式。

5.4 面向对象的缺点

代码需要通过对象来达到抽象的效果,导致了相当厚重的“代码粘合层”。因为太多的封装以及对状态的鼓励,导致了大量不透明并在并发下出现很多问题。大量的封装,完全不知道里面在干什么事情。而且封装屏蔽了细节,具体发生啥事你还不知道。

6. 编程的本质

6.1 程序=逻辑+控制+数据

Programs = Algorithms + Data StructuresAlgorithm = Logic + Control

所有的语言或编程范式都在解决上面这两个公式。

Control 是可以标准化的。比如:遍历数据、查找数据、多线程、并发、异步等,都是可以标准化的。因为 Control 需要处理数据,所以标准化 Control,需要标准化 Data Structure,我们可以通过泛型编程来解决这个事。而 Control 还要处理用户的业务逻辑,即 Logic。所以,我们可以通过标准化接口 / 协议来实现。

6.2 代码复杂度

绝大多数程序复杂混乱的根本原因:业务逻辑与控制逻辑的耦合。

有效地分离 Logic、Control 和 Data 是写出好程序的关键所在!

参考

左耳听风–编程范式 (11讲)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。