跳到主要内容

超极长的总论

程序语言是一切计算机程序的载体,可谓是计算机技术的核心。

这个世界上有各种各样的程序语言,本合集将介绍尽可能多的、著名的程序设计语言。了解更多的程序语言(以及背后的哲学、生态)有助于技术选型。选择正确的程序语言让开发事半功倍。

本概述会介绍一些学习程序语言需要注意的东西,同时对程序语言的一些技术概念做铺垫。

应该关注什么?

大多数程序语言都可以完成广泛的编程任务,但各有所长,或者说各有侧重。那么,认识一个新的程序语言,应该关注什么?

语法特性

语法是一个程序语言最独特的东西。一些语言在语法层面可能提供了对某些功能的强化和支持。这样的支持或许可以看作是广义的 “语法糖”。我们来看一个例子吧:

// Cpp
for (int i = 0; i < n; i++) {
cout << arr[i] << endl;
}

这段 C++ 代码遍历数组 arr 并逐个输出。以上这种写法几乎在所有的编程语言中是通用的。然而,有以下语法糖:

// Cpp
for (auto i : arr) {
cout << i << endl;
}
// C#
foreach (var item in arr) {
Console.WriteLine(i);
}

这两种写法(来自不同语言)也可以实现相同的功能,但是更方便一点。这就是语法特性可能提供的好处。

具体的 “语法特性” 分为哪些将在下文中继续探讨。

生态

一般来说,每种程序语言都会有一个 “标准库”,伴随该语言的 SDK 提供。标准库中的功能拆箱即用,不需要安装第三方库。然而标准库的能力毕竟是有限的,人们封装一些高级功能,就形成了第三方库。一个程序语言的第三方库,就是该语言生态的重要部分。

当然,语言生态除了第三方库(是否足够丰富),还有社区支持、某些特定硬件的支持等。但本合集主要关注第三方库的生态。

第三方库生态和标准库是紧密联系的。如果标准库的功能太少,可能造成第三方库生态的碎片化。这是因为一些常用的功能标准库中没有实现,于是各路大神纷纷出手,打造自己的第三方库,百家争鸣,但是很碎片化。

还有一些细枝末节,比如安装和管理第三方包是否足够方便等。

建模能力

这是一个相当抽象的概念。

我们编程的最终目的是为了解决某一具体、现实的问题。那么,为了解决这样的问题,建模是一个必不可少的步骤,是程序设计中的重要一环。

直接给出衡量一个语言的建模能力的方法似乎太难了。请读者阅读后自行感知。


那么下面就谈谈需要关注的具体技术吧。

OOP 能力

面向对象编程(Object Oriented Programming,OOP)是构建现代应用程序的一项相当重要的技术。

OOP 非常适合构建大规模、长生命周期的应用程序。

OS 属于大型程序,但是由于其非常底层,需要用 C 编写,而 C 不支持 OOP。

这是上面那句话的一个例外。

为什么 OOP 如此重要?一定程度上是因为其 “对象” 这样概念的提出,符合人类意识对自然世界的建模,因而在建模现实问题中具有天然优势。

术语

下面简单介绍 OOP 的几个最最基本的术语:

  • 对象(Object):对象是 OOP 的基本单元,是 “数据” 和 “行为” 的结合体。简单理解一下,数据就是 “此对象是什么”,行为就是 “此对象能做什么”,或者 “此对象能做什么”。在程序语言中,“数据” 对应的术语是 “字段”,“行为” 对应的术语是 “方法”。
  • 类(Class):每个对象都有其所对应的类。类可以看作是对象的类型。类中定义了对象的成员,成员包括字段、方法等。类就像一个模板。我们按照这个模板来构建对象。构建对象的过程叫做 “实例化”。
  • 字段(Field):在程序语言中,字段就是变量(或常量)。字段是对象的属性或特征(此处的 “属性” 是广义概念,不特指某些语言的 “属性” 特性)。
  • 方法(Method):在程序语言中,方法就是函数,一个可以执行、调用的东西。方法是对象可以做出的行为,或者可以对对象做出的行为。

本文不会详细介绍 OOP。如果你从来没有了解过,请自行学习。

OOP 的四大基本特性是:

  • 封装(Encapsulation):有时也称为 “包装”。
  • 继承(Inheritance):子类继承自父类,父类派生(Derivation)出子类。子类又叫派生类,父类又叫基类(Super-class / Base-class)。
  • 多态(Polymorphism)。
  • 抽象(Abstraction):通常通过 “接口”(Interface)实现。

语言的差异

当然,不同语言对 OOP 的支持是有差异的,不同语言支持的(较高级的) OOP 能力不同;有的语言不支持 OOP(如 C)。这是因为要实现 OOP 是往往是有一定的性能开销的,这取决于语言本身如何实现 OOP。例如 C++ 的 “零开销抽象” 设计哲学。

OOP 在不同语言的语法也有细微的不同,这些细微差别导致了 OOP 体验的差别。例如 C++ 中需要使用 StaticClass::method() 来调用静态类的成员,我觉得写 :: 就很麻烦(也难看)。而 Java 等多用 StaticClass.method(),这个 . 就温和得多。

上面说的 OOP 四大基本特性,需要聚焦关注的是继承抽象。不同语言 OOP 能力的差异主要集中在此两点上。

跨平台能力 / 构建方式

无论什么程序语言,最终都要 “翻译” 成机器码才能在计算机上运行。

然而,何时 “翻译”、如何 “翻译” 在不同语言中是存在差异的。这就导致了不同语言跨平台能力 / 构建方式的不同。

术语

