為什麼 Go 語言把類型放在後面?
為什麼Go語言把變數或者函數的類型放在後面,這個是出於什麼樣的考慮呢?
這個也不叫與眾不同,Pascal 就是類型在後面的,不過pascal類型前面有個冒號。
關於類型,官網上有一段仔細地介紹了一下函數指針的部分,現在的設計比起 C 的語法,清晰很多。不是為了與眾不同。而是為了更加清晰易懂。
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: int
p: pointer to int
a: array[3] of int
這樣的聲明即便有些冗長,當至少是清晰的——你只需從左向右讀就行。Go 語言所採用的方案就是以此為基礎的,但為了追求簡潔性,Go 語言丟掉了冒號並去掉了部分關鍵詞,成了下面這樣:
x int
p *int
a [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 []int
x = a[1]
類似的,Go 的指針沿用了 C 的 * 記法,但是我們寫的時候也是聲明時 * 在變數名右邊,但在表達式中卻又得把 * 放到左左邊:
var p *int
x = *p
不能寫成下面這樣
var p *int
x = p*
因為後綴的 * 可能會和乘法運算混淆,也許我們可以改用 Pascal 的 ^ 標記,像這樣
var p ^int
x = p^
我們也許還真的應該把 * 像上面這樣改成 ^ (當然這麼一改 xor 運算的符號也得改),因為在類型和表達式中的 * 前綴確實把好些事兒都搞得有點複雜,舉個例子來說,雖然我們可以像下面這樣寫
[]int("hi")
但在轉換時,如果類型是以 * 開頭的,就得加上括弧:
(*int)(nil)
如果有一天我們願意放棄用 * 作為指針語法的話,那麼上面的括弧就可以省略了。
可見,Go 的指針語法是和 C 相似的。但這種相似也意味著我們無法徹底避免在文法中有時為了避免類型和表達式的歧義需要補充括弧的情況。
總而言之,儘管存在不足,但我們相信 Go 的類型語法要比 C 的容易懂。特別是當類型比較複雜時。
應該回到原點思考問題。原點在哪裡,就在命令式語言的老祖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#好像記得是強制要求"[]"放在變數前面,即將"類型[]"當成一個獨立的類型看).其實多接觸幾種語言,多了解編程語言的發展歷史,這些都不是什麼奇怪的。
除了其他人提到的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
前者到底是否難讀懂可能因人而異,不過我個人是覺得後者卻是容易讀懂一些。
沒學過go,但是類型放在後邊的有好多,比如actionscript之類的。
我傾向於認為是個人品位問題。
凡是說到象 C 那樣把變數類型放在名字前面不好的,從來都是用函數指針做例子,也從來只有這一個例子。問題是,C 語言里函數指針的類型並沒有放在變數名前面好不好!明明是極為奇葩地把變數名夾在類型中間好不好!拿這個來證明類型放在名字前面不好,能不能講點道理啊!
隨便 YY 一下,假設一門語言的函數聲明語法是這樣的:&
這一行代表 substring 是一個以 string, int 為參數,返回值為 string 的函數,那麼以函數為參數的函數,函數的數組的語法分別是:
&<&
&
和類型後置的版本比,可讀性區別很大么?
然後我這裡還有個額外的論據來支持「個人品位」的觀點:我有幸用過 Rob Pike 設計的另外一門語言,在那門語言里,聲明一個整形數組變數得這麼寫:var count_array: array of count: int;
大家感受一下 ……
早期學院派的ALGOL,typed lambda calculus等都是把type放後面的。21世紀的很多新語言的語法設計,更加受學院派品味的影響(。。)。譬如Go, Scala, Rust, Swift都把type信息放後面。當然不止是語法層面上。
另外提個小問題,賦值用 := 或者 &<- 來表示應該是比用=更符合」賦值「的語義的(非對稱)。當然習慣了C語系的語法後這也不是問題。我覺得,作為一個寫了10年parser的人,應該站出來說,類型放在前面還是後面,對於parsing的過程一定點影響都沒有。這只是一個品位問題,然後被go的開發者上升到一個哲學高度來忽悠你們的而已。如果不要上當受騙的話,就好好學習編譯原理,自己寫多幾個parser,不要來知乎問這種問題了。
偶湊熱鬧的,也扯不出什麼高大上的,說點偶理解的。
因為前面有 var 聲明變數了,並且能類型推導。如:
var p = 10
可以推導出 int 類型來。
然後要聲明類型如果:
var int p
看著廢,根據習慣要 var 幹啥還,直接
int p
好了
但是 var 得留著,還有類型推導得用呢。總不能不聲明就用?
p:=10
這個好,但是得賦初始值啊。
沒賦值光聲明 var 還得留著。
腫么辦啊,捉急啊。
掉個好了……
於是
var p int
還能說的通,反正前有 Pascal 和 AS 3 墊背呢。
就這麼快樂的決定了……
symbol table問題不能一概而論,不同語言不同。go保持語法易解析(用var x int而不是int x或x int),考慮的絕不是自己編譯器好不好寫,其重心是被大家忽略的那句:為了Debugger、IDE或分析工具的便利。薑是老的辣,某些人光關注編譯器技術,境界上不在一個檔次。
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給出的解釋,當遇到複雜函數時……
一個項目中能寫幾個複雜函數,為了去解決這麼一點小問題就把優勢給犧牲了
我覺得這種順序更自然。雖然和長年累積的習慣不相符。但是開發一段時間會覺得更自然。
例如
XXX是一隻羊(XXX is a sheep)
無論中文、英文都是這樣的表達,因此,var abc string,更符合人類的語言表達。
個人觀點...
當然編譯器的原因、誤會
相對C來說,類型放後面不就是省了一個void關鍵字了嗎
這樣有幾個好處的:
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
GO語言的作者顯然是個牛人,GO也有它自己的適用範圍。牛人就是這樣,寫出一面極其美麗,一面又極其噁心的東西,你要用,只能接受,不接受,就別用。
這背後的邏輯是:我就這樣,怎麼了,我就要這樣。
開始不喜歡OC,它那函數聲明相對C太古怪了,後面習慣了。開始不喜歡Python, 居然把縮進作為語法的一部分,而我一直認為,縮進與否,是代碼風格的問題。後面也習慣了。
開始不喜歡Go,好象一定要把變數放前面?好象大括弧也限定了?這種限制顯然把程序員的喜好帶進了編譯器,不得不說,SB。還沒喜歡,是因為還用不到。
尾部么,就是為了讓你覺得變數比類型重要,因為你第一眼掃過去就是變數名。
vczh說了,對編譯器沒啥關係,最後掃出來都是一顆ast的樹。沒啥特別的,只是能可選類型簽名的都會自然地把簽名放後面,比如近來流行的Scala和TypeScript都這樣。
a: Int = 1變成a = 1
Int a = 1變成a = 1
反正我覺得前者正常點。
如果類型簽名必填的話……在現在這個時代肯定火不了。
覺得好看而已,basic pascal也是這麼做的
也有好處,c要寫(*p).a導致出現了-&>,pascal的^.就順了很多
左右式都是選擇問題,這兩年流行這麼做,所以什麼go,swift都這麼做了;我倒是覺得c系的更自然,但也有可能是用多了感覺變了吧以前想過,世上能否推出一門語言, 代碼形式就是文檔格式
因為一個項目,文檔傳承太重要了...
所以以後做項目,就直接演化成寫文檔了.
另外讀起源碼來,就像讀文檔(小說)一樣..
現在看到GO的項目源碼, 尤其是聲明這塊兒, 真有種讀文檔的感覺,
很好,我支持這種變化, 雖然我寫起來真的不習慣....
UML也是name在type前面的, name: type這樣,還有一些語言也是。說不上與眾不同啦
推薦閱讀:
※新手關於如何看編程經典書的一些疑惑?
※網上常能見到的一段 JS 隨機數生成演算法如下,為什麼用 9301, 49297, 233280 這三個數字做基數?
※大學學的不是喜歡的專業怎麼辦?
※寫代碼時,縮進使用 tab 還是空格?
※計算機系學生,感覺自己編程能力很差勁,怎麼提高自己編程能力?