聊聊学习编程语言


3楼猫 发布时间:2024-05-01 10:33:51 作者:韩大 Language

“PHP 是世界上最好的语言——(破音)”
关于编程语言的话题,一直是程序员们的经典话题。几乎每种语言,都有一批近乎宗教狂热般的粉丝。曾经的我,也是其中一份子,现在回想起来,有一部分原因,是由于学习并掌握这门语言的生态,需要付出不小的时间精力成本,所以自然会有“维护”自己的付出的偏见。当我学习并使用的语言越来越多,我却发现很多有意思的事情,于是想聊聊这些发现,也希望能给学习编程语言的读者,一些微薄的帮助。

学习编程需要什么前提条件

数学和编程的关系

在我真正开始学习编程之前,我就听说过:“编程需要很好的数学能力”。由于我以前的数学考试成绩不算很好,所以一直都不觉得自己适合搞编程。想不到的是,由于接到一个兼职的工作,需要用到编程能力,从此走上了穿格子衫的码农生涯。
现在回头来看,“编程需要很好的数学能力”的这个认知,我起码犯了两个错误。第一个错误是,我把数学考试成绩等同了自己的数学能力;第二个错误是编程工作是一个具有广泛内容的事情,在很多领域并不需要你掌握很多高级的数学工具。
国内的数学教育,由于高考指挥棒的存在,所以大部分都是为了“解题”而设计的,而真正的数学能力,是抽象思维能力以及想象能力。很多做题高手,可以凭借海量的题目信息,以及高超的记忆力,去考出高分。但是面对需要复杂的逻辑问题,需要自己设计一些逻辑工具去解决问题的时候,往往并不能很好的解决。编程就是需要有抽象的理解能力,并且能通过想象力,在脑海中构建出一系列的概念,并且推理出方案的活动。而在一般的信息管理程序开发领域,我们要用的数学工具,最常见的也只有初中代数而已。如果你还写一点 2D 的游戏,可能会用到一些平面几何知识,如果做一些策略游戏,可能用到一点概率论或者仅仅是排列组合的知识。除此之外,很多高级的数学工具,在编程工作中都并不普遍,起码写个 APP 网购什么的是用不上的。
import math # 使用勾股定理 a = 3 b = 4
c = math.pow(a*a + b*b, 1/2) print('勾', a, '股', b, '弦', c)
如果你要开发 3D 游戏,特别是和图形渲染相关,需要学习计算机图形学,还是需要一些数学知识的。但是这类知识,和高考数学成绩,个人感觉关系不是很大。如果要开发机器学习的程序,可能需要对线性代数、微积分有一定的了解,不过就算你不是特别懂这些,也不会让你完全没法从事机器学习的工作。
从基本的数学能力,也就是抽象思维、逻辑推理、想象力这些角度看,编程工作确实和数学关系匪浅;但这并不表示数学考试成绩不好,就不适合编程,也不用因为没学过离散数学或者图论,就觉得自己不能成为优秀的程序员。如果你手上有一个问题,看起来可以用编程解决,完全可以放心大胆的开始。兴趣和需求,才是真正的学习编程的前提条件。

语文和编程的关系

一般情况下,很少人会认为编程和语文有什么关系,最多可能觉得,写写技术文档会用到语文。但是软件开发界有一句名言:
任何人都能写出计算机能读懂的代码,只有好的程序员,才能写出人能读懂的代码
There are only two hard things in Computer Science: cache invalidation and naming things (计算机科学中最难的两件事是命名和缓存失效) - Phil Karlton
现在软件开发的核心矛盾,是日益增长的需求变更,和相对落后的开发效率之间的矛盾。解决这个矛盾的基本方法,就是提高代码的可读性。只有这样,才能让代码的修改更快速,才能让更多的人投入到一个软件项目里并行开发。
如果你要写一份程序源代码方便人类理解,清晰准确的注释必不可少,但更重要的是,整份代码的思路是要清晰合理的,是要以方便阅读的角度进行“谋篇布局”的。在具体的代码表达式上,也应该选择更符合人类思维习惯的进行编写;同时对于变量、函数的名字,也需要认真的设计,以确保表达其含义,这就是编程所需的“遣词造句”。
常见的判断流程代码

常见的判断流程代码


在我们的语文课程学习中,最常见的课题是:中心思想、段落大意。如果我们能很好的掌握,如何从文字篇章中分析、理解这些含义的技巧,那么我们在编写软件的时候,也可以用同样的技巧用在代码的阅读和编写上。在我看来,语文水平的差别,代表了平庸的程序员和优秀的程序员之间差别。

英语和编程的关系

以前,很多编程技术资料、手册都是英文的,所以那个时候,英语水平确实对技术学习有一定的影响,但现在机器翻译水平已经很不错了,相当多的技术学习,完全可以使用母语来开始。当然,有很多“硬核”程序员,坚持要看原版英文书籍和手册,并宣称这才是最好的学习方法,不过在这个信息爆炸的年代,这么做对于自己要求确实也是太高了一点。
尽管英语水平,现在早已不是从事编程工作的门槛了。但是拥有一定的英语能力,还是很有必要的。我曾经见过使用汉语拼音作为变量名和函数名的代码,阅读起来除了很慢以外,而且时不时我还会改错:要知道,中国有很多方言地区,这些地区的人,对于普通话的读音,可是各不相同的,譬如“灰机”、“资识”。与其折腾五花八门的方言拼音,还不如查一下字典用个好点的英文单词。
有的人会说,为啥我们不直接用汉字作为编程的文字呢?事实上这个讨论在网上一直都有,也有使用汉字的编程语言,譬如“易语言”。总体来说,汉字编程有两个比较大的问题,其一是国际化的问题,毕竟编程技术在全球范围内的共享和共建,英语还是最常见的选择;其二是我们手上的是一个英文键盘,从输入效率来说,写英文的效率会比较高。
总体来说,英语水平会影响编程技术的学习和使用,但不是一个核心门槛。相反,如果长期从事编程工作,可能还会提高一定的单词量,因为常常需要查一下字典,为自己辛苦写下的代码,取个“洋气”的名字。

经济条件和编程的关系

