如何选择编程语言(一)

如何选择编程语言(一)
0

“PHP是最好的语言!”“人生苦短,我用 Python!”“JavaScript 一统天下”……关于编程语言的讨论和争论时不时就在我们眼前出现。

其实没有一种编程语言在所有领域(操作系统、硬件驱动、办公软件、游戏、Web 服务器、数据库、搜索引擎、大数据处理、高频交易、机器学习、数值计算等等)都是银弹。我们要学会根据应用场景选择最适合的编程语言。

那么我们如何根据应用场景和编程语言的特性来选择适合的编程语言呢?我认为可以考察编程语言的以下特点和特性:

1. 执行方式

1.1. 汇编执行型

汇编语言(assembly)的源文件由汇编器(assembler)转换为 CPU 可直接执行的二进制程序文件。

1.2. 编译、(汇编)、执行型

C / C++ / Fortran / Pascal / Golang 等编程语言的源文件一般由编译器(compiler)先编译为汇编指令,再由汇编器生成 CPU 可直接执行的二进制程序文件。

现在编译器一般不会把编译出来的汇编指令输出到文件,而通常在内存中直接交给内置的汇编器进行处理,所以我们会看到这些编程语言的编译器直接就输出一个可执行的程序文件。

C / C++ 的编译器 GCC / G++ 和编译器后端 LLVM 都可以把编译出来的汇编指令输出到文件中,供我们查看。

1.3. 编译、解释执行型

Java / Scala 源文件由 Java 编译器编译成二进制的 class 文件,由 Java 虚拟机(Java virtual machine ,简称 JVM)解释执行。C# / VB.net 源文件由编译器编译为二进制的 exe 或 dll 文件,由 .net 运行时(runtime)程序解释执行。

1.4. 解释、执行型

解释执行型语言通常又被称为脚本(script)语言。例如 Bash 脚本、Powershell脚本、JavaScript、Python、PHP、Ruby、Matlab 脚本、Mathematica 脚本等。它们的源文件由相应的运行时程序直接读取并解释执行。

1.5. 编译转换、解释执行型语言

TypeScript、JSX、CoffeeScript 等语言通常是先由编译转换程序转换为 JavaScript,再由 JavaScript 运行时解释执行。

1.6. 其他类型

例如 Kotlin 即可以被编译为 Java 的 class文件由 JVM 解释执行,也可以被编译转换为 JavaScript ,还可以被编译为可执行程序直接被 CPU 执行。

1.7. 不同执行方式的执行效率

Java 的 class 文件中和 .net 的二进制文件中的指令都是类似 CPU 指令的代码,各硬件平台上的虚拟机 / 运行时程序只需花很少的代价就可以将其转换成该平台上的 CPU 指令执行,而不需要再对源代码进行词法语法分析。因此这些编译、解释执行型语言的运行效率通常比下面要介绍的解释执行型语言要高。

但是编译、解释执行型语言又比汇编 / 编译执行型语言多一个将虚拟机指令转换为 CPU 指令的过程,所以它们运行效率通常又比汇编 / 编译执行型语言的低。

因此,在对执行效率要求高的系统(例如高频交易的系统、核心网络的路由器中的路由程序)中,通常不采用解释执行型语言,而是采用编译执行型语言来开发。

1.8. 在系统级编程方面的适用性

由于编译解释型语言或者脚本语言的运行效率不如编译执行型语言,而且需要虚拟机或者解释器才能运行,因此在操作系统或者驱动程序的编程中通常使用的是编译执行型的语言,如 C / C++。

而在与 CPU 架构密切相关的地方,例如操作系统的内存管理模块的设置页表地址的程序,或者需要使用 CPU 的 IO / 中断等指令的时候,基本上只能使用汇编语言。

2. 内存管理

2.1. 手动管理内存

汇编语言以及 C / C++、Fortran、Pascal 等许多编译执行型语言都需要手动管理内存,由程序员手动回收无用的数据占用的内存。

2.2. 自动管理内存

编译执行型语言中的 Golang,Java、C#、VB.net 等绝大多数编译、解释执行型语言,还有 JavaScript、Python、PHP 等绝大多数脚本语言都是由运行环境自动管理内存(增加作为对象池的堆的内存、无用对象的垃圾回收等)。

其中 Golang 主要采用标记清除算法做垃圾回收(garbage collection)。而 Java 可以指定标记清除、标记整理、标记复制与标记清除结合的分代 GC 算法等等算法中的一种做垃圾回收。对于其他语言的 GC 算法,我没了解过,读者有兴趣可以自己查阅资料。

2.3. 自动管理内存的优劣

2.3.1. 优点

把程序员从释放无用内存或者管理内存池等逻辑中解放出来,降低内存泄漏的可能性。

