为什么关心?


对于任何一种人类语言来说,最好是即可以说出这用语言,也可以理解它。任何人都可以从互联网上复制粘贴

一段代码到他们的项目中,就像任何人都可以使用 google 翻译“说”法语一样。但是当那些复制来的代码没有

像 你想要的那样工作的时候,会发生什么?显然会有百万零三种可能,在这些令人苦恼和压抑的环境下,对这个语言有更多的了解就真正的起到作用了。尽管如此,我 不打算在这篇文章里对任何特定语言写些什么;我将会讨论一种小说式语言 CGCSL (Clinton’s Generic C Style Language 克林顿的通用c样式语言),就如名称所说,这些基本原则将(大部分)适用于任何c样式语言( PHP, JavaScript , Java小等)。


运算符优先意味着什么?


简单地说,这意味着一 些(数学或逻辑)运算符比另一些重要。这是非常重要的概念,没有这些关于先做那些指令的规则,计算机将会非常愚蠢。以 2 + 3 * 4 为例。最为人类,我们知道(学校就是这么教的)应该先做乘法,再做加法。如果没有这些规则,我们不知道结果应该是 14(2 + (3 * 4)) 还是 20((2 + 3) * 4)。幸运地是,发明计算机语言的聪明人定义了这些规则并且告诉了我们。通常它们会是冗长而枯燥的文档,但谢天谢地,我们有让这些文档稍微易读点的不讨厌 的人。比如,可以看一下 Mozilla Javascript 运算符优先级文档。这些规则有助于编译器(解释器)创建解析树,告诉它们需要做什么,以何种顺序做。


解析树


现在我们就进入了一个非常复杂的,在计算机科学领域被深层研究的话题。但是思想是很简单的。拿到代码并将其分割成片段,指定以何种顺序执行这些片段。显然除了一段小而简单的代码之外,这么做会非常冗长而复杂,所以这些规则最好用计算机数据结构来表示而不是人类可读的例子。


扯够了,来看看实例吧


首 先我们看看为简单的 2 + 3 + 4 * 5 生成一个解析树。结果是 25,对吧?但是我们是怎么得到这个结果的呢?首先,从把它分成更小的块开始,通过加上括号 – (2 + 3) + (4 * 5) 可以做到。现在对于一个(简单的)解析树来说就比较完美了,问题被分成小块,每块只做两个数的计算。在这一点上必须指出解析树并不总是只有两个运算数,但 这样有助于简单化。




读 取(我的)解析树的算法是深度优先遍历。总的来说:从头一直遍历到左边分支的底部(在这个例子里是 2)。返回上一级(到 + 处),从右边分支得到第二个参数。如果需要,以这个运算符为根,重复步骤知道所有的树被遍历。在我的例子中,阅读节点的顺序是:2(左分支) + 3 +(树根) 4 * 5。(和原来表达式的顺序一样,挺酷吧?)


其他有用的符号


i++

如你所知,这意味着把 i 增加 1(把 1 加到 i 上)。它一般(对可读代码来说)并且应该用在 for,while 和 do 循环里,它表示取得 i 的值,运算结束后将其加 1。 一个 CGCSL 的例子:

i=1;
print i++; // 输出 1
print i; // 输出 2

++i

看起来和 i++ 一样,其实有点区别。这次,要先将 1 加到 i 上,再使用得到的新值。因此 i++ 返回的值会比 ++i 小 1。

一个 CGCSL 的例子:

i=1;
print ++i; // 输出 2
print i; // 输出 2

(i=1) 和 (i=-3)

或许这一点你不知道,当你为变量指定一个值的时候,会返回等号右边的值。也许你不知道自己知道这一点,你很可能已经用上了。以下面的 PHP 为例(取自 PHP opendir 页面):

if ($handle = opendir('/path/to/files')) {
// 读取 opendir 所指的文档,如果可以打开一个目录,或者无法打开,返回一个目录句柄
// 因此这个指定值应该是个句柄,或者布尔值 FALSE
}

将其延伸到 (i=1),我们就会看到这段代码会:

a)将 i 的值设为 1

b)返回值 1


回到原问题上来


利用我们使用的新工具,我们尝试着消除代码歧义,创建一个解析树,并得到结果。从运算符优先级列表中,我们得知乘法运算要比加法运算重要,因此我们把问题分解成乘法的加法运算。(听起来吓人,加上几个圆括号就能更容易看到了)。

// 原代码
(i=1)*i-- - --i*(i=-3)*i++ + ++i
// 加上括号的新代码
((i=1)*i--) - (--i*(i=-3)*i++) + (++i)

让我们一块块的理解,找到答案,并把它们加起来。


(i=1)*i–

括号中的代码将 i 的值设为 1,然后返回值 1。乘法符号右边,我们取得值 1,并从中减去 1。这段代码执行完之后,我们知道:


这段代码之后,i 的值为 0

这段代码返回的值为 1 (1*1)


–i*(i=-3)*i++

我 们知道 –i 将 i 减去 1,然后返回得到的值,因此我们知道现在 i 的值和用于乘法的值均为 -1。接下来,将 i 的值设为 -3,圆括号内的代码返回值 -3,因此 i 的值和用于乘法的值均为 -3。最后, i++ 返回加 1 之前的 i 值,因此乘法公式的最后一个数是 -3,i 此时的值为 -2。我们知道:


这段代码之后,i 的值为 -2

这段代码返回的值为 -9(-1*-3*-3)


++i

终于到了这段代码最简单的地方了,把 i 加 1(得到 -1)并返回新值。现在我们知道:


这段代码之后,i 的值为 -1(我们不关心这个)

这段代码返回的值为 -1


把以上的值全部加起来

执行完这个计算式的各部分之后,把答案代入括号,而不是看起来吓人的表达式,最终代码看起来像下面的片段,我敢肯定我们都同意等于 9。


(1) – (-9) – (1)


转移你的视线!


我把这段讨厌的代码的解析树放到一起,但是我必须警告你,如果你从错误的叫顿看待它,它会让你变得木讷。或许你会发现 ++i 和它的朋友不见了,这是因为我把他们放进了类似 i+1 运算的位置。




最后的想法

像 我大部分的文章一样,我想用经典的 “只因为你可以做,不意味着你应该做”来结尾。这篇文章的目的是用相当flouncy和创造性的方式来解释运算符优先级以及为什么它很重要。想象一个这个 场景:你要维护一段代码,而开发者决定秀他们的技术并且把多行的 if/else 全部写入一行里面。我们不是在谈论一个一线的喜剧天才 – 单行编码相当伤人啊!


你和我都是那些“开发者”中的一员。我已经记不清有多少次我回顾那些“精彩的代码”的是胡不知道它是干什么用的了。 在我编程生涯这些年中(快10年了 – 怪吓人的),我学到了其实“精彩的代码”并不精彩。也许一时看上去很爽,但是真正的好代码应该多写点(并且注释)以便与在几年内更容易读懂。我最喜欢的编 程语录之一是:


“程序首先是写给人们看的,只是顺带着让机器执行。” -

Harold Abelson (Structure and Interpretation of Computer Programs, Second Edition)


现今的编译器和解释器非常聪明而且会在运行代码前将其优化,因此瞎摆弄代码没啥意义。当然了,重构你的算法比较有用而且应该这么做,但是使用难以理解而且令人讨厌的语言结构来优化代码就没必要了。