现在电脑已经不是什么高档电器了,甚至很多手机都比电脑要贵。而且一般的编程工作,也无需特别豪华的硬件配置,很多二手的电脑,都完全能胜任很多编程工作。甚至攒硬件自己装一台电脑,也是一个能学到不少知识的过程。
过去很长一段时间,程序开发的工作机会多,收入水平可观,所以吸引了大量的人员投入这个行业。不过根据个人的经验,也有很多人,在真正从事了一段编程工作,都放弃了这种工作。而且本身经济条件越好的,越容易放弃。毕竟,编程工作是一个“严格”的工作:你可能写一篇文章,里面有几个错别字,不太会影响这篇文章的可读性;你可以画一副画,有几笔是画错了的,也不一定被观众发现……然而,你写错了一行代码,首先编译器就会暴跳如雷和你较劲;如果你搞定了编译器,如果有一些隐藏的 BUG,可能让程序运行到一半突然就崩溃了,如果你见过所谓的“Windows 蓝屏”,你就能知道这类问题多么让人烦躁。 ——一直浸泡在高浓度的“失败”情绪中,而且还提心吊胆的害怕不知道什么时候出现 BUG,这不是一个让人容易接受的工作。
如果一个人既不用担心柴米油盐,又喜爱编程这个工作,这是最好的状态。这样才能真正的去探索软件开发的技巧,而不是天天打听学什么技术,能得到更高薪的岗位。技术的潮流变来变去,如果仅仅是试图赶上风口,是一件很累人的活。所以,归根到底学编程和有钱没钱关系不大,如果只是想混口饭吃,这个工作,可能和其他工作差异不大;如果真的喜爱编程,那么从中也能获得非常大的乐趣。

C 语言为什么使用如此广泛

说到编程语言,C 语言是一个绕不过的话题。一直到今天,这门历史悠久的语言,依然是软件开发中最常见的语言之一。很多人都说,学编程必须要要学 C 语言,但是事实上,不会写 C 语言的程序员也比比皆是。只是简单的学习过一下,没有真正的开发过工程,是不能叫做“懂 C 语言”的。那么,到底 C 语言是不是一定要学的呢?我觉得要学,也可以不学。下面说说我的理由。

操作系统的原生语言

大概大家都知道,我们现在用的很多操作系统,譬如 LINUX,都是用 C 语言开发的。那么,是因为 C 语言“特别好”,所以操作系统才用 C 语言开发的吗?我觉得相当大的原因是历史造成的,也就是说,当很多操作系统在第一个版本的开发时,C 语言可能是当时的最好的开发语言。
现在我们说到操作系统,譬如 iOS、安卓、windows,似乎操作系统是一个提供给用户,进行应用程序的安装、卸载、运行的平台软件。有些还会自带一些好用或者不好用的软件。但事实上,操作系统远远不止上面说的这些,甚至可以说,提供给最终用户进行操作的界面,并不是操作系统的核心功能。操作系统的真正的核心功能,是提供对硬件(最主要的是内存、CPU、磁盘)的功能封装和细节屏蔽,简单来说,操作系统的主要用户是应用程序的开发程序员。微软的第一桶金 MS-DOS 系统,全名是 Microsoft Disk Operating System,翻译过来就是“磁盘操作系统”,看起来是不是就特别“硬件”?
由于操作系统,对于大多数的计算机外设,譬如磁盘、网卡、显示卡等,都做了功能封装,这样应用程序开发者就不需要针对硬件去编程,而是只需要使用操作系统提供的编程接口,就可以使用这些外设的能力了。正因为 C 语言是很多操作系统的开发语言,所以很多操作系统都提供了 C 语言的 API。因此很多开发者都选择继续使用 C 语言来开发其他程序了。
在 Linux 上 man epoll

在 Linux 上 man epoll

我在使用 JAVA/C#/PHP 等语言的时候,会比较注意能找到什么样的“库”或者“SDK”,因为我的程序可能需要依赖这些“库”。举个例子,我要读写操作系统的“共享内存”,如果我用 C 语言开发程序,我可以直接调用操作系统提供的 C 语言 API,在 LINXU 上就是所谓的“系统调用”;如果我用 Java,就必须要找到 MappedByteBuffer 这个类,并且只能用 mmap 类型的共享内存,至于其他类型的共享内存功能,可能就要再找找有没有人封装过了。如果没有,那你就需要自己写一个符合 JNI 标准的 C 语言程序,封装一下这个功能函数,然后再提供给 Java 调用。——看,这不还是得写一些 C 的代码吗?所以,直接用 C 语言来写应用程序,就可以避免这个麻烦。

3L 和 ABI 规范

刚刚上面提到,JAVA 如果想要调用 C 语言的代码,需要按照 JNI 的规范写一个封装的程序。这个 JNI 规范,全称是 Java Native Interface,是 Java 提供的一个功能,可以调用一切 C 语言编写的库。事实上,绝大多数的语言,都可以调用 C 语言编写的库,甚至在 Go 语言的源码文件里,以注释的形式写的 C 语言源码,都可以被编译运行。而这些语言都能使用 C 语言代码的原因,是因为 C 语言的 ABI 格式,是最广泛被接受的一种 ABI 规范。
ABI 全程是 Application Binary Interfce,意思是应用程序二进制接口。这类接口定义了不同的二进程程序,如何互相调用(链接)。对比于大家更熟悉的 API,全程 Application Programming Interface,这个是提供给程序员编程用的接口。由于 C 语言的历史悠久,所以其他不管什么语言,一开始都会考虑支持 C 语言的 ABI 规范,以便新的语言可以使用大量的现成的 C 语言编写的库。
C 语言还有一个特点是“简单”,这里的“简单”不是说使用起来很简单,而是这门语言定义的内容比较简单。C 语言的关键字非常少,常用的概念只有“变量”和“函数”两种,恰好大多数语言都有这两个概念,所以去对应 C 语言的“变量”和“函数”就非常方便。这对于适配 C 语言库 ABI 接口非常有利。
如果你想写一个框架,或者比较通用的库,你可能会希望更让这些代码运行在各种编程语言环境下,现在来看,几乎只有 C 语言是最合适的。这样就“促使”很多人继续编写 C 语言代码了。
虽然 C 语言的库几乎被所有语言支持调用,但好玩的是,C 语言自己并没有规定这个 ABI 规范。提供这个 ABI 规范的实现代码,往往是编译器开发商做的。所以我们只学会 C 语言的内容,会几乎连编译运行都无法实现,而是需要再学习一门奇怪的知识,名叫《3L》,才能真正让程序运行起来。
所谓的 3L,就是 Link/Load/Library 的意思。这里面的知识,在每个学 C 语言的第一课就能碰到,但要真正掌握它,却往往没有那么容易。举个例子,我们的 C 语言的 hello world 程序往往是这样的:
#include <stdio.h> int main() {    printf("Hello, World!");    return 0; }
这段代码虽然简单,但却有一个值得思考的问题:“printf() 这个函数的代码,到底在什么地方呢?”很多人会说,在 #include <stdio.h> 里面嘛。这个说法对,但不完全对。因为 .h 文件被称为“头文件”,这种文件里面往往只有“声明”而没有定义。也就是说 stdio.h 里面只是 printf() 函数的“形式”,而不包含实现代码。
而上面 printf() 的具体代码,实际上是通过所谓的“链接”,被编译器“放”进你要编译的程序中的。而“链接”的对象,就是一个叫做 /usr/lib64/libc.so.6 的库文件——这个库文件被称为“C语言标准库”。当我们编译 helloworld 程序的时候,就算不写“链接”的命令,编译器也会自动帮我们链接这个库。
$ ldd a.out         linux-vdso.so.1 (0x00007ffc4bfb0000)         libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f6b48f24000)         libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f6b48c20000)         libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f6b48a09000)         libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6b4866a000)         /lib64/ld-linux-x86-64.so.2 (0x00007f6b494ab000)
在 Linux 上可以用 ldd 命令查看链接的动态库
然而,如果不是标准库,而是其他的库,就需要我们学习如何使用编译器参数,去指定要链接什么库文件。这里的链接还分动态链接和静态链接,静态链接的意思是,把需要的功能代码打包到最终的可执行文件里面去;而动态链接,则是让可执行文件在运行时,再去加载库文件。动态链接也可以作为一种软件更新的技术:我们可以通过发布和替换动态库(linux 往往是 .so 文件、windows 则是 .dll 文件)来更新一个软件的功能。
由于链接的过程,是由各个编译器软件来实现的,并不是统一在 C 语言的规范里,也没有一个公司或者组织来约束,所以使用不同的编译器,以及使用不同的编译器生成的库的时候,就会出现大量的“兼容”问题。加上 C 语言也没有后来语言的“包依赖管理”的系统,所以计算链接同一个库,如果用的是不同的版本,也可能出现链接错误,这些问题,也是 C 程序员需要经常处理的问题之一。
尽管 ABI 和链接规范有很多问题,但这些确实是我们现在操作系统的真实底层原理。所以当我们没有其他方法的时候(或者不想使用其他方法),我们最后还是有 C 语言这样的一个手段。