2.3.2. 缺点

程序员通常无法控制垃圾回收的时机,而垃圾回收通常需要暂停程序的运行,这会导致无法预料的程序卡顿。

3. 并行 / 并发模型

首先要说明的是,并行和并发并不是一样的概念,它们的区别可以参考这个知乎问题。并行的一定是并发的,并发的不一定是并行的。

3.1. 多线程并行

汇编语言可以直接调用操作系统的 API 来实现多线程。

C / C++ 可以通过 pthread 库或者直接调用操作系统的 Api 来实现多线程。

Java 可以调用 Java API 来实现多线程,C# / VB.net 则可以调用 .net 的 API 来实现多线程。

Python 也可以通过其官方 API 实现多线程。虽然最普遍使用的 Python 运行时 CPython 和 PyPy 目前由于全局解释器锁(GIL)的缘故使不同线程不能在多个 CPU 核心(core)中并行运行,但还是有某些 Python 运行时如 Jython 已去掉了 GIL。

JavaScript 在支持 worker API 的运行时程序上可以通过创建 worker 来实现多线程,但这些线程之间只能通过消息通讯,不能共享内存。

3.2. 用户态线程 / 协程并发

Python 可以通过生成器、asyncio 库或者 async / await 语法来使用协程(coroutine)。

Golang 可以通过 goroutine 来使用协程。Gorotine 是可以并行的,可以充分利用多个 CPU 核心。

Kotlin 可以通过 kotlinx.coroutine 包提供的 API 来使用协程。

3.3. 非阻塞 IO API + 内置事件循环并发

JavaScript 运行时通过提供非阻塞的 IO API 以及内部实现事件循环的方式,使得 JavaScript 可以在等待 IO 返回数据的同时可以并发地运行其他处理逻辑。

3.4. 多进程并行

PHP 目前没有语言上的特性或者自带的 API 可以实现多线程或协程,要实现多线程需要额外安装 pthreads 拓展或者 Swoole 拓展。通常 PHP 的并发是通过多进程来实现的。

如果使用有 GIL 的 Python 运行时程序、Ruby 运行时程序或者不支持 worker API 的 JavaScript 运行时程序(例如版本 10 以前的 Node.js),如果想要充分利用多个 CPU 核心,也需要通过多进程来实现并行。

3. 强类型 VS 弱类型

3.1. 强类型

通常在需要编译的语言(如 C / C++ / Java / C# / Kotlin 等语言)中,一个变量 / 对象的属性必须在声明之后才能使用(一个变量必须被声明后才能被读,必须被声明后或者在声明时才能写;一个对象的属性没被定义的话不能被写入),而且一个变量 / 对象的属性 / 函数的入参与返回值的数据类型在声明时就确定下来、不能再改变了。如果违反了这两个约束,那么在编译源代码的时候就会报错。

3.2. 弱类型

通常在脚本语言(如 Python、JavaScript、PHP)中,一个对象的属性是可以动态变化的,甚至变量不需要声明就可以使用(读或者写);一个变量 / 对象的属性 / 函数的入参与返回值的数据类型是不固定的。

3.3. 强弱结合

在 TypeScript 和在版本 7 以后的 PHP 等语言之中,既可以使用动态类型,也可以规定某些变量 / 对象的属性 / 函数的返回值的数据类型。如果违反了规定的数据类型,会引起编译转换器 / 解释器报错。

3.4. 不适用

在汇编语言中通常没有变量(不考虑预处理器),因此不存在静态类型或者动态类型的问题。

3.5. 弱类型的优劣

3.5.1. 优点

初学者不需理解数据类型就可入门,进行简单的编程。

程序员不受数据类型的限制,可以采取更灵活的实现方式,例如在某个类的对象中增加一个属性来存储一些额外的数据,而不需要改变类的定义。

程序变更前后的兼容性比较好。例如一个类增减了成员属性,旧类的实例对象序列化后,反序列化为新类的实例对象时,强类型语言可能会因字段不一致而出错,而弱类型语言则通常不会出错。又例如 JavaScript 中某个函数增加一个默认为空的入参,调用方不需修改即可兼容。

3.5.2. 缺点

弱类型语言不能在编译期确定变量的数据类型,从而不能在编译期对一些变量的内存使用进行优化(例如内存页对齐、缓存对齐等)。

弱类型语言在运行时可能出现由于类型混乱导致的错误。例如错把数字变量比较的规则应用于字符串变量的比较,从而得到 “10” < “2”。

弱类型语言中,如果一些变量、对象属性、函数的入参或返回值使用的数据类型过多,会降低代码的可读性和可维护性,甚至导致程序代码混乱而出错。

2赞