什么是编程范式?
本文最后更新于146 天前,其中的信息可能已经过时,如有错误请发送邮件到aichantech@qq.com

编程范式,学术上的定义是:在编写程序时所采用的基本方法和规范。

目前常见的编程范式有:结构化编程、面向对象编程和函数式编程。在我们学习的过程中,一般先学习结构化编程,再接触到面向对象编程和函数式编程。这种渐进的学习过程会让我们产生一个错觉:面向对象编程和函数式编程比结构化编程更强。

这种强的幻觉是出自哪里呢?就拿结构化编程和面向对象编程来说。一方面是由于我们学习编程的过程中能力得到提升,所以到了面向对象时更加游刃有余;另一方面是由于在学习结构化语言时一般接触到的是C语言,而后续学习面向对象时接触的是Java,Java封装的集合类、工具类等的简便让我们误以为是面向对象编程比结构化编程更强。

但是各种编程范式之间没有强弱之分,因为它们的原意不是使程序员的能力得到扩展。事实上,从结构化编程,再到后来的面向对象编程、函数式编程等,这些范式实际上都从某一方面限制和规范了程序员的能力,这种限制和规范才是编程范式的原意。

从结构化编程说起

什么是结构化编程?简单来说,结构化编程代表可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。

为什么是这三种结构呢?这要从结构化编程的出发点讲起。Dijkstra提出采用数学推导的方法,让程序员可以对自己的程序进行推理证明。即程序员可以用代码将一些已证明可用的结构串联起来,只要自行证明额外的代码是正确的,就可以推出整个程序的正确性。

而顺序结构、分支结构和循环结构这三种结构的正确性可证明。下面是证明的简单思路描述:

  1. 顺序结构:枚举法,针对序列中每个输入、跟踪对应输出值的变化。
  2. 分支结构:分支可以看成多个顺序结构的组合,所以可以用枚举分别证明每个分支路线的正确性。
  3. 循环结构:数学归纳法,证明1次和n次的正确性,那么n+1次循环的正确性就可以证得。之后再用枚举法证明循环结构起始和结束条件的正确性。

再回到本文中提到的:

这些范式实际上都从某一方面限制和规范了程序员的能力。

体现在结构化编程中的是,其限制了Goto语句的使用,因为Goto这种不受限制的直接控制转移语句的某些用法会导致某个模块无法被递推分成更小的、可证明的模块。

谈到面向对象编程

我们学习到面向对象编程时,常常会说到要以对象的角度分析系统,以面向对象编程的方式进行实现。那么面向对象编程到底是什么?

谈到这个问题时,由于面试八股文的影响,往往会搬出一些神秘的词语,譬如封装、继承、多态。其隐含意思就是说面向对象编程是这三项的有机组合,或者任何一种支持面向对象的编程语言必须支持这三个特性。

我们接下来可以分析一下这三个特性。

封装

封装指将一组相关联的数据和函数圈起来,圈外的代码只能看到部分函数,数据则完全不可见。这是面向对象编程的特性吗?随着语言的演化(由C到C++,再到Java等),实际上封装性不断的被削弱。在C语言中使用头文件,可以完美的让头文件中的具体实现细节对外部不可见,但在后续C++、Java等语言中,外部可以看到更具体的细节。比如在Java使用构造器时,使用者可以意识到类中的属性,虽然我们可以通过private关键字禁止我们对这些属性的直接访问,但使用者仍然知道了它们的存在。

继承

继承的主要作用是让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖。实际上C程序员早在面向对象编程语言发明前就一直在做了。在C语言中,可以使用数据结构的超集的性质实现继承的功能。

多态

多态是让一个抽象实现可以在不同情况下对应不同的具体实现。面向对象编程中多态的基础来源于C语言,归根结底,多态其实不过是函数指针的一种应用。

#include <stdio.h>

void copy() {
    int c;
    while( (c=getchar()) != EOF )
        putchar(c);
}

观察上面的C语言代码,getchar()从STDIN中读取数据,putchar()将数据写入STDOUT,但是STDIN和STDOUT究竟指代的是哪个设备呢?很显然,这类函数其实就具有多态性,因为它们的行为依赖于STDIN和STDOUT的具体实现类型。

这里的STDIN和STDOUT和Java中的接口类似,但是C语言中是没有接口这个概念的,那么getchar()这个调用的动作如何真正传递到设备驱动程序中,从而读取到具体内容的呢?

其实很简单,UNIX操作系统强制要求每个IO设备都要提供open、close、read、write和seek这5个标准函数,也就是说,每个IO设备驱动程序对这5中函数的实现在函数调用上必须保持一致。

在具体的多态实现上,这种形式实际上是利用了函数指针。getchar()调用的read函数指针最终会指向操作IO设备提供的read标准函数。

这种实现多态的方式弊端很明显,即抽象提供方和实现提供方都要严格遵守函数指针方法的说明书进行编程。注意这里的说明书是类似于需求规格说明书这样的文档类材料,不是接口这样的代码层面的约束。这种人工遵守约定的方式带来了不确定性和危险性。

而面向对象编程中,实现多态的方式是通过接口,具体实现类在实现接口后,会进而要求实现具体的方法。这种方式虽然在多态上并没有理论创新,但它们也确实让多态变得更安全、更便于使用了。

约束性

综上,面向对象编程实际上没有功能的创新性,更多的是对函数指针进行了约束,所以可以认为,面向对象编程其实是对程序间接控制权的转移(函数指针)进行了约束。

最后说到函数式编程

开门见山,函数式编程中的变量是不可变的,变量一旦被初始化后,就不会再改变。

Java中提供了一套蹩脚的函数式编程方案,但是也可以从中窥见函数式编程的思想。

Java的Stream流中规定传入的变量必须是final或是不会发生改变的变量。

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = List.of(1, 2, 3, 4);
        int num = 0;

        List<Integer> result = list.stream().map(i -> {
            i += num;
            return i;
        }).toList();

        System.out.println(result);
    }

在这段代码中,num虽然不是final,但它没发生过改变,所以符合要求。

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = List.of(1, 2, 3, 4);
        int num = 0;
        num+=1;
        List<Integer> result = list.stream().map(i -> {
            i += num;
            return i;
        }).toList();

        System.out.println(result);
    }

如果将代码改成这样,则会报错,因为num被改变了,不是final或不会改变的变量。

这证实了函数式编程中变量不允许改变的思想。

这种思想的好处是什么呢?在高并发中,竞争、死锁以及各种并发更新问题都是由于可变变量导致的。即一切由于使用多线程、多处理器而引起的问题,如果没有可变变量的话都不可能发生。

但在实际开发中,完全的不可变性显然是不可能的,所以需要将应用内部访问切分为可变和不可变两种组件,不可变组件用纯函数式编程实现。而保护可见变量,隔离组件等都不是本文的范畴,故不多赘述了。

总结为一句话:函数式编程对程序中的赋值进行了限制和规范。

总结

将本文总结为以下小点:

  1. 结构化编程:对程序控制权的直接转移进行了限制和规范。
  2. 面向对象编程:对程序控制权的间接转移进行了限制和规范。
  3. 函数式编程:对程序中的赋值进行了限制和规范。

其中所有编程范式都从某一方面限制和规范了程序员的能力。即每个编程范式的目的都是为了设置限制。这些范式主要是为了告诉我们不要做什么,而不是可以做什么

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