数学符号关键字

从使用硬件的角度来看,大部分编程语言,其语法功能实际上是用来操控“内存”和“CPU”这两种硬件的。C 语言设计两个重要的概念,来抽象和使用这两个硬件,一个概念是“变量”,另外一个是“函数”。这两个来自于数学的概念,被用于计算机编程,对于推动软件开发的进步,起到了非常重要的作用,以至于现代几乎所有编程语言都有这两个概念。但是“借鉴数学概念”用于编程,却并不是完美无缺。
  1. 最臭名昭著的数学符号误解,就是=号。在 C 语言中,这个符号实际上对内存的读取和写入操作,但在数学上这是一个“相等”的声明。这导致了大量的因为 if (foo = bar) 的 BUG 诞生。PASCAL 语言用 := 作为赋值符号,可以说是对这种错误的一个纠正。
  2. * 号,同时具备“乘法”“声明指针”“解引用”三个含义,具体是什么意思,取决于这个符号写在什么地方。这也是 C 语言代码阅读和学习比较困难的一个原因。
#include <stdio.h>   int main () {    int  var = 20;       int  *ip;   /* 指针变量的声明,这里的星号表示声明的是一个指针 */    ip = &var;  
/* 等号表示赋值,把 var 的地址写入 ip 变量的内存中 */      /* 使用指针访问值,这里的星号表示“解引用操作符”,即读取指针指向的内存块内容 */    printf("*ip 变量的值: %d\n", *ip );    return 0; }
  1. 在数学上,函数可以理解为对某种规律的描述:你可以输入自变量,获得返回值。然而,我们往往把“函数”当成一个功能处理过程去编写,这就导致了“就算相同的输入,也不会有相同的输出”的“函数”的出现。我们表面上好像是用函数在计算一些结果,实际上是利用这些函数的“副作用”去完成一些功能。这种表达和实际的差异,也是造成大量 BUG 的原因。
  2. 由于变量对应着内存,所以代码中的变量,并不能单纯的认为是一个数值的容器。譬如在 C 语言中,你如果返回了一个局部变量的指针,这个指针指向的变量内容,很可能在下次使用时,被不知道什么数据所覆盖。所以使用 C 语言必须要理解所谓“堆”和“(堆)栈”的差别。如果你认为局部变量不好用而使用“堆”里的变量,那么就必须注意自己进行内存的回收释放,否则就又掉进了“内存泄漏”的坑里。
  3. C 语言中的变量,虽然有各种类型,但实际上编译器几乎不会自动的对其进行什么操作,于是不管是什么类型,其实都代表的一块内存而已。类型不同仅仅是代表不同长度的内存块。而在不同编译器和不同操作系统下,同样的类型对应的长度还不一样,这就更增加了这门语言的复杂性。譬如 32 位系统下的 long int 是 4 个字节,64 位系统下则是 8 个字节。本来这种长度差异不太应该影响程序员编码,但很多 C 的库又设计成使用 指针+长度 的方式来传参,所以变量长度变得不得不关心了。同样的问题还有结构体的字节对齐问题。
不过话说回来,C 语言有再多的问题,还是比汇编语言更利于人类理解和操作。而对于内存操作的直接和方便,也让程序员们能创造更多有用且高效的通用数据结构,让我们处理复杂问题变得更加简单。因此在追求高性能程序模块的程序员眼里,C 语言依然是不可替代的一种工具。
那么最后来说,C 语言是不是作为程序员,必须要学的语言呢?从开发实践上来说,不是必然要学。很多编程岗位,并不会因为你懂 C 语言就给你躲开工资。但如果你懂这门语言,用这个语言开发过程序,你会有一种接触底层原理的感觉。计算机科学的基本形式,就是层层抽象。而 C 语言,刚好处于擅长形式化的高级语言,和汇编这种硬件操作语言之间。穿透了这层抽象,就能触摸到硬件的层面,从而对计算机科学有更深一层的理解。

自带内存管理的语言们

内存为什么需要管理

