前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >当我渡过计算机语言的海

当我渡过计算机语言的海

作者头像
韩伟
发布2024-04-28 12:40:59
650
发布2024-04-28 12:40:59
举报
文章被收录于专栏:韩伟的专栏韩伟的专栏

“PHP 是世界上最好的语言——(破音)”

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

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

数学和编程的关系

在我真正开始学习编程之前,我就听说过:“编程需要很好的数学能力”。由于我以前的数学考试成绩不算很好,所以一直都不觉得自己适合搞编程。想不到的是,由于接到一个兼职的工作,需要用到编程能力,从此走上了穿格子衫的码农生涯。

现在回头来看,“编程需要很好的数学能力”的这个认知,我起码犯了两个错误。第一个错误是,我把数学考试成绩等同了自己的数学能力;第二个错误是编程工作是一个具有广泛内容的事情,在很多领域并不需要你掌握很多高级的数学工具。

国内的数学教育,由于高考指挥棒的存在,所以大部分都是为了“解题”而设计的,而真正的数学能力,是抽象思维能力以及想象能力。很多做题高手,可以凭借海量的题目信息,以及高超的记忆力,去考出高分。但是面对需要复杂的逻辑问题,需要自己设计一些逻辑工具去解决问题的时候,往往并不能很好的解决。编程就是需要有抽象的理解能力,并且能通过想象力,在脑海中构建出一系列的概念,并且推理出方案的活动。而在一般的信息管理程序开发领域,我们要用的数学工具,最常见的也只有初中代数而已。如果你还写一点 2D 的游戏,可能会用到一些平面几何知识,如果做一些策略游戏,可能用到一点概率论或者仅仅是排列组合的知识。除此之外,很多高级的数学工具,在编程工作中都并不普遍,起码写个 APP 网购什么的是用不上的。

代码语言:javascript
复制
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

我在使用 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 程序往往是这样的:

代码语言:javascript
复制
#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 程序的时候,就算不写“链接”的命令,编译器也会自动帮我们链接这个库。

代码语言:javascript
复制
$ 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 语言代码阅读和学习比较困难的一个原因。
代码语言:javascript
复制
#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 中是很少出现的。异常处理就好像一个安全围栏,把你的程序保护起来。

代码语言:javascript
复制
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 语言代码并且调用!
代码语言:javascript
复制
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++ 的对象可不简单,而且很多公司面试很喜欢问这个。

代码语言:javascript
复制
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 对象的内存结构

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

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

代码语言:javascript
复制
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++ 库。

代码语言:javascript
复制
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)。

代码语言:javascript
复制
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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 韩大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 学习编程需要什么前提条件
    • 数学和编程的关系
      • 语文和编程的关系
        • 英语和编程的关系
          • 经济条件和编程的关系
          • C 语言为什么使用如此广泛
            • 操作系统的原生语言
              • 3L 和 ABI 规范
                • 数学符号关键字
                • 自带内存管理的语言们
                  • 内存为什么需要管理
                    • Java:你妈觉得你冷
                      • CLASSPATH
                      • 内存管理
                      • 异常机制
                      • 并发支持
                    • C#:我全都要
                      • Go:专门为服务器端设计的语言
                      • 面向对象语言是一个骗局吗?
                        • OO 三特性
                          • 类爆炸、构造器混乱和“基于对象”
                          • C++ 到底是什么?
                            • 并不是 C 语言
                              • 动态绑定:把指针玩出花儿来
                                • 静态绑定:真正的架构师语言
                                • 编程语言分类学
                                  • 编译、虚拟机、脚本
                                    • 通用型和专用型
                                    • 编程语言的理解思路:用什么手段,解决了什么问题
                                      • 提高开发效率
                                        • 跨平台
                                          • 安全性
                                          • 和游戏有什么关系呢?
                                            • 为什么是 C++ ?
                                              • 为什么不是 C++ ?
                                                • Talk is cheap, show me the code
                                                相关产品与服务
                                                容器服务
                                                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                领券
                                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档