小编按:本文系 Rust编程 公众账号的原创文章,作者为 失落的神喵。本着共同建设Rust社区的愿景,我们两个公众账号准备一起来做。
细心的朋友们可能注意到了一个很小的问题,Rust中struct的初始化赋值,是这样的语法:
let p = Point { x:3, y:5 };
其中,每个field的赋值,用的是冒号:而不是赋值号=。
实际上,在Rust的设计过程中,这个话题被多次的提起,许多人都建议希望在这里使用赋值号。原因很好理解:为了一致性。在Rust中,既然普通变量的赋值是用=,为何在struct初始化的时候又换成了冒号呢。一般情况下,冒号后面跟着类型说明,可是在struct初始化的时候,冒号后面跟着的是初始化赋值。这一点很不可思议。
谈到程序设计语言的一致性,是个很有趣的话题。一般来说,不一致的设计非常惹人反感。一方面,它提高了学习成本,使得学习者难以触类旁通举一反三;另一方面,它降低了程序语言的美观度,容易给人一些预料之外的感受。
一致性设计是每一个体面的设计者都应当注意的问题。在神雕侠侣中,郭襄长大后与杨过的初次相逢是在风陵渡,到了倚天屠龙记中,灭绝师太告诉周芷若,她的师父、郭祖师的徒儿叫做风陵师太。权利的游戏中,詹姆把布兰从楼上推下,后来他儿子托曼也是坠楼而亡。前后呼应,因果轮回。更不用说红楼梦里让人津津乐道的草蛇灰线伏笔千里。细细品味,回味无穷,所以它们才能成为经典。
Rust的设计者一开始就将“一致性”作为一条非常重要的设计原则来考虑的。在许多地方,我们都能感受到这一点。比如,数学运算、逻辑运算、流程控制甚至语句块,全部都是表达式,它们都有类型,任一表达式都可以嵌入到另外一种表达式中。再比如,下划线的含义就是占位符,这个占位符可以是变量,可以是类型,也可以是类型参数,或者是match语句中的默认分支。
fn function(_: Foo){}
let v : _ = vec![1i32, 2, 3];
let v : Vec<_> = vec![1i32, 2, 3];
match x { _ => default_action(); }
再比如“模式解构”,let语句能引入局部变量,它支持模式解构;函数的参数可以引入局部变量,它也支持模式解构;match语句if let while let语句,都能引入局部变量,它们都支持模式解构。不同的场景,同样的规则。清晰直观,有一种简洁的美感。
那我们回到本文开头的那个问题,来看看如果struct的初始化使用=,即foo { a = b } 是合法的语法,会产生什么问题。考虑以下语句:
if foo { a = b }
这条语句在语法解析阶段,可能会出现歧义,它可能被解析为if ( foo {a = b} ),也可能被解析为if ( foo ) { a = b}。再考虑以下语句:
for b in foo { a = b }
同样产生了歧义。foo { a = b } 是一个结构体初始化赋值,还是for语句的一部分?
这样的问题实际上有多种的解决方案,每种方案都有利也有弊。对于Rust设计者最终做的这个选择,究竟是好还是坏,笔者也不敢妄下结论,起码它不是一个非常坏的决策。
在大多数情况下,Rust的各个语法特性之间都保持了比较好的一致性。可是也有少数例外。除了刚才说的这个小问题之外,在泛型的设计上,也有一处非常明显的“不一致性”。在声明泛型的struct/trait/fn的时候,直接将类型参数写到尖括号内就可以。
struct Foo<T>{}
trait Foo<T> {}
fn func<T> {}
可是在调用泛型函数的时候,必须使用双冒号将泛型参数分开:
foo::<i32>();
因为如果不加上这个多余的双冒号分隔符的话,在解析以下这种语法的时候,会发生歧义:
(foo<A, b>())
这个表达式在语法解析阶段,可以有两种含义:
它可以被解析成一个泛型函数的调用,被小括号括起来了;
它可以看成一个tuple,其中有两个元素,一个是【 foo()】 。
因为逗号既可以用在 tuple 中分隔各个成员,又可以用在泛型参数列表中分隔各个参数。可能有同学又问了,那可以通过语义分析来解决这样的解析困难的问题啊,如果我们知道每个名字是类型还是变量不就可以区分开了么。可是Rust的语法分析器在设计的时候还有另外一条规则,即语法尽量做到上下文无关,越简单越好。如果语法规则变得更复杂,不仅意味着编译器的维护成本变大,更麻烦的是语法高亮、自动提示、静态分析等周边工具的开发更复杂。
当然,我们还可以通过其它方式来解决这样的冲突。比如,用单独的一种类型的括号来用于泛型,不与比较运算符冲突,不就可以了么。可是问题是ASCII编码下总共只有那么几种括号,其它的括号都有了更合适的使用场景。如果我们把选择范围扩展到ASCII编码外的UNICODE范围内选择,当然可以做到让所有的符号只有唯一的一个功能,不再有任何冲突。从这个思路发展下去,或许我们可以设计一种 emoji oriented programming。excited!
实际上,在绝大多数的编程语言中,都碰到过各种设计目标之间的冲突的问题,或多或少的存在着这样或者那样的“不一致性”。
拿C++来说,举个简单的例子,C++11标准以前,class 里面的 static 变量,只有整数类型可以直接在头文件中初始化,其它类型比如浮点数必须写到 cpp 文件中初始化。这样的限制毫无道理。
拿Java来说,基本类型是特殊处理的类型,它们不能被用在泛型参数中。
拿我们一个生活中的楼层作为例子来说,也存在不一致性。一般地面上的那一层,我们称作“第一层”,往上一层呢,就叫“第二层”,依此类推。对于地下的楼层,可以称作“负一层”,继续往下,就叫“负二层”,依此类推。看起来很一致,对不对。但是请大家再想想,从x层走到y层需要爬几层楼?如果直接使用y-x来计算对么?凭什么从-1楼往上爬一层就到1楼?-1+1=1符合直觉吗?
保持一致性一直是Rust设计者念兹在兹的一条设计准则。但在所有的地方都做到一致性非常不容易,关于这个问题,Perl 语言的设计者 Larry Wall有非常精辟的总结,一语中的:
The problem with being consistent is that there are lots of ways to be consistent, and they’re all inconsistent with each other. —— Larry Wall