在使用 C/C++ 这类需要手工管理内存的语言时,感觉就好像去食堂吃饭:你需要先自己取餐盘,然后把餐盘装上食物,最后吃完后还得把餐盘还回去。如果餐盘太小了,还得多跑几趟多拿几次餐盘。如果我们使用带内存管理的语言,就感觉是在饭店吃饭:只要点好菜,服务员就会端上做好的饭菜,我们不必担心每道菜应该用多大的盘子,吃完也不需要打扫桌子。这就是所谓的内存资源管理工作,餐盘就是内存,我们希望使用的数据,是盘子里的菜,而不想操心盘子。
除了资源管理,我们写的程序现在往往都是“并发”的,譬如多进程或者多线程的。如果没有任何工具,我们是很难控制多段“同时”运行的代码,对同一块内存(变量)的读写结果。可能你想运行 i++,但是这个变量在多个线程同时运行时,可能 i 会被赋值为其他值;如果你把这个变量作为循环判断值,有可能你的线程会陷入死循环……
另外,安全性也是内存管理的重要原因,经典的“栈溢出”程序漏洞,就是由于对内存缺乏管理限制导致的:如果你从文件、网络或者地方读入一段数据,而没有安排足够的内存空间来存放,譬如使用了一个固定长度的数组作为局部变量,那么你就有可能在读取这段数据之后,让你的堆栈里放入一堆未曾预料的数据,而这段数据中的某一块可能正好能覆盖当前函数的返回地址,于是程序就会在执行完本函数后,跳到一个你刚刚读入的数据所决定的程序里,这样你的程序就可能被用来做任何事情。如此危险的漏洞,只是源于一个读入数据的数组没有检查长度而已!
由于编程语言最基本的能力之一,就是操控内存,所以内存管理功能的实现,自然成了很多编程语言的重要课题。

Java:你妈觉得你冷

Java 语言给人的感觉特别贴心,贴心到有点烦恼。对于初学者来说,一旦学会了和它“和谐共处”,写起程序来就会感觉非常“稳妥”。但如果你已经有其他一些语言的使用经验,你会有一种被强行套上秋裤的感觉。

CLASSPATH

相对于 C/C++ 让人眼花缭乱的各种链接错误,JAVA 语言由于对于 CLASSPATH 的错误,就显得简单太多了——尽管 ClassNotFoundExption 还是最常见的问题。事实上你可以把所有经过 javac 编译的文件,都视为动态库;所有你依赖的库,都通过 CLASSPATH 参数去添加,就可以解决问题了。不过,由于在很多 JAVA 框架里面,组织 CLASSPATH 内容的工作,可能被放在各种配置文件里面,所以很多时候我们“学习”的额外内容,是那些框架“造成”的,但本质上也就是 CLASSPATH 而已。
Java 还具有在运行时通过代码下载、加载 .class 文件的能力。这种能力对于动态更新代码,开发诸如边下边玩功能很有意义。这种能力对比纯脚本型语言要复杂一些,但是性能会更高一点。
CLASSPATH 这种机制很方便,唯一的缺点就是,所有的 java 程序,在进程列表里面,都是一串以 java 开头的长长的命令行(里面大部分都是 CLASSPATH 的内容),看起来一点都不像一个正经进程

内存管理

我们用 C 语言定义一个变量,我们可以决定变量所占用的内存长度,以及这块内存是在堆上,还是堆栈上;我们还可以决定,数据在变量之间传递的时候,是传递内存地址(指针)还是传递值(复制)。但在 Java 语言里,这些自由统统都不存在:
  • 基础类型变量,譬如 int/boolean 这些(注意 String 并不是基础类型),永远都是值传递,你也没法关心是在堆上还是栈上。
  • 对象类型变量,所有的 Object 的子类和数组这些,永远都是引用传递(类似指针),所以肯定是在堆上。
  • 如果你硬是想弄一个基础类型的数据容器,譬如存放 int 类型的“对象”,那么你需要进行“装箱”这种仪式,幸好新的 JDK 已经从语言层面支持了。
Java 的数组,包含 String 类型,终于是会自动检测长度了,不会让你写入数据到预期的地址之外了,如果你的程序没注意,Java 会抛出一个 OutOfIndexException。这样“栈溢出”的安全漏洞风险,会大大的降低。虽然一不小心就会碰到这种异常很烦人,但是每个这种异常,放在 C 语言程序里,可能就是一个致命的安全漏洞,修复这种问题还是很有必要的。
当然,Java 是不需要自己回收内存的,因为所有的“对象”都会被 JVM 在运行时进行“垃圾回收”。这个过程我们可以想象,在整个内存池中扫描成千上万的地址,是挺消耗性能的。一般来说我们无法直接参与这个过程,因此也被很多程序员诟病,还想出各种“奇技淫巧”试图影响这个过程。
虽然 Java 有自动的“垃圾回收”机制,但还是有可能出现内存泄露的。如果你使用了一个 static 类型的变量,恰好这个变量又引用了大量的其他对象,譬如说你这个变量是一个 HashMap,这就可能成为一个“内测漏洞”。当然,一个无穷递归也很容引发内存耗尽,这个“栈耗尽”和其他语言是一样的。

异常机制

