在參與這個(gè)討論 http://www.iteye.com/topic/33971 后,這段時(shí)間對(duì)這個(gè)話題有了一些新的思考,寫(xiě)下來(lái)和大家分享分享。
重點(diǎn)探討一下動(dòng)靜態(tài)編程語(yǔ)言的語(yǔ)義,兼帶些DSL及通用語(yǔ)言,以及新手上手難易問(wèn)題。
編程語(yǔ)言的語(yǔ)義,在論壇里討論不多。在這里先分析一下幾門(mén)主流靜態(tài)語(yǔ)言,C,C++,Java,C#的語(yǔ)義。這些語(yǔ)言從編程風(fēng)格角度講,都稱之為”imperative programming language”,(命令式的編程語(yǔ)言)。究其原因,這些語(yǔ)言都是對(duì)計(jì)算機(jī)的核心部件,CPU及內(nèi)存,施發(fā)號(hào)令的。
1. int a = 4;
2. int b = 4 + a;
3. int c = 5.2345;
第一句,具體語(yǔ)義是,在內(nèi)存里分配一塊內(nèi)存,大小為4 bytes,在這塊內(nèi)存里,寫(xiě)入4。第二句,具體語(yǔ)義是,在內(nèi)存里分配一塊內(nèi)存,大小為4 bytes,從a中取值,和4進(jìn)行加法運(yùn)算,結(jié)果寫(xiě)入b指向的4 bytes內(nèi)存。第三句就是個(gè)潛在的錯(cuò)誤,等號(hào)右邊是個(gè)8 bytes的double,把8 bytes的數(shù)據(jù)寫(xiě)到4 bytes的內(nèi)存塊里去,數(shù)據(jù)會(huì)損失的。
要把這些靜態(tài)語(yǔ)言內(nèi)存分配的經(jīng)驗(yàn)照搬來(lái)理解動(dòng)態(tài)語(yǔ)言,完全是搞錯(cuò)了方向。看看下面一段javascript代碼:
1. var a = 5;
2. alert(a);
3. var a = "foobar";
4. alert(a);
這是一段完全合法可以正確運(yùn)行的javascript程序,然而對(duì)于只編過(guò)靜態(tài)語(yǔ)言而且對(duì)靜態(tài)語(yǔ)言的語(yǔ)義很了解的人,卻很難理解。變量a,明顯不是指向根據(jù)類型分配出來(lái)一塊大小固定的內(nèi)存塊。
如何理解這一段代碼的語(yǔ)義?
Revised Report on the Algorithmic Language Scheme 一文里有這么一段:
引用
Scheme has latent as opposed to manifest types. Types are associated with values also (also call objects) rather than with variables. (Some authors refer to languages with latent types as weakly typed or dynamically typed languages. Other languages with latent types are APL, Snobol, and other dialects of Lisp. Languages with manifest types (sometimes referred to as strongly typed or statically typed language) include Algol 60, Pascal, and C.
Paul Graham在其“What Made Lisp Different”一文中這么說(shuō):
引用
A new concept of variables. In Lisp, all variables are effectively pointers. Values are what have types, not variables, and assigning or binding variables means copying pointers, not what they point to.
這兩段合在一起,可以正確理解動(dòng)態(tài)語(yǔ)言的語(yǔ)義。
靜態(tài),變量實(shí)際是分配的內(nèi)存塊,大小固定。

動(dòng)態(tài),變量實(shí)際是個(gè)指針,可指向內(nèi)存任何一塊。
(當(dāng)然是運(yùn)行的不同時(shí)期指向不同的內(nèi)存塊)
看看下面幾句:
1. JavaScript: var a = 5;
2. ML: val a = 5;
3. Scheme: (define a 5)
這些語(yǔ)句應(yīng)該理解為, (等號(hào)右邊)表達(dá)式evaluate出來(lái)一個(gè)值,這個(gè)值綁定到變量a里面去。用來(lái)描述上述代碼語(yǔ)義的正確的詞是binding。
看看下面ML語(yǔ)言解釋器對(duì)ML代碼的解釋:
1. Moscow ML version 2.01 (January 2004)
2. Enter `quit();' to quit.
3. - a;
4. ! Toplevel input:
5. ! a;
6. ! ^
7. ! Unbound value identifier: a
8. - val a = 5;
9. > val a = 5 : int
10. - a;
11. > val it = 5 : int
12. - val a = "foobar";
13. > val a = "foobar" : string
14. - a;
15. > val it = "foobar" : string
注意第七行的提示。
第十行,第十四行光打入a,也是個(gè)表達(dá)式,evaluate出來(lái)的值,綁定給省缺變量it。
看看下面Scheme語(yǔ)言解釋器對(duì)Scheme代碼的解釋:
1. > a
2. ; Unbound variable: a
3.
4. > (define a 5)
5. ; Value: a
6.
7. > a
8. ; Value: 5
注意第二行的提示。
一定要分清動(dòng)態(tài)語(yǔ)言的變量綁定和靜態(tài)語(yǔ)言的變量賦值的區(qū)別。變量是一個(gè)數(shù)學(xué)上的概念,在靜態(tài)語(yǔ)言中,叫變量其實(shí)不合適,還不如直接叫a memory box,更能清楚地說(shuō)明其本質(zhì)。
對(duì)于靜態(tài)語(yǔ)言,弱類型是致命傷,因?yàn)樵诼暶髯兞康臅r(shí)候,內(nèi)存塊已經(jīng)分配好了,往這個(gè)內(nèi)存塊里寫(xiě)一塊內(nèi)存塊存儲(chǔ)不下的數(shù)據(jù),帶來(lái)的傷害是致命的。對(duì)于動(dòng)態(tài)語(yǔ)言,強(qiáng)弱類型未必重要。
在C/C++/Java/C#里面,內(nèi)存是可以分配到Stack里面,也可以分配到Heap里面, 程序員一定要搞清楚區(qū)別, 像在C里:
1. int a = 5;
2. int b[] = { 1, 2, 3, 4}
3. int* ptr = (int*)malloc(10*sizeof(int));
a 和 b 所分配的內(nèi)存都在stack里,c 指向heap里的一塊,退出前不把c 給free掉,就會(huì)遺漏內(nèi)存。給function傳值的時(shí)候,更要小心,傳a是把5這個(gè)值給傳過(guò)去,傳b是傳b這個(gè)array第一個(gè)元素的地址。
到了C++,更加繁瑣,因?yàn)镃++的 Object是可以分配在stack上的,隨便寫(xiě)幾句代碼,都會(huì)用到assignment operator = , address-of operator &, copy constructor.
1. const ClassFoo e1; // default constructor, destructor later
2. ClassFoo e2(e1); // copy constructor
3. e2 = e1; // assignment operator
4. ClassFoo *pe2 = &e2; // address-of operator (non-const)
5. const ClassFoo *pe1 = &e1; // address-of operator (const)
C++編譯器自動(dòng)生成這些函數(shù),有時(shí)不符合需要就要自己手寫(xiě)。
Java里面所有的object allocation, 都是分配在Heap里的,光這一點(diǎn),就大大減輕了編程的繁瑣度。從Java轉(zhuǎn)向C++的朋友,一定要記住這一點(diǎn)。C++的 Object是可以分配在stack上的。
Java里面的primitive變量是分配在Stack上的,其實(shí)如果廢除這八個(gè)primitive types,全部用Object reference,動(dòng)靜態(tài)語(yǔ)言的差別已經(jīng)不那么大了。Type inference在C# 3里面,已經(jīng)開(kāi)始實(shí)現(xiàn)了:
1. var str = "Hello world!";
2. var num = 42;
3. var v = new TypeWithLongName<AndWithTypeParameter>();
歐美計(jì)算機(jī)專業(yè)的第一門(mén)語(yǔ)言,一般是ML或Scheme。這些語(yǔ)言,做到了程序員不用思考內(nèi)存是分配在stack上還是heap上,內(nèi)存回收由GC管,因而可以集中精力,學(xué)習(xí)算法,遞歸等等。
用編程來(lái)解決問(wèn)題,需要三方面的技能:1. 對(duì)編程語(yǔ)言,語(yǔ)義及運(yùn)行環(huán)境的掌握,2. 對(duì)解決問(wèn)題的算法的掌握,3. 擁有寫(xiě)出結(jié)構(gòu)清晰,簡(jiǎn)潔易懂的代碼的能力。
第一點(diǎn)和第二點(diǎn)經(jīng)常交匯在一起,因?yàn)檎Z(yǔ)言,經(jīng)常是為了解決某個(gè)領(lǐng)域的問(wèn)題而設(shè)計(jì)的,解決算法,遞歸之類的問(wèn)題,用functional programming language,操作系統(tǒng),應(yīng)該用C,web領(lǐng)域之PHP,科學(xué)計(jì)算之Matlab,試驗(yàn)儀器控制之labview,關(guān)系數(shù)據(jù)庫(kù)之SQL,莫不如此。
那么什么算是通用語(yǔ)言,什么算是DSL?通用不通用是相對(duì)的。C是一門(mén)通用語(yǔ)言,但也可以說(shuō)是操作系統(tǒng)的DSL。從某種角度來(lái)說(shuō),能夠全面控制計(jì)算機(jī)的,才叫通用語(yǔ)言,那么只有匯編才符合這個(gè)條件,C和C++勉強(qiáng)算得上。
新手上路,該學(xué)什么?應(yīng)該從某個(gè)領(lǐng)域?qū)W起,學(xué)習(xí)解決那個(gè)領(lǐng)域問(wèn)題需要的方法,而且學(xué)習(xí)那個(gè)領(lǐng)域的DSL。這樣成效出的最快,而且不受干擾。
現(xiàn)在學(xué)校里教學(xué)靜態(tài)語(yǔ)言占主流,有歷史原因。以前計(jì)算機(jī)不夠快,用C編程是唯一的選擇。現(xiàn)在對(duì)運(yùn)行效率要求很高的領(lǐng)域,還得用C,C++。但是在很多領(lǐng)域,這已經(jīng)不是個(gè)問(wèn)題了。由于歷史的慣性,靜態(tài)語(yǔ)言還在繼續(xù)教。學(xué)校老師學(xué)新知識(shí)的動(dòng)力,可不大。這些老師教出的學(xué)生,只會(huì)靜態(tài)語(yǔ)言,那么公司為了保證人手充足,也會(huì)傾向靜態(tài)語(yǔ)言。這種狀況,慢慢會(huì)打破。