Let’s talk about the history of operator precedence design in C. In a reminiscence email from Dennis Ritchie, the father of C, he recalled why some operator precedences in today’s C are “wrong” (e.g., both & and && have lower precedence than ==, whereas in Go, & is higher than ==).
From a type system perspective, the final result of expressions involving operators in if/while contexts is a boolean. For the bitwise operator &, the input is numeric and the output is numeric, while == must accept two numeric values to produce a boolean — therefore & must have higher precedence than ==. Similarly, == must be higher than &&.
However, early C didn’t distinguish between & and && or | and || — there were only & and |. At that time, & was interpreted as a logical operator in if and while statements, and as bitwise in expressions. So &, which could be treated as a logical operator, was designed to have lower precedence than ==, e.g., if(a==b & c==d) would execute == first then &.
Later, when && was introduced as a logical operator to split this ambiguous behavior, C already had a user base. Even though raising the precedence of & above == would have been better, such a change was no longer possible — it would silently break existing code behavior (b&c would first compute some value, then compare with a and d using ==). The only option was to place &&’s precedence after &, without correcting & (obviously, Go as a successor could easily make the right design since the distinction between & and && was already well established). But has Go’s design always been flawless? There’s a recent counterexample.
In the upcoming Go 1.16, there’s a similar “historical episode”: after introducing io/fs, the restructured os package added a new File.ReadDir method whose functionality is nearly identical to the existing File.Readdir (note the capitalization difference). Having functions with such similar functionality and names seems to contradict Go’s design philosophy of orthogonal features. Removing the old File.Readdir would make it more intuitive for users, but this faces the same dilemma as C — for compatibility guarantees, any breaking change is unacceptable. Both methods were ultimately kept.
今天来聊聊 C 语言算符优先级设计的历史吧。在C语言之父 Dennis Ritchie 的回忆邮件 (https://www.lysator.liu.se/c/dmr-on-or.html) 中曾提起过为什么今天 C 语言里有些运算符的优先级是 “错误” 的(比如,& 和 && 的优先级都比 == 低,但 Go 的 & 比 == 高)。
从类型系统的角度考虑,if while 环境下算符参与的表达式的最终结果是布尔值。对于位运算符 & 而言,位算符的输入是数值、输出是数值,而 == 则必须接受两个数值才能得到一个布尔值,因此 & 的优先级必须高于 ==。同样的原因 == 必须高于 && 。
可是,早年的 C 并没有 & 和 && 或者 | 和 || 算符的区分,只有 & 和 |。那时 & 在 if 和 while 语句中被解释为逻辑算符,并在表达式中作为位运算进行解释。所以能被视为逻辑算符的 & 被设计为低于 == 算符,例如 if(a==b & c==d) 将先执行 == 再判断 &。
后来在引入 && 作为逻辑算符将这种二义行为进行拆分时,C 已经有一定用户了,即便将 & 其优先级提升到 == 之前更好,也已经无法再做这种级别的改动了,因为这将在没有任何感知的情况下破坏现有用户的代码行为(b&c 将先取得某个值,并依次与 a、d 做 == 比较),只能无奈的将 && 的优先级放到 & 之后,却不能对 & 做任何修正(显然 Go 作为后继,& 和 && 的区别已经司空见惯,也就很容易做出正确的设计)。但 Go 的设计就一直都很完美无暇吗?最近就有一个反例。
在即将到来的 Go 1.16 中同样也有这样的"历史插曲": 在引入 io/fs 后,重新调整的 os 包中,增加了一个新的 File.ReadDir 方法,功能与已有的 File.Readdir (注意字母大小写)几乎完全一致,这种功能、名字都高度相似的情况,似乎与 Go 注重特性垂直独立的设计哲学相违背,删除老旧的 File.Readdir 固然能够让用户更加直观的理解应该使用哪个 API,但实际上这与当年的 C 面临的是同样的困境,即为了兼容性保障,任何破坏性的改动都是不可取的。他们最终都得到了保留。