任何声明了会抛出异常的方法,你调用它就必须要捕捉这些异常,否则不能通过编译检查。——这个对于初学者来说,简直就是一场和编译器的搏斗。但是这场让人精疲力尽的博斗的结果,还是挺有价值的。绝大多数的错误,都会被强迫处理,以往那些“不判断返回值”而导致的 BUG,在 Java 中是很少出现的。异常处理就好像一个安全围栏,把你的程序保护起来。
FileInputStream in; try {   in = new FileInputStream(file);  // 正常流程就这两行   in.read(filecontent); } catch (FileNotFoundException e) {   e.printStackTrace(); } catch (IOException e) {   e.printStackTrace(); } finally {   try{     in.close();   } catch (IOExeption e) {     e.printStackTrace();   } }
读个文件,异常处理代码比正常代码要多一大堆
然而,再安全的围栏,也有一些缺口。对于服务器端 Java 程序来说,最常见的有两个:
  1. NullPointerException 这个异常是由于你调用了一个内容为 null 的对象的方法或者属性。由于 null 这个值非常特殊,它可以被赋值给任何类型的对象,所以很难被简单的发现。什么情况下,一个 Object 变量被赋值为 null 呢?很可能是在调用一个 HashMap 的 get() 方法后,没有判断返回是 null 导致的。有一些 API 调用会返回 null,这个是非常需要注意的。
  2. ConcurrentModifyException 如果有两个以上的线程,在同时操作一个容器变量,就可能会出现。譬如一个线程正在用迭代器遍历,另外一个线程在插入元素。大多数情况下解决方法也比较简单,就是用 synchronized 关键字来“锁”住这个容器变量就可以了。当然如果迭代器遍历循环代码里,又调用到另外一个容器的方法,那么就有可能发生“死锁”。当然也可以用一些支持并发的容器,来解决可能的并发修改问题,Java 提供了一整套“并发”所需的库( java.concurrentcy.* ),包括各种形式的锁、线程安全容器等。

并发支持

尽管没有关键字来直接启动线程,但是 synchronized 关键字让 Java 的“并发锁”用起来变得非常容易。JDK 自带的 Thread 类及其相关类库,让编写多线程程序变得非常简单。
不过,对于并发问题的处理,除了多线程以外,单线程异步是一种运行效率更高的方式。因为有可能节省大量的线程栈内存的占用,而且也可以利用到 Linux 的 epoll 能力。java.nio 提供了比较好的支持,不过,对比多线程的支持,异步回到或者“协程”的支持就没有 Go 语言那么好。
Java 的多线程,在 Linux 上还是使用 pthread 库,用子进程来模拟的线程。虽然 Linux 的多进程性能也相当不错,但是在成千上万的“java 线程”的疯狂切换的情况下,对内存和CPU都会造成比较大的压力。这个问题也是后续其他很多语言和框架着眼的地方。譬如 go 语言就会根据 CPU 的核心数来启动真正干活的子进程,而编程概念上的“协程”和真正的子进程是不捆绑的。

C#:我全都要

C# 就是 Java 异父异母的亲兄弟:两者都号称可以跨平台,也确实做到了windows/linux 双栖;两者都是运行字节码代码,有自己的虚拟机进程;以前觉得 M$ 特别封闭,觉得 SUN 相对开放,现在反过来对比,微软比甲骨文更开放。
C# 好像一个各种语言特性的大杂烩,或者叫博彩众家之长:
  • Java 不是没有值拷贝的变量类型吗?C# 有,叫 struct
  • Java 反对使用“输出参数”,C++ 使用输出参数很普遍,C# 都有,既可以返回一个对象而没有心理负担,也可以用 out/ref 关键字做输出参数。
  • 异常处理 try-catch,C# 也有,但不会像 Java 一样强迫你处理
  • RTTI、反射,C# 全有
  • Java 的 Annotation,C# 叫 Atrribute
  • Interface Java 有,C++ 没有,C# 还是有,而且比 Java 的更彻底:要访问一个接口的方法,只能通过接口类型的变量访问,通过真实实现类型的变量是无法通过编译的。
  • JavaBean 苦口婆心推广 Setter/Getter 的写法,C# 直接用语言实现掉,就是 Property 特性。
  • C++ 可以重载运算符,Java 不可以,所以 C# 也可以自定义+ - * /运算符
好像上面这样的特性还有好多好多。你可以按 Java 类似的特性去写 C#,也可以用 C++ 的想法去写 C#,不知道这是不是这门语言设计者的目的呢?

Go:专门为服务器端设计的语言

Go 语言的设计相当的“自我”,它不会去考虑迁就不同“习惯”的程序员,而是直接定死自己觉得好的“规矩”作为默认用法,这和 C# 简直就是一个强烈对比。
  • 所有的源码编译出来就是一个二进制,全部都是静态链接。(在服务器上一般只运行这个程序,也不需要动态更新或者和其他程序共用动态库啥的)
  • 包依赖功能自动和 git 绑定,你不喜欢用 git 也不行。(客户端开发,特别是游戏客户端开发,工程里面包含太多二进制文件,用 git 会特别的慢,所以你别用 Go 写客户端吧。)
  • 没有什么引用传递,全是值传递,如果不想复制一个巨大的数据块,就用指针。这个指针不能像 C 语言一样进行数学运算 ++ --,但还是要叫“指针”,而不是换成什么别的名字遮掩一下。
  • 局部变量返回之后,就变成了堆上的变量,所以也可以垃圾回收了。(不需要手工指定使用堆还是堆栈,就算你用new()指定了,可能也是白忙活)
  • 自带并发“协程”,但又不是异步的,一样需要你把容器类型变量锁起来,或者你用线程安全的容器,用起来和多线程几乎一样。
  • 把 select、chan 这种东西直接弄到语言里面,尽管这功能完全可以用库的方式提供,但 Go 就是要让你膜拜一下。
  • 没有 try-catch-finally 捕捉异常这种玩意,只有 defer 关键字,你就是要用 finally 的写法去干捕捉异常的事情。
  • 用注释来生成文档有什么特别的,看我用注释来写程序:go 源码的注释可以写一段 C 语言代码并且调用!
package main /* #include <stdio.h> void c_print(char* str) {   printf("%s\n", str); } */ import "C" func main() {   s := "hello C"   cs := C.CString(s)   C.c_print(cs) }

面向对象语言是一个骗局吗?

OO 三特性

我们常常说,面向对象的三个特征:封装、继承、多态。但是这三个特性,几乎每个特性,都有一堆反对者,认为这样的特性是无效的。
  • 封装:历来就有“失血模型”和“充血模型”的争议。在 WEB 开发领域,由于存在大量的数据库 CRUD 操作,所以不管你的类有什么属性,大多数都是那么几个方法,所以把方法和属性封到一块,看起来没啥必要,反而增加了大堆类似而重复的代码。
  • 继承:继承会破坏封装,让父类的行为被改变,所以不要继承!用组合来代替继承!多继承是邪恶的,所以只能单继承,如果你需要一个对象同时可以是几个不同的“类型”,那么你只能为这些类型每个都定义一个“接口”,增加一大堆代码。如果“继承树”一开始设计的不合理,极有可能需要修改“树干中”的某一个节点,也就是某个基类,导致下游一大堆的子类被迫要修改。
  • 多态:这个特性是争论最小的特性,但是也有人觉得,其实就是一种 switch case 嘛,最高级的程序员(食材)往往只需要最简单的语法(做法)……
在没有面向对象特性支持的时候,编程语言也可以完成一切逻辑表达。如果我们不把面向对象视为一种信仰,而是一个工具,我们才能发挥它的作用。
面相对象是一种名词性的定义,它希望编程语言不再是动词形式或者数学形式的,而是类似日常人类思维的方式去描述问题。所以才有了把函数和结构体放到一起,成为一种逻辑单元的定义。如果我们已经把一个事情的处理流程,完整的细化分割出来后,其实是不需要面相对象的,这种场景在现存的进存销、运输管理、财务、电信这些现成业务环境下,是很常见的。上面失血模型的支持者,连封装都不想要了,还怕什么继承破坏封装?所以说谈面向对象的时候讨论失血模型,本身可能就是一种错误的面向对象建模导致的问题。
如果不使用继承,即便相似的功能,也必须要定义很多用法类似,但名字不同的函数(库)来提供给程序员。PHP 的库里面就有大量这种例子。学习 API 在这种情况下,成为一种效率比较低的工作。如果你只是开发某个特定的工作细节,这种消耗可能不甚明显,但如果你是某个外包软件公司的程序员,可能你每天都必须不停翻查各种 API 手册。更重要的是,你不能只修改一个库里面的几个函数,然后把一整个库提供给你的同事,而是必须重新写一整套的库,即便库里面大多数代码都是只有“包装代码”——这也是用组合替代继承的常见情况。
关于多态,甚至有一个设计模式,基本上就是多态特性的“使用指南”,这个模式叫“策略模式”。不过,也有一个走火入魔的例子,就是类似早年的 Java Spring 框架,整个程序的初始化,并不是 Java 代码,而是一个巨大而且复杂的 XML 配置文件。所有登记的类都按照一套复杂的规则,实现某一批接口,然后在没有 IDE 和编译器检查的帮助下,试图组合运行起来。事实上,如果你认为多态是一种好的编程特性,那么必然也会认可,降低程序员的心智负担是一个有价值的事情。只不过继承和封装,并不像多态对于复杂逻辑的简化程度,有如此立竿见影的效果。

类爆炸、构造器混乱和“基于对象”

“面向对象综合征”最典型一个症状就是类爆炸,最常见发病于 Java 领域:在 Java 中,任何东西都要放到一个类里面,就算只是一个 main 函数,也必须要找个类把这个函数包起来,还得加上 static public 修饰方法;用所谓面向接口编程的模式下,往往你为了增加一个方法,被迫新增两个类定义,一个实现类,一个接口类。
如果沉迷于 MVC 的模式,一个功能可能被弄成三组类型:全是结构体属性的 model 大队、全是用于显示的代码的 view 大队、还有不知道为什么一定要有的一堆 control 大队,即便你写了一堆代码,还是发现有一批业务逻辑不知道放哪里,于是又写了一堆 service 类型,用来被 control 或者 model 调用。我们很多时候学习面相对象编程方法,都是向各种框架去学习,但是框架为了通用性,本身就是一个带有大量的接口的程序。所以完全学着某些框架去设计类,或者过于热衷实现某种设计模式,就特别容易搞出大量的类。
面向对象语言一直有一个问题,就是对象构造的过程非常麻烦。所以设计模式里面,有差不多一半是用来构造对象的。在 Java C# Python C++ 等语言里面,都有所谓的对象构造器的设计。但是在本类的各种属性初始化、本类构造器、父类的各种属性初始化、父类的构造器这些代码的顺序上,事情变得异常的复杂,加上构造器还有不同的参数和重载,加上类的静态成员也需要构造。如果类似 C++ 是多继承的语言,这种问题会变得更加复杂。很多编程的面试题,最喜欢考这一类问题,但我却觉得,这种复杂性是编程语言本身的一种缺陷。编程语言是给人用的,不是考人用的。
某种语言的对象构造顺序

某种语言的对象构造顺序

在比较新的语言(相对 C++/JAVA)上,很多时候会抛弃“类模板”的设计,就是不再设计一个叫“类”的概念,而是保留“对象”的概念。没有了“类”,就不存在“类爆炸”了。继承的实现,就用简单的“原型链”的思路:A 对象如果是 B 对象的“原型”,那么在 B 对象上找不到的东西(方法或者属性),就顺着原型链往上去找,也就是去 A 对象那里找。JavaScript(TypeScript)、Lua、Go 都是用的原型链,我称之为“基于对象”。使用这种方法,灵活性和代码的编写复杂度,显然是比较小的。在现代 IDE 的帮助下,往往也能获得足够的对象成员提示,不至于太多的编译错误。大部分传统的面向对象设计模式,其实都可以用基于对象的语言来实现,而且“构造类”模式,譬如工厂模式之类的,会比类模板的语言更加简单直观,甚至你都不会意识到在用的写法,曾经就是一种设计模式。

C++ 到底是什么?

并不是 C 语言

C++ 号称兼容 C 语言,意思是你可以像写 C 语言一样编写 C++ 代码。同时,一般的 C++ 编译器,也能很好的链接 C 写的库。但是,如果不特别的标注 extern "C",C++ 写的库是不能被 C 语言代码链接的。C++ 为了在语法上兼容 C 语言,让很多新的特性“嫁接”在 C 语言的概念上。譬如 指针 这个概念,整个面向对象的动态绑定,几乎都利用‘指针’来表达(另外还有 C++ 专属概念‘引用’)。同样的还有 struct 这个关键字,C 语言和 C++ 语言都有这个关键字,但真正的功能可像相差很远。对于 C 语言来说,结构体变量的内存长度、布局其实是比较简单的,但是 C++ 的对象可不简单,而且很多公司面试很喜欢问这个。
class Parent { public:     int iparent;     Parent ():iparent (10) {}     virtual void f() { cout << " Parent::f()" << endl; }     virtual void g() { cout << " Parent::g()" << endl; }     virtual void h() { cout << " Parent::h()" << endl; } }; class Child : public Parent { public:     int ichild;     Child():ichild(100) {}     virtual void f() { cout << "Child::f()" << endl; }     virtual void g_child() { cout << "Child::g_child()" << endl; }     virtual void h_child() { cout << "Child::h_child()" << endl; } }; class GrandChild : public Child{ public:     int igrandchild;     GrandChild():igrandchild(1000) {}     virtual void f() { cout << "GrandChild::f()" << endl; }     virtual void g_child() { cout << "GrandChild::g_child()" << endl; }     virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; } };
上面代码一个 GrandChild 对象的内存结构

