关于编程语言的理解
不再细究某个编程语言的细节,而是从宏观角度来理解编程语言的设计哲学。
部分内容取自网络资料和GPT,仅作为学习整理,不保证正确性,事实上这部分内容在网上的中文资料很混乱,有很多都是错的。
编程范式的历史与演变
简单回顾一下编程范式的历史与演变。
顺序编程
在早期编程中,人们采用的编程方式被称为顺序编程(非结构化编程)——程序按照上下文顺序执行指令,GOTO
语句在其中发挥了重要作用,
它可以无条件地跳转到程序的任意部分。
GOTO
语句的滥用导致了“意大利面条式代码”,使得程序变得难以理解和维护。为了克服GOTO
语句带来的代码混乱,20世纪60年代Edsger
Dijkstra等人提出了结构化编程。
结构化编程
结构化编程的核心是控制结构,例如顺序、选择(如if
、switch
)、循环(如for
、while
)等结构,通过这些控制结构来组织代码,尽可能避免GOTO
语句的使用。控制结构提高了代码的可读性和维护性,是现代编程语言的重要基础。
面向过程编程
面向过程编程是基于函数调用的编程范式,通过一系列过程(函数)的调用来组织代码,C语言是其中的典型代表。 面向过程编程的特点是数据与操作分离,这对于较小规模的程序没有问题,但是对于更大型的项目,就会遇到维护困难:难以通过函数管理复杂的数据和状态。
面向对象编程
为了应对面向过程编程在大型项目中的局限性,20世纪70年代提出了面向对象编程, 面向对象可以将数据和操作数据的方法封装到一起,通过类的继承可以实现代码的复用和扩展,允许不同的子类通过相同接口提供不同的方法。 Java是典型的面向对象语言。
函数式编程
上面的这一套发展脉络可以统称为命令式编程,程序通过执行具体命令来完成对应的功能,与之相对的是函数式编程(或者称为声明式编程,一个比函数式编程更大一点的概念,直接表明需要做什么,除了函数式语言还包括SQL等), 命令式编程和函数式编程是编程思想的两个“极端”:
命令式编程立足于现实世界中的计算模型,即计算机能够做什么。冯诺伊曼结构的计算机本质上通过顺序地执行指令来完成计算,这种执行方式与命令式编程高度契合。Fortran 是最早的命令式编程语言。
函数式编程则立足于我们希望实现的目标。它起初源于数理逻辑、数学证明等形式化推理的需求,其核心在于符号演算和函数组合。这些思想的产生早于现代计算机的发明。Lisp 是最早的函数式编程语言。
函数这个词在编程语言中有多种含义:
- 在命令式编程中的“函数”称为子过程更合适,它是一段命令的封装,目的是减少代码重复,早期会根据有没有返回值来区分函数和子过程,但是后来出现了无返回值的函数类型,就没有必要使用子过程这个特殊概念,统一称为函数。
- 在函数式编程中的“函数”则与数学中的函数和映射更加契合,它代表的是一种对应关系,具有不可变性和无副作用的特点:相同的输入必然得到相同的输出。
函数式编程的难度远大于命令式编程,学习曲线陡峭,应用很少,看起来有点像编译器语法处理中的中间语言,代表是Lisp和Haskell。
注:现在主流的编程语言都是属于命令式编程风格的,但是它们在发展过程中也不断吸收函数式编程的思想,两者之间并不是泾渭分明的。新语言例如rust和Julia在一开始就很大程度上参考了函数式编程的思想,传统语言例如C++则是在发展中不断吸纳函数式编程的语法。
编程模型的历史发展
1. 静态编译(1950s–1960s)
最早的高级编程语言(如 Fortran)采用了静态编译(Static Compilation)模式。 程序员编写源代码后,使用编译器一次性将其翻译成特定平台的机器代码,生成独立的可执行文件。 这种模式充分利用了当时极其有限的计算资源,保证了程序的运行效率。
典型代表:
- Fortran(1957)
- C(1972)
特点:编译生成机器码,运行速度快,但可移植性差。
2. 解释执行(1960s–1970s)
随着计算机应用领域的扩展,对开发效率和跨平台能力的需求上升,解释执行(Interpreted Execution)逐渐出现。 解释器可以直接读取源程序并执行每一条指令,无需预先编译,极大地方便了交互式开发与实验。
典型代表:
- BASIC(1964)
- Lisp(1958, 主要以解释方式实现)
特点:开发方便、交互式,但执行效率较低。
3. 虚拟机模型(1980s–1990s)
为兼顾编译执行的效率与解释执行的灵活性,出现了虚拟机(Virtual Machine)概念。 源代码首先编译成平台无关的中间代码(如字节码),然后在各平台的虚拟机上运行。 虚拟机可以根据目标硬件进行适配,极大地增强了可移植性。
典型代表:
- Smalltalk(1980年代)
- Java(1995,广泛普及虚拟机概念)
特点:一次编写,到处运行(Write Once, Run Anywhere)。
4. 即时编译技术(1990s–2000s)
解释执行与虚拟机执行虽然可移植,但运行效率仍不如本地编译程序。 为解决这一问题,引入了即时编译(Just-In-Time Compilation, JIT)技术:
- 在程序运行过程中,动态监测热点代码,并将其编译为本地机器码,从而大幅提升性能。
典型代表:
- Java HotSpot JVM(1999)
- .NET CLR(2002)
特点:结合了解释的灵活性与编译的高效性。
5. 多样化混合模式(2000s–至今)
现代编程语言往往采用混合执行模型(Hybrid Execution Models),根据不同应用场景灵活选择优化策略。 例如:
- Python:解释执行为主,模块加载时生成字节码(
.pyc
),并发展出 JIT 优化(PyPy)。 - Julia:函数粒度的即时编译(JIT),结合 LLVM 提供接近 C 的性能。
- MATLAB:最初为解释执行,后引入了局部 JIT 加速。
此外,出现了新的执行策略,如:
- AOT(Ahead-of-Time Compilation):运行前预编译热点模块(如 GraalVM、PyInstaller)。
- 增量编译(Incremental Compilation):仅重新编译修改过的部分(如 Rust 的 Cargo)。
特点:强调灵活性、可扩展性、易部署性。
常见语言的模型
C/C++:采用的是标准的静态编译方式,源代码通过编译器(如 gcc、clang)一次性编译为特定平台的机器码,生成可独立运行的可执行文件。运行时不依赖源代码或编译器。现代的发展主要关注静态编译环节,例如预编译加速,链接时优化等。
Python:采用的是标准的解释执行方式(如 CPython 解释器),直接逐行解析并执行源代码。在 REPL 模式或直接运行脚本时,源代码被即时解释执行,无预编译。但是后续引入了各种优化措施,例如引入了字节码虚拟机(CPython VM),当模块被导入时,源文件会编译为字节码(
.pyc
文件),加快后续加载速度。 除了 CPython,还出现了 PyPy(基于 JIT 技术的 Python 解释器),可以大幅提升运行效率。Java:经典模型是字节码编译 + 虚拟机执行,Java 源代码被编译为平台无关的字节码(
.class
文件),由 Java 虚拟机(JVM)负责解释执行或编译执行。现代的发展包括采用即时编译器(JIT Compiler),将热点方法编译为本地机器码。Julia:采用的模型是即时编译,源代码经过解析、类型推断,直接由 LLVM 后端编译为本地机器码执行。
MATLAB:最初采用的模型是解释执行,逐行读取源代码并执行。后续的执行模型已经发生了一些变化,包括引入即时编译器等。
类型系统分类
1. 强类型 vs 弱类型
强类型和弱类型主要体现了编程语言对数据的类型转换的态度:
- 强类型(Strong Typing):
- 定义:强类型语言要求数据在运算时必须符合其类型要求,如果存在类型不匹配的运算,就会抛出错误,不能隐式地转换数据类型。
- 特点:不允许不同类型的数据直接操作,编译时或运行时进行严格的类型检查,减少了类型错误和不一致性。
- 典型语言:Java、Python
- 弱类型(Weak Typing):
- 定义:弱类型语言允许不同类型的数据之间进行隐式转换,可以在不同类型之间进行操作而不会抛出错误,这可能导致预期之外的行为。
- 特点:允许不同类型之间进行隐式转换,灵活性较强,但可能导致错误或意外行为。
- 典型语言:JavaScript
2. 静态类型 vs 动态类型
数据必然有类型,但是变量却未必,静态类型和动态类型主要体现在变量和数据之间的关系:
- 静态类型(Static Typing):(变量严格绑定数据)
- 定义:静态类型语言要求在编译阶段就确定所有变量的类型(显式声明或通过类型推断),变量的类型在程序运行之前就已经确定,编译时进行类型检查。
- 特点:类型在编译时就已确定,如果通过编译就不会在运行时发生类型错误。类型的确定性使得程序可以优化运行效率。
- 典型语言:C/C++、Java、Rust
- 动态类型(Dynamic Typing):(变量可以被理解为数据的引用或指针)
- 定义:动态类型语言的变量的类型是在运行时因为绑定值而确定的,变量的类型是可以动态变化的,由它当时指向的值决定。
- 特点:语法灵活性高,不需要类型声明,适合快速开发。运行时类型检查必然带来性能开销,导致运行效率偏低,而且容易在运行时遇到类型错误。
- 典型语言:Python、JavaScript
常见语言分类
按照这两个维度对常见的编程语言进行分类:
- 强类型静态语言:Java
- 强类型动态语言:Python
- 弱类型静态语言:C/C++
- 弱类型动态语言:JavaScript
注:关于 C++ 是弱类型还是强类型其实是有争议的,本质上是因为强弱类型的定义并不清晰,wiki 上的观点认为现代 C++ 属于强类型。