分享

为什么 Go 语言把类型放在后面? |

 pgl147258 2014-08-30

【林建入的回答(50票)】:

不是为了与众不同。而是为了更加清晰易懂。

Rob Pike 曾经在 Go 官方博客解释过这个问题(原文地址:http://blog.golang.org/gos-declaration-syntax),简略翻译如下(水平有限翻译的不对的地方见谅):

引言

Go语言新人常常会很疑惑为什么这门语言的声明语法(declaration syntax)会和传统的C家族语言不同。在这篇博文里,我们会进行一个比较,并做出解答。

C 的语法

首先,先看看 C 的语法。C 采用了一种聪明而不同寻常的声明语法。声明变量时,只需写出一个带有目标变量名的表达式,然后在表达式里指明该表达式本身的类型即可。比如:

int x;

上面的代码声明了 x 变量,并且其类型为 int——即,表达式 x 为 int 类型。一般而言,为了指明新变量的类型,我们得写出一个表达式,其中含有我们要声明的变量,这个表达式运算的结果值属于某种基本类型,我们把这种基本类型写到表达式的左边。所以,下述声明:

int *p;int a[3];

指明了 p 是一个int类型的指针,因为 *p 的类型为 int。而 a 是一个 int 数组,因为 a[3] 的类型为 int(别管这里出现的索引值,它只是用于指明数组的长度)。

我们接下来看看函数声明的情况。C 的函数声明中关于参数的类型是写在括号外的,像下面这样:

int main(argc, argv) int argc; char *argv[];{ /* ... */ }

如前所述,我们可以看到 main 之所以是函数,是因为表达式 main(argc, argv) 返回 int。在现代记法中我们是这么写的:

int main(int argc, char *argv[]) { /* ... */ }

尽管看起来有些不同,但是基本的结构是一样的。

总的来看,当类型比较简单时,C的语法显得很聪明。但是遗憾的是一旦类型开始复杂,C的这套语法很快就能让人迷糊了。著名的例子如函数指针,我们得按下面这样来写:

int (*fp)(int a, int b);

在这儿,fp 之所以是一个指针是因为如果你写出 (*fp)(a, b) 这样的表达式将会调用一个函数,其返回 int 类型的值。如果当 fp 的某个参数本身又是一个函数,情况会怎样呢?

int (*fp)(int (*ff)(int x, int y), int b)

这读起来可就点难了。

当然了,我们声明函数时是可以不写明参数的名称的,因此 main 函数可以声明为:

int main(int, char *[])

回想一下,之前 argv 是下面这样的

char *argv[]

你有没有发现你是从声明的「中间」去掉变量名而后构造出其变量类型的?尽管这不是很明显,但你声明某个 char *[] 类型的变量的时候,竟然需要把名字插入到变量类型的中间。

我们再来看看,如果我们不命名 fp 的参数会怎样:

int (*fp)(int (*)(int, int), int)

这东西难懂的地方可不仅仅是要记得参数名原本是放这中间的

int (*)(int, int)

它更让人混淆的地方还在于甚至可能都搞不清这竟然是个函数指针声明。我们接着看看,如果返回值也是个函数指针类型又会怎么样

int (*(*fp)(int (*)(int, int), int))(int, int)

这已经很难看出是关于 fp 的声明了。

你自己还可以构建出比这更复杂的例子,但这已经足以解释 C 的声明语法引入的某些复杂性了。

还有一点需要指出,由于类型语法和声明语法是一样的,要解析中间带有类型的表达式可能会有些难度。这也就是为什么,C 在做类型转换的时候总是要把类型用括号括起来的原因,像这样

(int)M_PI

Go 的语法

非C家族的语言通常在声明时使用一种不同的类型语法。一般是名字先出现,然后常常跟着一个冒号。按照这样来写,我们上面所举的例子就会变成下面这样:

x: intp: pointer to inta: array[3] of int

这样的声明即便有些冗长,当至少是清晰的——你只需从左向右读就行。Go 语言所采用的方案就是以此为基础的,但为了追求简洁性,Go 语言丢掉了冒号并去掉了部分关键词,成了下面这样:

x intp *inta [3]int

在 [3]int 和表达式中 a 的用法没有直接的对应关系(我们在下一节会回过头来探讨指针的问题)。至此,你获得了代码清晰性方面的提升,但付出的代价是语法上需要区别对待。

下面我们来考虑函数的问题。虽然在 Go 语言里,main 函数实际上没有参数,但是我们先誊抄一下之前的 main 函数的声明:

func main(argc int, argv *[]byte) int

粗略一看和 C 没什么不同,不过自左向右读的话还不错。

main 函数接受一个 int 和一个指针并返回一个 int。

如果此时把参数名去掉,它还是很清楚——因为参数名总在类型的前面,所以不会引起混淆。

func main(int, *[]byte) int

这种自左向右风格的声明的一个价值在于,当类型变得更复杂时,它依然相对简单。下面是一个函数变量的声明(相当于 C 语言里的函数指针)

f func(func(int,int) int, int) int

或者当它返回一个函数时:

f func(func(int,int) int, int) func(int, int) int

上面的声明读起来还是很清晰,自左向右,而且究竟哪一个变量名是当前被声明的也容易看懂——因为变量名永远在首位。

类型语法和表达式语法带来的差别使得在 Go 语言里调用闭包也变得更简单:

sum := func(a, b int) int { return a+b } (3, 4)

指针

指针有些例外。注意在数组 (array )和切片 (slice) 中,Go 的类型语法把方括号放在了类型的左边,但是在表达式语法中却又把方括号放到了右边:

var a []intx = a[1]

类似的,Go 的指针沿用了 C 的 * 记法,但是我们写的时候也是声明时 * 在变量名右边,但在表达式中却又得把 * 放到左左边:

var p *intx = *p

不能写成下面这样

var p *intx = p*

因为后缀的 * 可能会和乘法运算混淆,也许我们可以改用 Pascal 的 ^ 标记,像这样

var p ^intx = p^

我们也许还真的应该把 * 像上面这样改成 ^ (当然这么一改 xor 运算的符号也得改),因为在类型和表达式中的 * 前缀确实把好些事儿都搞得有点复杂,举个例子来说,虽然我们可以像下面这样写

[]int("hi")

但在转换时,如果类型是以 * 开头的,就得加上括号:

(*int)(nil)

如果有一天我们愿意放弃用 * 作为指针语法的话,那么上面的括号就可以省略了。

可见,Go 的指针语法是和 C 相似的。但这种相似也意味着我们无法彻底避免在文法中有时为了避免类型和表达式的歧义需要补充括号的情况。

总而言之,尽管存在不足,但我们相信 Go 的类型语法要比 C 的容易懂。特别是当类型比较复杂时。

【vczh的回答(55票)】:

我觉得,作为一个写了10年parser的人,应该站出来说,类型放在前面还是后面,对于parsing的过程一定点影响都没有。这只是一个品位问题,然后被go的开发者上升到一个哲学高度来忽悠你们的而已。如果不要上当受骗的话,就好好学习编译原理,自己写多几个parser,不要来知乎问这种问题了。

【邓毅的回答(4票)】:

这个也不叫与众不同,Pascal 就是类型在后面的,不过pascal类型前面有个冒号。

关于类型,官网上有一段仔细地介绍了一下函数指针的部分,现在的设计比起 C 的语法,清晰很多。

【Nogard的回答(6票)】:

不光说Go语言,把类型放在变量名后面确实比较好(我假设大多数人都这么认为),是有客观原因的。

1. 人类语言模式:

人类的大多数语言,在一般陈述语气中,语法大致上 是 主语-谓语,这样的语序和结构;并且这样的结构和语序历久不变

为什么?因为语言本质上是一种约定,即以简短的声音或简单的图形(即文字)来指代一种众所周知的知识、体验、自在物。人先听到或者看到语言(声音、文字),而后在脑中形成印象,前者是抽象的、简单的符号,后者——对自在物的印象——则是具体、丰富的体验或记忆。

并且,由于语言书写媒介的原因,阅读总是线性的;声音变化的时间维度也是线性的……

所以,所有的人,都习惯从符号 到 具体的这个阅读和理解过程。

编程代码的阅读,也是这么一个过程。因此有实用价值的编程语言,大致上遵循先定义后使用这么一个法则。因为必须先有“是什么”,而后才能有讨论“怎么样”的空间。概念需要一个名字,才可以进行交流。

由此我们可以总结出一条关于易读性的原则:先正名,后名实

拿 int a = 10; 这种简单的编程语句,根本无法揭示因为人类的语言想象能力而被掩盖的本质上的反人性。

借用 @vczh 博客上的一个例子(原文 2013年4月27日 随笔档案 ),谁要试试解读以下这段C代码?

typedef int(__stdcall*f[10])(int(*a)(int, int));

这个例子当然很极端。但重点是,我们应该尝试感受一下,采用易读性原则来设计,可以变成什么样?

var f: array[0..9] of function(a: function(x: integer; y: integer):integer):integer;

这是Pascal 语言的写法,可能很多人已经忘了Pascal的语法甚至没学过,但我还是比较有信心,有更多的人会觉得这种更易懂一点。

原因在于这个句子优先交代了f的存在,然后在冒号后描述了f 的概念。

假如我们完全摒弃简洁至上的思维,在C语言的基础上稍加演进,完全可以表达出同样的意思,并且有很好的可读性:

typedef f := array:ptr:__stdcall:(ptr:(int, int)->int)->int;

这里也是我极度不喜欢Go语言里面的那种自以为是的简洁的一个原因:空白并不足以让语法分析器——也包括人脑——很好地推进对代码的理解;而且实质上也是反直觉的。

typedef f := array:ptr:__stdcall:(ptr:(int, int)->(int, int)->int)->(int, int)->int;

Go语言应该庆幸甚少摊上上面的那个例子,不然它可能得这写(先此声明,我完全不懂Go语言,因此我完全有可能写错了):

f []func(func(int, int) func(int, int), int) func(int, int) int

人在思考应该怎么做的时候,通常需要或者为自己做一个足够显著的标志,以助于思维从一个状态转变到另一个状态。譬如在路口决定走或停,需要红灯或者绿灯来帮助判断,而面对时紧时缓的车流时,则犹豫的时间会更长。

标点对于文字阅读的作用也是如此。有人可能会说,在代码里面提供更多标点,但是解析时却并不是真的需要这些标点,岂不是倒容易让写代码的人容易犯错?

对于这样的说法,我举的例子是,我从来没见过写赋值语句的时候会忘记写等号的,而且理论上,解析一个赋值语句,完全可以用一个空格来取代“=”。然而这不是真正的简洁。

2. 从编译器演化的角度

现代的编程语言越来越表现出智能化的倾向,力图让程序员可以用更少的代码表达出同样精确的意思。像C++这种经典的、静态类型、强类型语言编译器中,出现像动态类型的、弱类型脚本语言中的类型推断能力,则是这一倾向的最佳例证。

正是因为编程中广义上的函数本身的性质,以及更深层的人类思维的特性,使得类型说明后置这种语法设计风格,成为类型推断优势的一种必然要求。

变量的类型推断通常出现在定义一个变量的场合中,并且这个符号在定义的场合中马上被初始化。因此变量的类型可以由函数的返回值类型来推断,例如这样:

auto it = vector.begin();

更为复杂一点的是函数调用的推断。

我们不妨这么简单地认为,函数的类型由两个部分组成:参数列表的类型,返回值的类型。

同时注意到一个基本事实就是,决定调用哪一个函数的依据——函数签名——在大多数编程语言里面,通常由函数名和参数列表组成,是不包括返回值这一部分的。

这导致了像C++这种允许函数重载(即字面函数名相同而参数列表不同的函数,会被视为不同的函数)的语言会遇到这样的一个困难:

template< typename T, typename U>auto mul(T t, U u)->decltype(t*u){ typedef decltype(t*u) NewType; NewType *pResult = new NewType(t*u); return *pResult;}

像C++11中的这种写法,C++11之前的应该怎么办呢?

在这个例子里,函数的返回值类型,由传入函数的参数类型决定,但因为返回值类型必须先于参数名定义,所以C++11之前很难表达出这个“由T类型的实例t 与U类型的实例相乘而得到的结果的类型”。

但是实际上,如果你认为 auto 这个关键字的出现其实很多余,赞同你的人应该很不少。因为C++11 必须保持对“函数返回值写在函数名之前”这一语法的兼容,而将auto添加到该位置以便于编译器处理语法。

由此可见,类型前置的写法就使得类型推断毫无用武之地。C/C++ 一系的语言,将函数类型的两部分拆开,中间夹住函数名的写法,甚至成为制约代码灵活性的桎梏。

【杨肉的回答(2票)】:

除了其他人提到的parser啥的之外(如果只说为了parser啥的方便,我觉得@vczh 说的完全对,这么设计并没啥实际意义)https://golang.org/doc/articles/gos_declaration_syntax.html 这篇文章提到了一个可读性的问题:

int (*(*fp)(int (*)(int, int), int))(int, int)

f func(func(int,int) int, int) func(int, int) int

前者到底是否难读懂可能因人而异,不过我个人是觉得后者却是容易读懂一些。

【saijichan的回答(1票)】:

应该回到原点思考问题。原点在哪里,就在命令式语言的老祖Algo 58/60。将类型放右边这语法,其实那是命令式语言老祖algo 58/60而来的东西,C语言只是algo 60的旁系,很多东西都有所改变,如赋值符号,Algo 60是用":="(其实最初是要“<=”这个符号做赋值符号,但当时的技术弄不出"<",所以就使用":"代替)来做赋值号(这在Algo 60的嫡系语言Pascal仍看得见),到了C中就被简化成"="。如果学过Flex的Actionscript 3就知道,里面的变量声明语法就是,如“var i:int;”这样的。个人感觉":"在编程语言中有“继承”的味道(当然除了前面说的那个赋值符号":="--因为那是由于当时技术限制被迫使用的一个替代解决方案),如C++、C#中就用冒号来声明继承。"变量:类型",其实可以这么理解变量为此变量所属类型的“子嗣”,语义可认为是对该类型的“继承”。

其实就就跟数组声明"[]"是放在变量前还是变量后一样(java中是前面也行后面也行,不过一般建议放在前面,即与类型放到一起;C#好像记得是强制要求"[]"放在变量前面,即将"类型[]"当成一个独立的类型看).其实多接触几种语言,多了解编程语言的发展历史,这些都不是什么奇怪的。

【王瑞期的回答(2票)】:

这样有几个好处的:

1:对于类型推导来说,实现起来更方便(这块得懂parser,编译器等的人来回答了)

2:人眼更关注左侧的东西(更看重),后置可以突出变量名而不是类型

3:后置类型与UML图是统一的

4:完整原因请阅读:http://stackoverflow.com/questions/1712274/why-do-a-lot-of-programming-languages-put-the-type-after-the-variable-name

【郑好的回答(0票)】:

Pascal出身?

【胡东卿的回答(0票)】:

UML也是name在type前面的, name: type这样,还有一些语言也是。说不上与众不同啦

【zetachow的回答(0票)】:

我觉得这种顺序更自然。虽然和长年累积的习惯不相符。但是开发一段时间会觉得更自然。

例如

XXX是一只羊(XXX is a sheep)

无论中文、英文都是这样的表达,因此,var abc string,更符合人类的语言表达。

个人观点...

当然编译器的原因、误会

【知乎用户的回答(0票)】:

reddit上面的一条评论"C declaration order makes the grammar unparseable without a symbol table, which is kinda evil. I'm glad they fixed it."

Go官方的FAQ的一句话"the language has been designed to be easy to analyze and can be parsed without a symbol table. This makes it much easier to build tools such as debuggers, dependency analyzers, automated documentation extractors, IDE plug-ins, and so on".

【知乎用户的回答(0票)】:

没学过go,但是类型放在后边的有好多,比如actionscript之类的。

【康晓晓的回答(1票)】:

go让名称放在第一位,但是我感觉在开发的时候类型才是最重要的

1. 当需要定义一个整形变量a。

心里是这样想的:我现在需要一个整形的变量,我要定义它,于是我先写一个int,再思考它的名字 a ,于是就这么写出来了int a 。而不是我写了个变量a,我得给它区分个类型int。

2. 在调用一个方法的时候,func(abdfsasdffdg int, bagressdgf string, csdgesredg bool)

那个go函数看的很乱,程序员其实根本就不怎么看参数名字是什么,而只是看需要传入什么类型,注意力只在于int,string,bool这三个,如果如上那么写,反而影响了视线,乱系八糟的。

func(int adsfasdfsdaf, string asdfasfasf, bool gwegasgs),这么写我只注意类型,就不受名称影响了。

3. IDE自动提示

go本身就是为快而生,定义一个结构变量Rectangle rectangle,当键盘敲下r时候,IDE会自动给出rectangle,直接回车就出来了,反过来就的自己一个字母一个字母敲上去,蛋疼啊

4. 至于go给出的解释,当遇到复杂函数时……

一个项目中能写几个复杂函数,为了去解决这么一点小问题就把优势给牺牲了

【许式伟的回答(4票)】:

symbol table问题不能一概而论,不同语言不同。go保持语法易解析(用var x int而不是int x或x int),考虑的绝不是自己编译器好不好写,其重心是被大家忽略的那句:为了Debugger、IDE或分析工具的便利。姜是老的辣,某些人光关注编译器技术,境界上不在一个档次。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多