上面代码一个 GrandChild 对象的内存结构

动态绑定:把指针玩出花儿来

C++ 在面向对象的多态上,几乎完全依靠“指针”。由于 C 语言当中,变量的类型决定了变量的内存数据,所以你一旦声明了一个父类变量,这个变量就时固定为父类对象了,再也没有机会用作任何的子类对象变量, C++ 也兼容了这一点,但是如果没有办法拿一个父类变量作为子类变量使用,动态绑定就无从谈起,于是 C++ 就借用了“指针”这个概念:所有类型的指针,内存长度都是一样的。于是 C++ 的整套面向对象的动态绑定(多态)机制,就都建立在指针上了。
Parent *obj = new Child()
如果对于指针搞不明白,不但 C 语言玩不转,C++ 也是基本没法用的。这个糟糕的星号,从 C 语言一直留到 C++。

静态绑定:真正的架构师语言

如果你希望写一套程序库,而且希望约束使用者的用法,那么你除了希望这个库有足够的功能外,肯定也希望编程语言能提供给你一些工具,能够让用户能足够灵活的使用你的库。特别是对于“有一部分”代码,你预期是使用者编写,然后放在你的框架内运行的情况,俗称“回调”,譬如说你写了一个 web 服务器的框架,希望使用者只用填写访问某个 URL 就执行的函数;或者说你写了一个游戏的框架,希望使用者只编写某个角色被击中的效果等等。
这种代码在传统的面向对象变成方法上,一般需要定义一个 interface,然后让使用者来实现。这种扩展方法,也是导致“类爆炸”的原因之一,因为使用者如果使用了多个框架,那么为了使用这些框架而写的回调函数,可能需要定义一大堆 interface。而 C++ 的另外一个特性,就很好的解决了这个问题,这就是“模板”功能。
有的人会认为“模板”特性,几乎是另外一种语言。然而“模板”特性被用在 C++ 最重要的组成部分 STL 里面,已经成为 C++ 这个三位一体语言(C语言、面向对象、模板泛型)不可缺少一部分。所以 C++ 如此的复杂,是因为其实整合了三类特性到一门语言中。“模板”特性虽然复杂,但是用来开发被复用的模块,却有非常大的好处:
  1. 不需要实现 interface,只要“语言签名”对的上,都能直接用
  2. 编译时会检查出所有的错误
  3. 通过使用泛型类的继承,可以实现对方法调用的“反射”
