C/C++、Java、C#、PHP、Python和JavaScript等编程语言浅谈及比较

  代码写到现在也已经接触过很多种编程语言了,不同的编程语言有不同的特点,也各自适合于不同的项目,这里就根据自己的理解来谈一谈我接触过的几种编程语言以及对它们的比较。

0x01 C/C++

  C语言我相信是有最多大学生学过的语言,因为目前来说,所有的理科或工科大学生的必修计算机课程还是以C语言作为教学。虽然有很多学生觉得学了以后根本不会用到,但是我觉得选择C语言进行教学还是很正确的。非计算机的理科专业还有大部分的工科专业所需要的编程工作主要是仿真模拟一类。这些工作主要都主要以数值计算为主,也就是需要CPU密集型程序,因此对代码的效率是十分看重的。

  C/C++、Fortran这类静态编译语言会直接将代码编译成二进制的机器指令。它们不像C#、Java那样先编译出一种中间语言(字节码)再利用解释器以及即时(JIT, just-in-time)编译器运行,也不像Python、JavaScript那样直接将脚本代码解释运行。编译运行的效率是解释运行根本无法企及的,不过随着技术的发展,即时编译已经可以达到和静态编译差不多的计算效率了,因此许多脚本语言也都使用了JIT技术来加快运行效率,比如Python的PyPy以及JavaScript的Chrome V8引擎。不过这种数值计算的场景中即时编译并不能发挥出它相比静态编译的优势,因此数值计算的程序大多还是会以静态编译的语言为主。

  另一方面,也是因为C/C++以及Fortran在科学计算中的流行,有很多软件及硬件支持能够让我们用这些语言便捷地开发出更加高效的程序。比如通过OpenMP(需要编译器支持,仅C/C++及Fortran可用)以及MPI规范(可在任何语言中实现)来实现并行计算,或者使用CUDA来进行程序的GPU加速。这些技术使编程人员不用关心具体的实现,只需专注于程序本身的运算逻辑就可以编写出高效的数值程序。

  C/C++编写的程序固然有着极高的运行效率,但是考虑到开发效率,并不是所有的应用场景都适合用C/C++开发,许多场景下都有其他既便捷又不会损失太多性能的编程语言可供选择。比如利用.NET Framework(C#)开发Windows应用程序,或者利用Electron编写JavaScript开发跨平台的桌面程序等等。对于客户端程序来说,除去Windows、Linux这些系统层面的软件(主要是驱动程序一类),依旧适合使用C/C++开发的基本上都是显示绘图、视频渲染、编码解码、PC游戏、游戏引擎和机器学习等这一类CPU密集型程序了。至于服务端软件,目前也就大型游戏服务端、交易所撮合引擎、数据库等这些对计算性能要求非常高的程序还需要用C/C++来开发了。其他的应用场景下,C/C++不但开发成本高,一有点错误就会整个程序崩溃掉,并且还难以debug,与其他语言相比并没有什么优势。以动态页面技术为例,如今几乎已经没有人再利用CGI编写C/C++去实现动态页面了。

  此外C/C++虽然Linux下可以借助apt或yum等包管理工具实现依赖管理,但是这些工具都需要root权限才能够使用,并且容易因为依赖库的版本问题干扰到系统中其他程序。如果没有root权限就需要手动处理所有的依赖库了,这通常是一件非常麻烦的事情。再加上十多二十年前C/C++盛行的时候开源环境还不像今天这般,对社区产品的质量普遍不信任,不像Java、Python以及Node.js那样有公认且流行的库实现。种种这些导致关于C/C++开发出现一个梗,C/C++程序员就是在不断地造轮子。以一个服务端程序为例,socket的读写要造个轮子,序列化工具要造个轮子,数据库要造个轮子……不断地在造轮子。不过这样也有一定好处,比如避免了引入第三方库引发的许可证问题。

  由于一般操作系统都是C语言开发的,所以这些操作系统要么自带C语言的编译器(比如Linux),要么可以在其他相同的系统上编译好C程序拷贝过来直接运行(比如Windows NT),因此C程序不需要安装其他任何软件或者环境即可直接运行。其他语言比如Java需要JVM,C#需要CLR,Python需要CPython,JavaScript需要Node.js,PHP需要Zend Engine,都需要系统上有相应的环境才能够运行相应的代码。这在某些特定情况下也是C/C++语言的一个优势所在。

0x02 Java

  Java和C++都是面向对象的编程语言,要说它们之间最大的区别应该算编译运行方式了,Java是先将源代码编译成Java字节码然后运行在Java虚拟机上,在虚拟机中对Java字节码进行解释运行,并且用到了即时编译技术提高计算效率,而C++是使用静态编译器直接将源代码编译成目标机器的机器码。Java这种编译运行方式的优点,除了众所周知的跨平台特性以及垃圾回收机制外,还有即时编译相比静态编译在某些情况下更好的优化效果。对于一个面向接口编写的程序(这在Java编程中非常常见,Java的接口对应到C++就是虚函数),即时编译可以在运行时根据具体的实例进行编译优化,而静态编译对于这种情况是根本没有办法进行优化的。这也是为什么Java中大部分程序都是面向接口编写,而C++中却不流行这种编程方式的原因了。Java的虚拟机运行方式也带来了许多非常有用的特性,比如反射、注解、动态代理等等。这些特性使得我们能够面向切面编程,即使代码不预留hook,也可以很容易地在不改变目标源文件的情况下插入代码。这在对引入的第三方库代码不够满意时尤为有用,如果C/C++遇到这种问题只能自己去修改第三方库的源码,但是之后第三方库的版本更新又成了件麻烦事。而Java利用反射和动态代理可以做到不侵入第三方库代码,将需要插入的代码写在自己的代码库中,利用切面来实现定制化功能,这样既实现了功能又可以很容易地进行第三方库的版本更新。

  Java的设计也并不是完美无缺的,我个人认为其不支持运算符重载是个遗憾。这就导致用到BigInteger或者BigDecimal进行运算的代码,如金融计算及加解密之类,会十分繁琐且丑陋。不过我也在网上看到个说法:“运算符重载对于数值算法的编程来说有很大意义,但是对于非数值算法就用处没有那么大了。对于非数值算法,其中的操作往往不能直观上看成是加法乘法之类,所以直接定义新的二元函数即可,无需重载原有的运算符。”这话也有一定道理,Java主流的应用场景并不是数值计算一类,因此有无运算符重载影响并不是特别大,不过是不是从另一个角度可以认为,因为Java不能够运算符重载语法不够简洁,所以才导致它在数值计算领域不流行呢?此外Java的泛型实现方式也是一大缺陷,具体在后面C#内容中讨论。

  说到Java的主流应用场景,其中之一必是服务端程序开发,如果不是微软在自家操作系统中对Java的故意阻挠,相信Java在Windows桌面程序开发中也会非常流行。另外因为Android是Java编写的,所以Android应用开发也是Java其中一大应用场景,虽然近年来随着跨端开发框架(比如React Native)的流行,Android平台的Java原生应用比例有所下降。

  Java在服务端开发的流行得益于两方面,一方面是Servlet技术,另一方面是它良好的生态及丰富的第三方库。在Servlet技术推出之前动态网页服务端生成及数据处理主要靠CGI实现,CGI是将HTTP协议的处理与动态页面的处理分开,Web服务器(或者可以叫网关)只负责对HTTP协议进行处理,然后解析出对应的资源,将动态资源的请求解析后递交给相应的后续CGI程序进行处理,并将CGI程序的输出返回给客户端。CGI采用的是fork-and-execute的方式,就是说对于每个CGI请求,Web服务器都会fork出一个进程然后运行指定的CGI程序。这样会有两个问题,一个是在高并发的情况下会创建出一大堆进程,严重降低服务器性能造成卡顿甚至无法访问,从而只能在较低QPS(每秒请求数)的情形下使用。另一个是CGI的进程在处理完请求后就会销毁释放所有内存资源,这样只利用CGI程序无法实现变量常驻内存,只能通过其他程序比如Redis、Memcached等来实现。而利用Java的Servlet技术编写的Web服务器却不存在这些问题,Servlet容器也是利用Java实现,这样就不需要在它之前再放一个Apache2、Nginx之类单独的Web网关了,整个Web服务端只需要持久运行一个Java程序即可。也是因为这样Java程序的变量可以一直常驻内存,不借助Redis等程序也可以实现内存缓存等技术。

  也是得益于Java在服务端的流行,Java才能在服务端开发领域有这么好的生态环境以及这么多优秀的开源第三方库。比如Tomcat、Jetty等Servlet容器,不只是单纯处理了HTTP协议,还处理了多线程、线程池、IO方式等等问题,使得开发者只需要简单的配置一下就可以获得高性能的Web服务器。再比如利用Netty这个网络通信框架可以方便的编写出并发性能极高的服务端程序;Hibernate、Mybatis等数据库工具不只封装了数据库连接,还帮开发者处理好了连接池、数据映射等等问题;Jackson、Gson等为开发者提供了高效的Json序列化及反序列化工具;Apache Shiro等安全框架使得身份验证、授权、密码和会话管理更加容易;slf4j、log4j等日志工具使得日志记录更加便捷。有这么多优秀的第三方库,当然也少不了便捷又强大的依赖管理工具比如Maven、Gradle等,也是因为优秀的依赖管理,编译Java的项目一般不会出现C/C++那么让人头疼的依赖问题。最后当然要提到如今最流行的开源Java框架Spring了,它包括了Spring Framework、Spring Boot、Spring Data等等一系列项目。Spring的强大之处在于它利用控制反转(IoC)只需要简单地配置就可以将各种第三方类库整合在一起管理,另外还提供了非常多实用的功能,比如异步、事务管理、定时任务、数据校验等等,同样只需要一行简单的注解或者几行简单的配置就可以实现。

  这些优秀的第三方库为Java服务端开发工作节省了大量的时间,但同时也使得Java开发门槛大大降低,涌入了一大批低水平的Java程序员,这些低水平程序员作为Web开发者很多甚至连HTTP协议都搞不清楚,可见其质量之低。要明白的是这些第三方库本质上只是可以复用的组件,它们的出现只是使程序员不必一遍遍地重复造轮子,将时间及精力用在更有意义的地方,但是只有深刻理解了其中的原理才能够掌握正确使用它们的方法,使它们发挥出最佳的性能。

0x03 C#

  讨论Java就有必要说一说微软的C#,当时Java横空出世的时候,微软视Java为巨大的威胁,而微软解决问题的方式也非常奇特,他们想出来的方法是悄悄地为Java提供某些扩展,使得用Java 编写的程序能够在Windows中工作得更好,但是在其它平台上却不能运行。因此微软推出了J++语言,号称是符合Java规范但其实并没有实现Java的RMI和JNI,还在J++中增加了Delegate、Event以及一些直接调用Windows API的功能,对Java进行了大量的修改并且阻碍了其跨平台特性。不过微软这种破坏Java跨平台特性的做法被Sun公司以违反反垄断法为由起诉了,最终Sun公司胜诉,微软被判决不得对Windows系统中包含的Java语言作出任何改动。这之后微软推出Windows XP及新版IE时故意不安装Java软件,并推出了自己仿造Java的C#语言,结果又被Sun公司起诉了。直到今天在Windows下安装Java以及配置Java环境都是一件麻烦的事。这也是为什么虽然Java很适合视窗操作系统,却在Windows下应用较少的原因。

  虽然最开始的C#是仿造Java为了与其竞争诞生的,两种语言极为相似,但是随着两者的版本更新,它们之间的差别也越来越大。其中一个重大的差别是这两种语言对于泛型的实现方式,Java的泛型采取的是类型擦除的方式实现的,也就是说在Java字节码中不包含泛型类型的任何信息,只保留了原始类型,这意味着Java在运行时不能推断出任何泛型信息,因此被叫做伪泛型。这种伪泛型当应用到基本数据类型时会造成非常多不必要的装箱拆箱操作,严重影响性能。而C#则是真正地将泛型信息保留在中间代码里将泛型落实在CLR层面上,运行时CLR会为不同的泛型类型生成不同的具体类型代码执行。此外C#能够支持运算符重载而Java则不能。

  C#的缺点在于它是微软公司的,然而服务端很多开源组件都是优先支持Unix/Linux系统的。对于服务端开发来说,这就意味着要么同时维护两个平台,要么使用微软全家桶解决方案。虽然C#有着理论上的跨平台特性,但是.NET Framework是闭源的,早先.NET的开源实现只有Mono,而Mono与JVM比起来就像个玩具,因此C#也就在Windows下能够发挥出它最大的功效。不过这几年微软慢慢拥抱开源,推出了开源的.NET Core项目,希望其能作为跨平台.NET的基础并且构建一个更强大的生态系统,具体会如何还要看接下来的发展。

0x04 PHP

  PHP这门语言要从它的起源说起,PHP的作者最初为了替代当时CGI中广泛使用的Perl脚本语言,将一些C语言开发的模块用一种脚本语言组合在一起来实现CGI程序功能,进行简单的动态页面展示,这种脚本语言就是PHP。这种起源使得PHP最初并没有一个完善的设计,存在着许多设计上的缺陷,即使PHP到现在已经发展了二十多年,还是能从它最新版本中看到最初这些设计的影响。

  对于其他语言,比如C/C++、Java、C#、Python等等,都会有非常多大部分是用自身语言编写的基础类库代码,而在PHP中这是不存在的,PHP的角色更多地是一种胶水代码,把很多C语言函数片段黏合在一起,因此PHP中有一千多个内建的函数,每个函数都是对C语言库函数的调用。由于最初的PHP并没有命名空间并且是用函数名的字符数量来作为函数hash的,因此内建函数命名有长有短并且根本没有统一的规则,这使得PHP程序员记函数名也成了一个梗。类似这样语言本身的设计问题还有很多,具体可以看这篇文章『PHP: a fractal of bad design / fuzzy notepad』。

  PHP这种类似胶水代码的语言特性也使得其最初根本不需要什么包管理,因为最初面向过程的PHP根本没有多少能够复用的代码,一有点需要复用的东西都用C语言写成PHP扩展了。后来PHP 5引入了面向对象编程,引入了类和命名空间,也使得PHP慢慢变得完善可以做越来越多的事情,这时候才有了包管理工具Composer等,也有了一批优秀的Web开发框架比如Laravel、Yii2、ThinkPHP等等。FastCGI技术的出现解决了原来CGI的性能问题,FastCGI有点类似线程池的设计,只不过这里将线程换成了进程,因此PHP利用FastCGI技术并结合Nginx,在I/O性能及CPU资源利用效率已经能和Java、Node.js相比较了。然而FastCGI本质上还是利用了CGI,因此PHP在每次请求到来时都需要重复进行类的创建、数据库链接的创建等等,无法实现连接池等技术,并且PHP的纯解释运行效率也要比JIT编译低不少。不过大多数网站的并发量其实并不大,而PHP相比Java、Node.js的学习成本及开发成本要低很多,因此PHP仍然一直流行在Web领域。

  到今天为止,大部分PHP项目都还是传统的Web项目,其他用途的PHP项目非常少。因为一旦脱离了成熟的框架就要去裸写PHP,但是PHP还是没有脱离最初的语言特性,大部分内建函数都还是C语言那种面向过程的函数,这使得PHP用起来相比其他完善的面向对象语言来说会非常不友好,有用PHP封装这些的时间还不如直接用C++去封装C呢。不过这几年PHP有了一个非常强大的扩展——Swoole网络通信引擎,这有可能会让PHP在除了Web开发的其他应用场景中也占有一席之地,具体如何也要看后续发展。

0x05 Python

  Python这门语言有着非常多的优点,大家公认的优点就不必多说什么了,要用一个词来形容的话就是“优美”。Python的语法相当简洁明确又不失强大,并且是完全面向对象的语言,虽然是脚本语言但是有着强大完善的标准库,这些都使得Python无论在什么应用场景中都越来越流行。

  不过Python也是有不足的,其中一个不足就是其鸡肋的多线程,由于全局解释器锁(GIL)的限制,同一时间只能有一个线程运行,因此在多核CPU上Python的多线程程序根本不能像Java那样发挥出多线程最大的效果,只有在单核阻塞I/O的情况下才会有些许用处,不过Python同样便捷的多进程一定程度上缓解了这个问题。另外由于使用最多的CPython是纯解释运行,因此Python的计算效率会比C/C++这种编译语言差很多。这时候的解决方法一般是用C/C++编写需要较高计算效率代码部分,然后在Python中对它们进行调用。还有Python的包依赖并不是像Node.js那样沿着目录向上遍历寻找,而是采用了与C/C++类似的根据环境变量寻找依赖,这就导致了不同Python程序的依赖也可能会像C/C++那样产生版本上的冲突,这时候可以借助第三方的虚拟Python环境隔离工具比如venv来解决。

  在科学计算领域,Python也同样变得越来越流行,这种流行得益于两点,一是Python出色的语法(运算符重载、面向对象等)使其能够以简洁的形式表达出各种高级数学形式,比如矩阵运算、向量运算等等,二是Python作为开源语言有着大量优质的第三方模块,比如SciPy、NumPy、Matplotlib等。这些优点使免费开源的Python语言有能力和专门用于科学计算的商业脚本语言MATLAB相互竞争。同样也是因为这些优点,Python在机器学习、网络爬虫、数据分析及可视化、服务端开发等领域中都非常流行,可以说Python一直在朝着繁荣的方向发展。

0x06 JavaScript

  JavaScript也得从它的起源说起,当时拥有着市占率前列的浏览器的网景公司为了使非专业程序开发者能够更好的展示网页,决定开发一种能够直接编写在网页标记中的脚本语言,这种脚本语言经过几次改名后最终被命名为JavaScript。虽然名字里有Java,但是它和Java一点关系都没有,叫这个名字纯粹是为了蹭当时Java的热度。由于这是运行在浏览器上的解释型语言,因此它需要借由浏览器来对其进行支持,当时除了网景公司的浏览器,微软的IE浏览器同样很流行,要想在IE浏览器上也能正确显示含有JavaScript的内容,就需要在IE浏览器上也对JavaScript进行实现。和微软对付Java的手法类似,微软又借助着其操作系统的垄断地位用不兼容的方式来打压竞争对手。虽然微软在IE浏览器中推出了JScript来作为JavaScript的实现,但是JScript中有许多专属对象只能够在IE浏览器中使用,这就导致了许多网页在非微软平台或浏览器下没有办法正常显示。后来网景公司将JavaScript提交给欧洲计算机制造商协会(ECMA)进行标准化,标准化的语言被叫做ECMAScript。虽然网景公司在第一次浏览器大战中败下阵来不复存在,但是JavaScript一直发展延续下来,直到现在仍是浏览器最流行的脚本语言。

  后来出现的Node.js使得JavaScript能够脱离浏览器环境跨平台运行,从此JavaScript和Python、Java等语言一样也可以用来编写服务端程序了。不过由于JavaScript起源于浏览器环境,因此它的设计和一般的编程语言有非常大的区别。一般浏览器只有一个线程来运行JavaScript代码,并且需要响应页面上各种各样的事件,因此这决定了JavaScript最大的语言特性,只能单线程运行并且是事件驱动天生异步的。这里要注意JavaScript的异步概念,JavaScript的异步不像是单核CPU上运行多线程,到时间片了就会切换到另一个线程运行。JavaScript是在遇到需要异步执行的地方,比如setTimeout,将事件(event)递交给浏览器另一个触发线程后继续运行之后的JavaScript代码,触发线程在发现事件满足运行条件需要被触发时,就将这个event传递给一个event队列,这个event队列包含所有等待执行的event。然后JavaScript的运行线程使用一个event loop,在没有JavaScript代码执行的时候每隔很短的间隔遍历event队列,如果有event就去执行相应的回调代码。因此JavaScript的异步不会存在多线程程序的那些线程安全问题并且也没有线程切换的开销。

  当然这种单线程异步的方式也存在着弊端,这种方式下永远是先将一个event处理完了才会处理下一个event,如果某一个event需要非常长的计算时间,那么后续所有的event都会因此卡住。而多线程计算时间到时间片后会由操作系统内核进行线程的切换,让所有的线程都有机会运行,不会存在这个问题。所以JavaScript这种单线程异步的方式适合于I/O密集型的程序而不太适合于CPU密集型程序。

  Node.js也是使用了浏览器的Chrome V8引擎作为JavaScript的解释器,因此Node.js也继承了JavaScript的特点,单线程、事件驱动、异步非阻塞I/O(也有少量函数接口是异步阻塞的,比如fs.read),不过一些函数也提供了同步阻塞的版本。Node.js的I/O核心是libuv库,它和Netty、Nginx等一样底层采用epoll进行网络I/O操作,因此Node.js编写的服务端程序同样能够获得极高的并发性能。不过需要注意的是,由于是单线程执行,只要代码中出现了未捕获的异常,就会使整个Node.js进程挂掉,因此在实际使用中Node.js编写的服务端程序常常会配合守护程序使用,比如pm2,从而保证进程意外挂掉后可以自动重启运行。Node.js发展到今天同样有着强健的生态系统,不过其默认的包管理程序npm常常会被人吐槽,因为经常会出现安装一个包会需要下载几百个依赖包的情况。

  早期的JavaScript仅仅只是想作为浏览器的脚本语言供非专业程序员使用,并未考虑其他用途,因此早期的语法设计存在着诸多不合理之处。比如同一个关键字function既可以定义函数,也被用来定义类;虽然是面向对象的语言但是有着奇怪的构造函数写法、方法写法以及继承写法等;回调函数中不知道是什么的this变量;回调函数中会受外部影响的变量等等。不过这些在ES6标准推出之后都得到了较好地解决,新的标准使得JavaScript也可以像传统面向对象语言一样有能力编写复杂的大型应用程序。但是这又产生了另一个问题,为了向后兼容,JavaScript引擎既要支持新的标准同时也不能放弃旧的语法,于是就导致了对于同样功能的代码会有好几种不同的写法,极大影响了语言的可读性和简洁性。而且由于各个浏览器对新标准的支持情况不尽相同,许多程序员为了能够兼容尽可能多的浏览器,故意不使用最新的语言特性编写代码,不过有了webpack等转换工具后这倒不是什么问题。


  其实编程语言没有简单的优劣之分,不同的语言所擅长的应用场景也不尽相同。这么多编程语言的出现无外乎解决两个问题,要么为了提高开发效率,要么为了提高运行效率,对于编程语言来说没有学哪种语言比另一种好的说法,语言只是一种工具,因此学会根据需要解决的问题选择合适的工具才是最重要的。

参考列表

发表评论

电子邮件地址不会被公开。 必填项已用*标注