文件或环境的术语:

  • 源代码(Source code):用(高级)程序语言编写。程序员使用某(高级)语言,就是写这种语言的源代码。源代码被设计成是对人类可读的。
  • 汇编代码(Assembly code):具体的机器指令,相当于直接操作 CPU。汇编代码虽然也是文本,可以阅读,但是可读性差非常非常多。汇编代码用汇编语言写成,这是一种低级程序语言,几乎没有人会直接用汇编语言写程序。
  • 机器码(Machine code):也是机器指令,不过是二进制文件,不可以直接阅读。机器码约等于平常说的 “可执行文件”。
  • 中间语言(Intermediate Language,IL):一些程序语言为了支持跨平台,构建时会把源代码编译成中间语言,而不是汇编代码或机器码。中间语言不能直接运行。
  • 运行时(Runtime):为了运行一些高级语言需要的环境,有时也指托管环境(见下一小节)。“运行时” 这三个字在中文中歧义太严重了(可以作状语),因而本合集中尽量用 “Runtime” 来指这一概念。
  • 平台(Platform):程序运行的 OS 和硬件架构(如 CPU 架构)的组合概念。例如:Windows on x64

过程术语:

  • 编译(Compilation):把源代码翻译成汇编代码的过程,与之相反的是反编译(Decompile)。反编译(几乎)不能得到原始的源代码,但可以得到 “功能相同” 的源代码。大多数软件都不欢迎反编译。**注意:**有些工具中 “编译” 这一行为选项中也包含了汇编的步骤,即编译和汇编一起完成。
  • 汇编(Assembly):把汇编代码翻译成机器码的过程,与之相反的是反汇编(Disassembly)。
  • 即时编译(Just-In-Time compilation,JIT):程序运行过程中把 IL 翻译成具体的机器码的过程,这是实现跨平台的一种方法。这一过程可能包括针对特定机器的优化,但是会增加程序启动时间。
  • 构建(Build):泛指从源代码构建出成品的过程。

跨平台的实现

主要有以下方式:

  • 编译成中间代码并发布:例如 Java 的 .jar 包。中间代码需要特定的 “虚拟机”(例如 JVM)来运行。
  • 使用同样的源代码,编译成适用于平台的程序并发布。
    • 可以编译成原生机器码,这种发布方式又叫 AOT(Ahead-Of-Time compliation)编译。这样的程序可以独立地运行,不需要 Runtime / 托管环境(这会使程序的发布包体积更大)。AOT 编译的程序无法使用反射。(见下一小节)
    • 编译成机器码,但需要安装 Runtime。即:使用一样的程序机器码,但是为不同的机器发布不同的 Runtime,以此实现一定程度上的跨平台。
  • 脚本型语言(如 Python)在不同平台使用不同的解释器。

跨平台的代价

跨平台的程序,其性能通常比原生编译的程序更劣,或者有更长的启动时间。

跨平台程序不能直接访问某一平台具体的 API。这就使得其无法完成高度底层的任务(例如硬件驱动)。

原生语言 / 托管语言

这一小节要介绍的内容实际上和上一小节密切相关。

原生语言指的是编译成原生程序的语言,其往往提供更底层的 API 调用、内存访问控制(如指针)等。这类语言往往需要手动管理内存的分配、释放。原生语言的运行速度、内存占用、启动时间等往往更优;但是由于手动管理内存比较麻烦,用原生语言构建大型程序会相对繁琐,需要编码者的语言造诣。

托管语言指的是运行在托管环境的语言,其往往搭配自动的**垃圾回收(Garbage Collection,GC)**机制,能够自动释放内存,减轻了程序员的负担。托管环境的引入,相当于在程序和 OS 之间加了一层,这会带来一定的性能开销。

反射

反射(Reflection)是托管环境提供的额外功能。

反射(Reflection)是一个强大的特性,它允许程序在运行时查询、访问和修改类、接口、字段和方法的信息。反射提供了一种动态地操作类的能力。

摘自:Java 反射(Reflection) | 菜鸟教程

反射还可以实现依赖注入(Dependency Injection,DI),这是一种更加减轻程序员管理内存的技术框架。

综上不难看出,托管环境的引入给内存管理带来了不少好处。

并发、异步编程

并发和异步编程是提高程序效率、处理大规模数据的重要手段。

这两者往往都要依赖于程序的多线程能力,所以我把它们归到一类中。

术语

  • 并发(Concurrency):使程序同时处理多个任务的技术总称。
  • 并行(Parallelism):实现并发的一种具体技术,使程序物理意义上地同时处理多个任务。
  • 异步(Asynchronous):不阻塞地完成任务,主要用于 I/O。例如请求网络资源,我们可以异步等待请求完成,此期间程序不会阻塞,可以完成别的任务。

并发模式

并发不一定要依赖于多线程,单线程也可以实现并发,比如 JavaScript。不同语言对并发的支持程度不同,这会在具体章节探讨。

别的什么

技术债(历史包袱)

技术债是一个很现实的问题。一般来说,古老的语言(如 C/C++)会有比较多的技术债,因为它们需要向下兼容。

语言本身的技术债,会使得编写和学习变得比较复杂,编码体验下降,还有可能导致语言功能的缺失(例如 C++ 异常处理)。

类型系统

程序语言大致可以分为:强类型语言、弱类型语言。

强类型语言的所有变量,其类型在编译时确认,而且不能隐式转换为别的类型。例如:

// Cpp (强类型语言)
int a = 1;
a = "Hello"; // 非法,不能通过编译
# Python (弱类型语言)
a = 1
a = "Hello" # 没有问题

Ending

这个超级长的总论到这里就结束了。现在请查看集合中的具体章节,探索程序语言的海洋吧。