对于 STL(Standard Template Library) 来说,很多“类型”只要支持一些数学运算符号,譬如“等号”“大于”“小于”这一类,就可以由 STL 提供大量的数据结构工具(如 List/Map 等等),这让这个库成为应用最广泛的 C++ 库。
template <typename T> void const& DoSomething (T const& a)      a.DoSomething();
上面代码中的模板函数 DoSomething(),可以接受任何类型实现了一个叫 DoSomething() 方法的对象。这是不是比面向对象写法中,强迫用户一定要“先声明一个含有DoSomething的接口,然后让要用的类型实现这个接口”,要简单的多?

编程语言分类学

在MOBA类游戏中有一句话流传甚广,叫做“没有最强的英雄,只有最强的玩家”,这句话被许多玩家奉为经典。编程语言也是这样,好的程序员往往会精通好几门语言,并且在合适的情况下选择合适的语言,去解决问题。因此我们可以对各种编程语言进行不同维度的分类,以便更好的选择。

编译、虚拟机、脚本

  • 编译型语言具有最好的效率,也是历史最悠久的语言类型。C 语言就是这类语言的代表。编译型语言在环境兼容和内存管理方面,往往不尽如人意,但是还是有很多后续的编译型语言在这方面做了长足的改进,譬如 Go 语言。作为环境兼容性“差”的另外一面,编译型语言生成的程序,在已经兼容的环境中,部署方面往往会比较简单,譬如用 Delphi(Object Pascal) 写的 GUI 程序,编译之后只有一个可执行 exe 文件,拷贝到目标机器上就能运行。
  • 在“虚拟机”下运行的语言,往往都具有非常丰富的运行时动态特性,譬如 JAVA 和 C#,反射功能只是一个最基本的操作,它们还可以运行时更新代码,甚至可以支持很多不同的语言在同一个虚拟机上跑(如 Jython 就是用 Python 语言跑在 JAVA 虚拟机上)。比较有趣的是,几乎所有这类的语言,都号称要“跨操作系统”。事实上它们也基本上都做到了这点,但是真正用于编写 PC 或者服务器的跨操作系统的项目非常少,反而在手机、游戏领域,JAVA(安卓) 和 C#(Unity) 这些语言却应用非常广泛。最后说说性能,在 JIT(Just In Time)技术的加持下,很多虚拟机字节码,实际上拥有了和编译语言一样的基础性能,而那些无法 JIT 的代码,往往是编译型语言不支持的一些动态特性。所以除了部署安装这类语言编写的软件,需要额外按照个环境(JRE/.NET)以外,使用起来没有什么实质上的差异。
  • 脚本语言的历史其实一点也不比其他语言更短,尽管它们被认为是最容易学习,但性能最差的一批。这类语言的一般都具有所谓的“动态类型”特性,也就是你可以不理睬变量的类型,直接把变量思维万能的信息盒子。甚至一些常用的数据结构,也被一种很容易使用的方式嵌入在语言中,譬如世界上最好的语言 PHP 就可以使用其万能的[ ]中括号——它既可以是数组,又可以是列表,还可以是哈希表。脚本征服“跨操作系统”难题,采用的另外一种方法:让自己的源码变得方便移植。其实这个方法,C 语言很早就尝试过,所谓的 ANSI C,就是明白无法让 C 语言编译出来的程序在任何环境运行,那就让 C 语言的源码变得可以在任何环境编译吧,虽然这个尝试现在来看不是太成功,因为我们使用 C 语言的一个重要理由,就是用来对操作系统进行控制,不同的操作系统提供的 API 本身就差异很大。脚本类语言只需要在不同的操作系统上,实现一遍自己的解析器,就可以成为所谓的跨操作系统了。其中一些语言(譬如 Python),还会连带把自己的常用库也移植到不同的操作系统上,而另外一些语言,压根就没有什么库,它的设计目的就是“寄生”(嵌入)到其他语言编写的程序中(如 Lua),所有需要移植的“库”,都是被嵌入的那种语言自己需要解决的问题。
如果要选择一种语言来作为某个项目的开发语言,我一般会这样思考:
  1. 人是第一要素:也就是这个项目的开发人员,最适合用哪种语言。一般来说脚本语言的开发速度是最快的,如果是复杂多变的需求,首选脚本语言。
  2. 运行环境的依赖因素:如果要开发一个“跨语言”可用的库,C 语言几乎是唯一的选择;如果要开发一款游戏,希望运行在不同的平台上,使用带虚拟机的语言,或者是某种脚本语言,譬如 TypeScript 可能是一个好的选择,很多游戏引擎、框架本身也会选择这一类语言。如果开发的程序可预见的,和某些环境依赖非常密切,那么就不要自找麻烦,直接选择环境相关的“官方语言”。
  3. 性能几乎是最后的一个考虑要素,除非有非常明确的性能测试结果,不要轻易用这个标准来选择语言。
由于学习一门新的语言,可能会消耗很多精力和时间,所以一般我们感情上并不喜欢学习新的编程语言。但是,当年学会了第二门语言的时候,你才真正的懂一门语言,这句话在编程方面也是对的。而且学的语言越多,学习的速度越快,而且越能欣赏到这些语言设计者在解决问题时的思考。

通用型和专用型

