各种编程语言之间的对比学习
断断续续的,我已经学过、用过很多门编程语言了,觉得这个过程非常有意思。然后最近心里一直有个想法,各种语言放在一起对比一下,会不会更有意思呢?
首先,各种语言都有相同的地方。最大的相同,我觉得应该是这个: 所有的语言,都是文本。所以有一些通用的文本处理工具和技能,可以用于搜索、阅读各种不同语言的源代码。这些工具应该包括(但不限于):
- 文本编辑器。比如 Vim 或 Emacs(我个人使用 Emacs)。
一般的源代码阅读、编写,都可以通过编辑器来进行,并且可以通过安装一些辅助程序也就是通常所说的插件,达到更好的代码自动格式化、色彩高亮等“酷炫”的效果。
- grep、find 等简单文本、文件搜索工具
比如搜索某个函数在哪里被定义、使用等,如果程序不是特别大的话,可以通过 grep 来满足这些需求
- 搜索引擎
如果整个系统的源代码特别庞大,也可以考虑加入本地搜索引擎的索引,提升代码搜索的速度
- 学习查看、阅读、检索文档
不管是哪种语言、库、工具,都会有文档教你如何使用这种语言。除了纸质书之外,有很多通用的文档系统,比如Unix下经典的Man手册、Info手册,html文档,Internet资源,Google搜索引擎,StackOverflow、Wikipedia,等等等等,熟练掌握如何灵活使用这些资料,对快速掌握一门语言,是非常有益的。
诸如此类还有很多相通的地方,此处不一一枚举了。注意这里并没有提到集成开发环境,比如 Eclipse、IntelliJ IDEA 等工具,主要原因是这些工具往往只对某种或某几种语言支持的更加完善,局限性比较大,并且不够有趣。
而上面提到的这些通用的工具,正因为其通用性,所以你用 A 语言编程掌握了的技巧,可以方便的转嫁到 B 语言编程上。
所以接下来从各种编程语言的以下几个方面对它们做一些对比。
- 数据结构
各种最基本的数据类型,比如数值、字符串、数组、关联数组等等。
- 语法
各种最常见的“句型”在不同语言中的表达方法。尤其是各种“俚语”(idiom)。
- 文档
不同的语言,大概应该怎么搜索文档。
- 工具
讲一下 beagrep 等工具与编辑器等结合使用。各种自动补齐。
- 其他
语言之间相互作用(调用),会产生怎样有趣的“化学反应”?
1 数据结构
1.1 数值
各种编程语言的数值类型的数据结构应该基本都是一样的,整数、浮点数、加减乘除求余数,不需要太专门的学习,因为从小到大一直在数学课上学。此外可能有的语言有一些专门的函数库,比如 sin、cos 等三角函数。
这里面还包括各种数值类型,比如各种整型(int、byte、short、char、signed、unsigned、long、long long)、浮点型(float、double)等等等等,以及它们各自的记法。各种进制,二、八、十、十六进制等。
数值之间的转换,比如从 float 强制转换为 int。在有些脚本语言里,甚至可以从字符串直接转换为 int。比如在 Perl 里,int("5") == 5
;甚至 "5" + 6 == 11
。这种现象在编程语言里有个术语,叫类型检查,比如 Java 这种语言,类型检查非常的强,上面 "5" + 6
这种写法是绝对禁止的;而 Perl 则属于没有类型检查的,或者说,它的类型检查比较弱。
类型检查给了程序员很多帮助,比如可以提前发现程序里的一些逻辑错误,同时也给程序员带来额外的负担,主要体现在表达起来变得更啰嗦了——每个变量必须声明其类型等等诸如此类的。
像 Perl 允许写 "5" + 6
,甚至也允许写 "x" + 6
,其结果为 6,因为 "x"
的数值被认为等于 0。有些脚本语言则会禁止写类似于 "x" + 6
这种语句,比如 Bash 等。
1.2 字符串
在所有的语言里都一定会有字符串。所以熟练掌握对字符串的处理函数、命令、工具,对编程的帮助是非常巨大的。因为所有编程语言最后写出来的源代码,其本质都只是一个可能特别特别长的字符串。
1.2.1 编码
字符串都需要编码。这一点对于我等非英语母语的中国人来讲,问题更突出一些。
- 单字节编码。一个字符只占一个字节——byte,可想而知,只能表达 128(或 256,区别不大)个字母。一般的 ASCII 编码就是单字节编码。
- 多字节编码。有很多种编码方式,比如 gbk,比如 utf-8。
这种编码有很多是不定长的,其中像 gbk 编码的规则相对简单,前面 128 个字与 ASCII 码相同,后面的则全部是两个字节表示一个字(一般是中文汉字,也有日文假名、汉字等)。
而 utf-8 编码就更复杂了。一个字编出来的码可以是 1 个字节,也可能是 2 个字节,3 个字节。等等等等。
在这里我个人遇到一个特别严重的坑,那就是错误的把字符 character 和字节 byte 给等同了起来,因为一开始学习的就是 C 语言,坑爹的是 C 里面内置了一个 char 类型,就是 8 个 bit。所以后来很长时间无法理解多字节编码是怎么回事儿。
- 统一编码
后来国际标准组织制定出了 Unicode 统一编码标准。这里所有的字都用双字节或者 4 字节来表示了。如果想表示一个字符串的话,不能用 C 语言里的 char[]字节数组了,而必须改成用 short[]甚至 int[]数组(会用 typedef 定义一个 wchar_t 的类型)。
Java 比较背,在 Unicode 还没有成熟的时候,决定了用两个字节表示一个 Unicode 字,结果后来发现两个字节(16bit)是不够用的,必须用 4 个字节,所以后来又作了一些扩充,这个就非常复杂了。
1.2.2 各种字符串相关的函数
- 求字符串长度
比如 strlen。注意只有在 ASCII 编码的情况下,strlen 返回来的值,真正代表了里面有多少个字码,否则只是代表这个字符串占用了多少个字节。
如果是多字节编码,想知道里面有多少个字码,建议先将其转换成 wcs(wide char string,宽字符串),然后再用 wcslen 函数。
比如这个字码:“你”,用 utf-8 编码表示的话,应该有 3 个字节。但用 wcs 表示的话,它占用 2 个或 4 个字节,但 wcslen 返回为 1(因为只有一个字码“你”)。
- 字符串拼接,strcat 等等
有些语言必须用专门的函数来表示字符串拼接,比如 C 语言里的 strcat。但有些语言里,字符串拥有自己专门的运算符,比如在 Python 里,字符串通过加法符号“+”来进行拼接。
Perl 语言比较“特殊”,其设计者认为,“+”运算必须是拥有交换性的,
a + b = b + a
,而字符串的拼接不满足此规则。所以 Perl 里用.
来表示字符串拼接。Lua 语言里则使用两个点:"a" .. "b" = "ab"
。这个区别非常有意思,不小心的话会搞混掉。但稍微熟练一点就不会了,尤其在你短期内大量使用某一语言的情况下。- 字符串的乘法
Perl 是比较变态的,除了“加法”外,它还对字符串提供了“乘法”操作符,使用的运算符是
x
(字母 x),使用的方法如下:'hello world' x 2
,得到的结果是'hello worldhello world'
。个人感觉这种内置的运算符并不是非常有用,使用的场景不多,如果是一个设计得非常好的语言,不应该提供太多这种花哨但不实用的功能,因为这种功能太多了,是会分散使用者的注意力的。所谓乱花渐欲迷人眼… 这个功能最大的使用场景可能是在字符串的格式化的时候,比如在以前的终端界面下,要出报表,主要是英文为主,中间要使用大量的空格来保持对齐等等。在其他语言里,Emacs Lisp 有个 make-string 函数,但它只用把一个字符 c 乘以一个整数 N,最后相当于把这个字符 c 重复了 N 遍。
- 字符串的乘法
- 字符串的格式化
最早学习的是 C 语言的话,大家最早学会的函数应该是 printf,它支持把字符串格式化后,然后输出到 stdout 标准输出设备上。
这里最有用的功能可能还是一些对齐相关的,比如
printf("%08d", n)
,如果 n 不足 8 位数字的话,前面补 0,以保证最后的长度是 8。又比如%08s
,用于格式化字符串的话,前面会补空格以保证最后长度为 8(这个用于对齐是非常棒的,但同样,可能是以前的字符终端界面的遗产)。注意,我个人在学习各种语言的过程中,会非常纠结于其他语言里是不是也有类似于 C 语言里的
printf
函数,如果没有的话,就会觉得有点失落,其实这是没有道理的。其他语言说不定有更好的,也说不定printf
本身其实并没有那么好,只不过是我个人有点恋旧罢了。嗯,以后还要克服一下这种失落感,要不然跟不上时代了呢。比如上面的对齐,也就是在全英文、终端环境下还有点儿用吧,你想在网页、Word 文档里对齐,那还得用别的手段。尤其是要写更严肃的学术文章,最好的排版工具,还得是 Latex 呢。而且,很多情况下,一定要提醒自己,格式、排版并没有那么重要,内容本身才是最重要的。 - 其他
跟字符串相关的函数还有很多,比如查找一个字符在字符串中出现的位置、比较两个字符串是否相等。这类函数建议在一种语言里基本全部掌握,之后不管到哪种语言里,都可以找一找类似的函数,基本上额外的学习成本是可以降到 0 的。
1.2.3 正则表达式
说到字符串,一定要说一下正则表达式。
各种语言里都有正则表达式,在一种语言里学会之后,到其他所有语言里都可以融会贯通。唯一一点,就是注意一些细节上的差别,不要想当然,生搬硬套,不确定的时候就仔细阅读文档或者做些小实验,别碰一鼻子灰再回来看文档,这样挫败感比较大。
比如 Perl 里正则表达式主要有 3 种常见的用途:
- 匹配:
m/x/
- 替换:
s/x/y/
- 抽取:
m/(x.*y)/
然后使用$1
变量
在第一条匹配上,Perl 就跟其他语言比如 Python、Java 有很大的区别,在 Perl 下,$str =~ m/x/
只要 x
在 $str
里出现,不论它出现在什么位置,匹配都是成功的。但 Python 里的 re.match("x", str)
,要求 x
必须出现在 str
的开始位置才能匹配成功。在 Perl 里只要加一个 ^
anchor 就能解决的问题,为什么 Python 里要提供一个单独的函数呢?注意,加了这个函数之后,如果我不想匹配开始位置,而是匹配任意位置的话,我有两个选择,一是用 re.match(".*x", str)
,二是用 re.search("x", str)
——这是多么的容易让人记混啊!这个问题我是有点想不通的。想不通的问题,就不想了,无奈,但默默的接受。这可能是个兼容性的问题。毕竟工作了这么多年,我也写过很多没用的函数,这样的函数写出来如果都是自己用的还好,但一旦给别人用上之后,想删都删不掉的。因为删掉可能就意味着用户的流失… 这个问题,我觉得 Linux Kernel 解决的就比较好,严格的划分了内核空间和用户空间的界限,用户空间的兼容性是至高无上的,内核内部则经常出现旧的接口被改良、甚至被淘汰的事情。
1.3 数组
讲完字符串后,马上就讲数组是比较合适的。因为字符串其实就是一种数组,只不过是比较特别的数组,尤其考虑到多字节编码的情况下,从这样的字符串数组里取一个元素出来,可能不是很有意义(除了与编码本身相关的问题可能有点意义,比如“你”这个字用 utf-8 编码后,第二个字节是什么?)。
注意数组的类型有很多,取决于我们从哪个角度来观察这种数据。
- 取长度操作/长度属性。
一个数组的长度是多少?这可以用一个函数来计算,也可能一个数组本身带有一个属性,可以直接告诉你答案。
- Perl
在 Perl 下,有个函数叫
length
,它可以告诉你一个字符串的长度。注意多字节编码和统一编码(Unicode)下,字符串的长度是不一样的,下图中“你”字在 utf-8 编码时长度为 3,统一编码后长度为 1。注意
length
只是给字符串用的。如果要知道一个数组的长度,Perl 下使用的方法是scalar @array
。 - Java
Java 下知道一个字符串的长度,用的函数是 String 类自己的成员函数
length()
。Java 下想知道一个数组 Array 的长度,用的是 Array 类的成员变量,
length
。上面的这种情况,是非常让人崩溃的。我自己经常记不住什么时候用成员函数(后面加括号),什么时候用成员变量(后面不加括号)。
- Python
Python 下不管是什么数组类型,取长度用的都是同一个函数,len。String、Tuple、List 都是用这个函数取长度。这是一个全局函数(虽然这种现代的语言对于什么是全局函数是很狡滑的,比如 Python 的全局函数,其实都是定义在
__builtins__
模块底下的函数)。 - Ruby
Ruby 下一切都是对象,所以每个数组类型也都对应着一个类,这个类有一个名为
length()
的成员函数。 - Lua
Lua 下面没有传统意义上的数组,只有一种叫做 Table 的数据类型,既可以当普通数组用,又可以当关联数组用。当普通数组用时取长度的话在前面加一个
#
就可以了。比如#{1, 3, 5, 7, 9} == 5
。注意 Lua 的这种设计,非常有意思,它给了你你想要的,同时也给了你一些你不需要的。比如以 0,1,…为下标的普通数组,Lua 通过 Table 机制,显然就可以提供。但如果你的某个 Table 只有下标为 1 和下标为 10 的两个元素的话,这算一个普通数组还是关联数组呢?它的长度应该是 10 还是 2 呢?我的建议是不要考虑这种问题,没有太大的现实意义,现实中碰到的机会不大,就像上面提到的多字节编码字符串里面随便取一个字节出来一样,实际意义并不大。
- Lisp
Lisp 有很多种方言。在我最熟悉的 Emacs Lisp 里, 所有 Vector 类型的数据,可以用 Elisp 自带的
length
函数来求其长度。 - Bash
在 Bash 下,求数组长度的写法是
${#array[@]}
- Perl
- slice 操作
Slice 中文意思是切片,从一个数组里切出一个或几个小片断来。
1.4 关联数组
关联数组有很多名字,比如叫 Map(映射),Hash(哈希),甚至还有叫 HashMap 的… 一定程度上说明这种数据类型的实现基本上都以用 Hash(哈希)算法实现居多。
1.4.1 关联数组的构造、输入
- 无内置关联数组
C、C++等语言,并没有语言本身内置的关联数组,而是通过标准库来提供的。所以这种语言里想初始化一堆关联数据的话,是需要稍微更啰嗦一点的。
还好 C++语言本身有个运算符重载的机制,所以使用起来的话,假设 m 是一个 map 变量,可以直接用
m['hello']
的写法。- Java
在 Java 里用 HashMap 或其他类似的数据结构都要通过成员函数来进行:
HashMap<String, Integer> x = new HashMap<String, Integer>(); x.put("hello", 1); x.put("world", 2); System.out.printf("%d\n", x.get("hello"));
习惯了就好…
- Java
- 有内置关联数据
- Bash
declare -A assocArray assocArray=( [hello]=1 [world]=2 ) echo ${assocArray[hello]}
- Lua
Lua 里面的关联数据和普通的数组内部都用同一种数据结构来表示,就是 Table,只不过前者用任意的数据作为下标,后者用整数作为下标(事实上,Lua 的整数下标是从 1 开始的,与绝大多数语言从 0 开始不一样,你会因为这个而拒绝使用 Lua 语言吗?)。
x = { ['hello'] = 1, ['world'] = 2, } print(x['hello'])
注意上面的写法跟 Bash 是有点相似之处的,除了一个用圆括号并且等号前后不能加空格,一个用花括号并且空格可以随便加。
- Perl
Perl 下的数据结构很有意思,普通数据前面加一个
@
(助记法:这个符号里面包着个a
字,象征着 array),关联数据前面加一个%
(助记法:这个符号里面有两个互相“关联”的小圈圈)。普通数组用[]
引用,关联数组用{}
引用(不然的话就不能区分是普通数组还是关联数组了,Perl 里$@%
是变量名不可分割的一部分,一个程序里既可以存在$x
,也可以存在@x
等等):%x = (hello => 1, world => 2); @x = (1, 2); print $x{hello}; print $x[1];
另外注意这里
hello
和world
因为是一个 identifier,所以不需要加引号。加上引号的效果和不加是完全一样的。Perl 里有很多这种耍小聪明的地方,一开始的时候我还蛮喜欢的,现在其实也还蛮喜欢的,但就好像见到一个总在使劲讨好别人的家伙一样,隐隐会觉得这个样子是有点儿问题的。好像能给你省点儿事儿,但多了之后谁又能全都记得住呢?规则简单一点,让用户啰嗦一点,理解起来也简单一点;规则复杂一点,用户可以各种省事儿,但理解起来也更费劲了。这些语法糖啊,就像真的糖果一样,我很喜欢吃,但吃多了真的可能是对身体有害的呢。
好了,说到这里也就差不多了,世界上的语言那么多,每种语言的特性也那么多,用穷举的方法把所有东西都列完是不现实的,就像所有自然数,数是数不完的。但我们可以把里面有点共性的东西抽出来,那就简单多了。比如无穷无尽的自然数,只要 5 条皮亚诺公理就可以概括了。
Lisp 语言学习的一本非常经典的小册子,“little schemer”,里面甚至可以没有整数数据类型,因为所有整数都可以用数组(其实是链表)来表示:整数 0 就是一个长度为 0 的 list,整数 1 就是一个长度为 1 的 list…整数 N 就是长度为 N 的 list,整数加法就是 list 的拼接…这种思路让我叹为观止。
很多语言还允许你自己定义数据类型,比如 C 里面可以用 struct,C++里可以用 struct、class,等等等等。我认为它们其实就是自带成员函数的关联数组而已嘛。C 里面
x.y
(一个带 y 成员变量的名为 x 的结构变量对 y 的引用) 和 Python 里面 x['y']有很大的区别吗?C++的类之间继承的实现,一般是有一个隐藏的 vtable(virtual method table)结构成员变量(注意,结构,我们已经说过就是关联数组,至于是不是隐藏的,其实没那么重要),最后还是七拐八拐的拐到一个函数上面,这跟 Python 里类的继承也是差不多的。 - Bash
2 语法
语法其实没太多好说的,所有的语言都有一些类似的基本构造,比如条件语句,循环语句,等等等等。
如果对新学的一门语言里的语法记得不是很清楚的话,建议赶紧查文档。
3 文档
提到文档的话,我想说一个标准,一个文档系统越 容易 查询,我们就认为这个系统越好用。毕竟现在已经不是 Linus 开始搞 Linux 的时代了,那时人们编程,文档可能就是厚厚的一本“386 汇编手册”。
参考这个标准,我以前用 Visual Studio 的时候,觉得 MSDN 这个文档系统真是了不起,哪个函数不明白,按一下 F1,直接就从 Visual Studio 跳转到 MSDN 的窗口里,并打开相关函数的帮助页面。
也可能当时觉得真心牛逼的最大原因,是从不知道有 MSDN 帮助,到突然发现有个 MSDN 帮助系统,所带来的巨大的冲击。
从那以后使用任何工具,都是文档先行,碰到问题,甚至碰到问题之前,第一时间就问一下自己,万一那什么的话,我该去哪里查文档?这个系统有没有一个大而全的文档帮助中心?毕竟,文档如果分散得这儿一块、那儿一块,对用户的价值就大打折扣了。
后来我发现 Emacs 也非常了不起,自带文档帮助系统。系统里所有的 Man 手册,所有 Info 手册,都可以在 Emacs 里打开看。尤其是 Emacs 还可以自己扩展,定义一些快捷键,方便的跳转。
没错,光有一个大而全的文档系统还是不够的,最好还必须有个像 MSDN 那样的一键跳转的功能,甚至要比它更方便。如果没有这个功能的话,我就会想办法在 Emacs 里自己扩展一下,在此之前心里都像有个毛毛虫一样的痒痒。毕竟,人生苦短,能 2 步走完的路,就不要走 9 步。
比如 Qt 的帮助文档,一般都用 Firefox 来看,那我在 Emacs 里会定义一个快捷键,按下去就自动把当前的关键字抽出来,然后自动用 Firefox 打开相关的 Qt 帮助页面。
另一个需要注意的问题是,代码其实也是一种文档,所以很多时候阅读文档、搜索文档觉得费劲的时候,我可能直接就阅读代码、搜索代码了。比如,我想知道 Javascript 底下的字符串,都有哪些相关的函数?这个问题我可能通过 Google 也能很快找到答案。但如果通过代码的话,我会下载 Firefox 的源代码,稍加研究后发现字符串相关的定义是在 js/src/jsstr.cpp
这个文件里,然后我就可以用我掌握的各种 Emacs 编辑技巧从这个文件里提取我需要的信息了。这一点 Firefox 里阅读 Google 出来的文档是无法做到的,打个比方说,我用 Firefox 的话,很多信息作者写的是什么样,我看到的就是什么样,如果作者给了一个数据表格,但没有给一个柱状图的话,我就无能为力了;但如果我是在 Emacs 里,画一个柱状图、饼图可能易如反掌,只有一个快捷键的距离,然后信息就变得更直观,一目了然了。这个区别是由 Emacs 是一个可扩展的编辑器决定的。就像在阅读纸质书的时候,你无法快速的进行全文搜索;在 Firefox 里,你无法快速的对信息进行编辑、排列、组合,换一种更好的方式呈现。
4 工具
4.1 编辑器
学习多种语言,一个很重要的工具是编辑器。推荐 Emacs。强烈建议阅读一下 Vim 作者写的那篇文章(Google “高效 编辑器 七个习惯”,我也有翻译过,所以搜出来第二条就是我的博客)。
Emacs 下有一些通用的插件,比如缩写,文本补齐(yasnippet、我自己写的 bbyac)。等等等等。
另外还有一些特定语言定制的插件,比如 C/C++、Python 等都有智能的上下文补齐。
4.2 搜索工具
然后是搜索,Google 搜索,本地搜索。除了纯文本搜索之外,因为代码都是有层次结构的,所以 tags 搜索也是非常重要的(主要指搜索类、函数、变量的声明、定义、引用的位置)。
本地搜索推荐我写的 beagrep、beatags 系列。也可以使用 opengrok。
4.3 自己
当然,最重要的工具,还是自己的脑子,仔细想好自己想解决什么问题,该学习的时候学习,然后该放下的时候放下,全力解决最重要的那个问题。
学多种语言,除了好玩之外,主要还是希望能开阔眼界、思路,最终要能更好的解决问题。像上面我写的这样,过于纠结一些茴香豆的“茴”字有几种写法之类的问题的话,就不好了。
通过多种语言之间的对比,可能可以刺激到大脑皮层的不同部位,最后 达到高潮的时候,来得更强烈一些呢 记得更牢一些、理解得更深刻一些呢。