上面讨论的大部分语言,都可以称为“通用型”语言;然而,编程中,我们往往还会碰到另一类编程语言,它们可以被称为“专用型”语言。最广为人知的可能是 SQL(Standard Query Language),我们用这类语言来操作数据库。还有一类被称为 Shell Script 的语言也很常见,譬如在 Windows 上的 .bat 和 .ps1 文件中,编写“批处理”命令,在 Linux 上则是 bash 以及其他各种 sh。我们常见的 HTML,实际上也是一种专用型语言,叫做“超文本标记语言”(Hyper Text Marker Language)。
select name, age, address from User where name = 'Tom';
从易用性上来说,一般“专用型”语言 DSL(Domain-specific language)相对会比较简单一些。因为这类语言比较少需要把“循环、分支”表达能力一起包含进去,甚至有一些用 JSON/YAML 的配置文件,都可以称之为一种 DSL。从这个角度来看,编程对人的要求其实并不高。
和通用型语言不同的,DSL 语言基本上都是只运行于某个特定的软件之内,所以使用 DSL 其实需要学习的最大负担,实际上这个宿主软件的功能。有的软件功能极其复杂,只好用通用语言来充当原来的 DSL,譬如在微软 Office 软件 Word、Excel 上面的“宏语言”,实际上是 VisualBaisc for Application,简称 VBA 语言,甚至曾经出现过用这个语言编写的“宏病毒”。

编程语言的理解思路:用什么手段,解决了什么问题

提高开发效率

虽然有人很热衷于讨论各种编程语言的性能表现,但是绝大多数编程语言都是为了写程序更方便而创造出来的。从汇编语言开始,到 C 语言,再到后面的 Go 语言等等。当我们在学习编程语言的时候,关注点应该更多是,一门语言到底用什么方法,去帮程序员提高开发效率。
譬如以 C 语言为例,if 和 while 关键字,就解决了大量的汇编上跳来跳去的问题,而 function 则对一个“子过程”提供了内存管理和代码跳转的很好抽象。又如 Java 语言,提供了标准的 JDK 让程序员有一个可用的基本类库,节省了大量自己造基础轮子的时间;内置多线程的支持,synchronise 关键字又简化了并发程序的编程方法。Go 语言可以返回多个返回值,一方面为错误处理提供了方便,另外一方面也避免了定义大量的结构体(类)。
各种语言的面向对象语法的支持,一般来说,都提供了多态支持,简化了程序扩展中的经典语法:switch case。而且多态也大大简化了程序员去学习和记忆大量相似函数库的工作。譬如多家数据库厂商,针对同一套接口推出各自的实现,程序员可以学习一次数据库的使用,就能使用多种不同的数据库。
对于已经掌握了一种语言的开发者来说,另外一种语言的用法,可能会让人感觉比较别扭,但是这背后的原因,可能是因为那种语言,在尝试解决一个其他语言没有去解决的问题。譬如 python 语言的代码块不是用大括号封起来,而是用的缩进,这样做是为了“强迫程序员写好缩进”,还有另外一个好处,就是不需要准确的为每个括号进行配对(虽然这个问题在现代 IDE 的帮助下已经不是问题了)。

跨平台

几乎所有的语言,都是希望跨平台的。这里的平台包含硬件平台、操作系统、宿主程序等等。但所有的跨平台能力,都需要付出一定代价:编译型语言的跨平台,就需要跨平台的编译器;虚拟机语言的跨平台,需要跨平台的虚拟机;脚本语言则需要跨平台的解析器。另外,跨平台还需要对平台相关的功能,进行一定程度的统一抽象和封装。譬如 windows 和 linux 的文件系统有很多差异,如果要跨平台进行文件读写,必须要抽象成统一的文件操作 API。

安全性

编程语言的安全性,除了包括可能出现的软件漏洞,还包括了减少程序员 BUG 产生的设计。譬如 C 语言由于对内存管理的支持很少,所以容易出现栈溢出漏洞、内存泄露、以及指针错误导致的崩溃;C++ 为此增加了一整套的 STL,在基本容器上减少了很多内存管理的 bug,但指针的使用依然很容易导致内存泄露和程序崩溃;Go 语言保留了指针,但不允许指针运算,而且自动管理全部变量内存,因此指针导致的 bug 被大大减少了。
Java 的异常捕捉“围栏”机制,强迫程序员处理每一个可能的异常,确实是一种提高安全性的好办法,但是这也让程序编写效率变低。Go 语言则使用错误返回的“惯例”来处理异常,开发效率是上去了,但是不免发生忘记判断返回值的问题。虽然 C++ 也有异常,但是因为没有内存管理,异常本身的内存分配反而容易变成一个问题,所以用的人需要更加小心翼翼。

和游戏有什么关系呢?

为什么是 C++ ?

游戏行业内,C++ 是最常见的一种语言。那么,到底为什么是 C++,而不是其他语言呢?有人会说,是因为游戏对性能要求比较高,同时业务逻辑也比较复杂,能承担这两点的语言,C++ 基本是唯一选择。这个理由,我觉得有一定的道理,但事情往往并不是简单的理论分析就可以看明白的。我觉得最主要的原因,是开发工具:这里最常见的开发工具,就是微软的 DirectX,这套库是 C++ 的,所以很多游戏就使用了 C++ 来开发。由于游戏团队中必须要用 C++,所以没必要增加其他编程语言,能用 C++ 的就也都用了吧。因此很多配套的游戏服务器端程序,也就用了 C++,毕竟团队比较熟悉。这也导致了为什么其他行业的服务器端,基本不用 C++,譬如电商、社区,而游戏服务器都是 C++ 的原因。

为什么不是 C++ ?

C++ 的开发效率实在算不上高。也有一些团队,从游戏服务器端开始,不用 C++,而是用 Java 或者 C#。由于 Unity 引擎默认支持的语言是 C#,所以服务器端也用 C# 也是一个常见的选择。说到底还是开发工具决定了语言。比较有意思的是,虽然 Unreal 的底层是 C++ 的,但是依然有很多团队会用 Lua 脚本来写逻辑。
使用脚本语言来写游戏逻辑,其实也是游戏的一个传统。Python、Lua、Js 在游戏行业内使用的都比较广泛。其中以 Lua 最为常见,因为这种语言的解析器非常小,性能也不错,很适合嵌入在 C/C++ 编写的其他程序中。作为脚本语言,还有支持“热更新”的优点,游戏的玩法变动非常频繁,这个特性对于游戏来说非常的重要。

Talk is cheap, show me the code

回想起来,为什么争论语言特性是程序员的一大“爱好”?原因除了大家都能参与、各自投入了心血以外,还有一个原因,就是写代码这件事其实比写文章要困难一些。如果我们能多写写几种不同语言的代码,很多的“争论”反而会成为我们深入了解这门语言的一个契机。在实践中比较,不管别人是否认可,自己的体会才是最重要的。

© 2022 3楼猫 下载APP 站点地图 广告合作:asmrly666@gmail.com