書名:《代碼的未來》
作者:松本行弘(Yukihiro Matsumoto)
譯者:**周自恒 **
本試讀章節摘自:『第3章 編程語言的新潮流』
接下來,我們從語言設計的角度,來比較一下 Java、JavaScript、Ruby 和 Go 這 4 種語言。這幾種語言看起來彼此完全不同,但如果選擇一個合適的標準,就可以將它們非常清楚地進行分類,如圖所示。

JavaScript 是客戶端語言的代表,Java 其實也在其黎明期作為客戶端語言活躍過一段時間,應該有很多人還記得 Java Applet 這個名詞。之后,Java 轉型為服務器端語言的代表,地位也扶搖直上,但考慮到它的出身,這里還是將其分類為客戶端語言。
另一個分類標準,就是靜態和動態。所謂靜態,就是不實際運行程序,僅通過程序代碼的字面來確定結果的意思;而所謂動態,就是只有當運行時才確定結果的意思。靜態、動態具體所指的內容有很多種,大體上來分的話就是運行模式和類型。這 4 種語言全都具備面向對象的性質,而面向對象本身就是一種包含動態概念的性質。不過,在這幾種語言之中,Java 和 Go 是比較偏重靜態一側的語言,而 Ruby 和 JavaScript 則是比較偏重動態一側的語言。
客戶端與服務器端
首先,我們先將這些語言按照客戶端和服務器端來進行分類。如前面所說,這種分類是以該語言剛剛出現時所使用的方式為基準的。
現在 Java 更多地被用作服務器端語言,而我們卻將它分類到客戶端語言中,很多人可能感到有點莫名其妙。Java 確實現在已經很少被用作客戶端語言了,但是我們不能忘記,誕生于 1995 年的 Java,正是伴隨嵌入在瀏覽器中的 Applet 技術而出現的。
Java 將虛擬機(VM)作為插件集成到瀏覽器中,將編譯后的 Java 程序(Applet)在虛擬機上運行,這種技術當初是為了增強瀏覽器的功能。再往前追溯的話,Java 原本名叫 Oak,是作為面向嵌入式設備的編程語言而誕生的。因此,從出身來看的話,Java 還是一種面向客戶端的編程語言。
Java 所具備的 VM 和平臺無關性字節碼等特性,本來就是以在客戶端運行 Applet 為目的的。在各種不同的環境下都能夠產生相同的行為,這樣的特性對于服務器端來說雖然也不能說是毫無價值,但是服務器環境是可以由服務提供者來自由支配的,因此至少可以說,這樣的特性無法帶來關鍵性的好處吧。另一方面,在客戶端環境中,操作系統和瀏覽器都是千差萬別,因此對平臺無關性的要求一直很高。
Java 誕生于互聯網的黎明時期,那個時候瀏覽器還不是電腦上必備的軟件。當時主流的瀏覽器有 Mosaic 和 Netscape Navigator 等,除此之外還有一些其他類似的軟件,而 Internet Explorer 也是剛剛才嶄露頭角。
在那個充滿夢想的時代,如果能開發出一種功能上有亮點的瀏覽器就有可能稱霸業界。原 Sun Microsystems 公司曾推出了一個用 Java 編寫的瀏覽器 HotJava,向世界展示了 Applet 的可能性。然而,隨著瀏覽器市場格局的逐步固定,他們轉變了策略,改為向主流瀏覽器提供插件來集成 Java,從而對 Applet 的運行提供支持。
向服務器端華麗轉身
然而,Java 自誕生之后,并未在客戶端方面取得多大的成功,于是便開始著手進入服務器端領域。造成這種局面有很多原因,我認為其中最主要的原因應該是在 Applet 這個平臺上遲遲沒有出現一款殺手級應用(killer app)。
處于剛剛誕生之際的 Java 遭到了很多批判,如體積臃腫、運行緩慢等,不同瀏覽器上的 Java 插件之間也存在一些兼容性方面的問題,使得 Applet 應用并沒有真正流行起來。在這個過程中,JavaScript 作為客戶端編程語言則更加實用,并獲得了越來越多的關注。當然,在那個時候 Java 已經完全確立了自己作為服務器端編程語言的地位,因此喪失客戶端這塊領地也不至于感到特別肉痛。
Java 從客戶端向服務器端的轉身可以說是相當成功的。與此同時,Sun Microsystems 和 IBM 等公司著手對 JVM(Java VM)進行改良,使得其性能得到了改善,在某些情況下性能甚至超越了 C++。想想之前對 Java 性能惡評如潮的情形,現在 Java 能有這樣的性能和人氣簡直就像做夢一樣。
在服務器端獲得成功的四大理由
由于我本人沒有大規模實踐過 Java 編程,因此對于 Java 在服務器端取得成功的來龍去脈,說真的并不是很了解。不過,如果讓我想象一下的話,大概有下面幾個主要的因素。
1. 可移植性
雖然服務器環境比客戶端環境更加可控,但服務器環境中所使用的系統平臺種類也相當多,如 Linux、Solaris、FreeBSD、Windows 等,根據需要,可能還會在系統上線之后更換系統平臺。在這樣的情況下,Java 所具備的 “一次編寫,到處運行” 特性就顯得魅力十足了。
2. 功能強大
Java 在服務器端嶄露頭角是在 20 世紀 90 年代末,那個時候的狀況對 Java 比較有利。和 Java 在定位上比較相似的語言,即靜態類型、編譯型、面向對象的編程語言,屬于主流的也就只有 C++ 而已了。
在 Java 誕生的 20 世紀 90 年代中期,正好是我作為 C++ 程序員開發 CAD 相關系統的時候。但當時 C++ 也還處于發展過程中,在實際的開發中,模板、異常等功能還無法真正得到運用。
相比之下,Java 從一開始就具備了垃圾回收(GC)機制,并在語言中內置了異常處理,其標準庫也是完全運用了異常處理來設計的,這對程序員來說簡直是天堂。毫無疑問,Java 語言 的這些優秀特性,是幫助其確立服務器端編程語言地位的功臣之一。
3. 高性能
Java 為了實現其 “一次編寫,到處運行” 的宣傳口號,并不是將程序直接轉換為系統平臺所對應的機器語言,而是轉換為虛擬 CPU 的機器語言 “字節碼” (Bytecode),并通過搭載虛擬 CPU 的模擬器 JVM 來運行。JVM 歸根到底其實是在運行時用來解釋字節碼的解釋器,理論上說運行速度應該無法與直接生成機器語言的原生編譯器相媲美。
事實上,在 Java 誕生初期,確實沒有達到編譯型語言應有的運行速度,當時的用戶經常抱怨 Java 太慢了,這樣的惡評令人印象深刻。
然而,技術的革新是偉大的。隨著各種技術的進步,現在 Java 的性能已經能夠堪稱頂級。
例如,有一種叫做 JIT(Just In Time)編譯的技術,可以在運行時將字節碼轉換成機器語言,經過轉換之后就可以獲得和原生編譯一樣快的運行速度。在運行時進行編譯,就意味著編譯時間也會包含在運行時間里面。因此,優秀的 JIT 編譯器會通過偵測運行信息,僅將需要頻繁運行的瓶頸部分進行編譯,從而大大削減編譯所需的時間。而且,利用運行時編譯,可以不用考慮連接的問題而積極運用內聯擴展,因此在某些情況下,運行速度甚至可以超過 C++。
在 Java 中,其性能提高的另一個障礙就是 GC。GC 需要對對象進行掃描,將不用的對象進行回收,這個過程和程序本身要進行的操作是無關的,換句話說,就是做無用功,因此而消耗的時間拖累了 Java 程序的性能。作為對策,在最新的 JVM 中,采用了并行回收、分代回收等技術。
4. 豐富的庫
隨著 Java 的人氣直升,應用逐漸廣泛,Java 能夠使用的庫也越來越多。庫的增加提高了開發效率,從而又反過來拉高了 Java 的人氣,形成了一個良性循環。現在 Java 的人氣已經無可撼動了。
客戶端的 JavaScript
Applet 在客戶端對擴展瀏覽器功能做出了嘗試,然而它并不太成功。在瀏覽器畫面中的一個矩形區域中運行應用程序的 Applet,并沒有作為應用程序的發布手段而流行起來。
幾乎是在同一時期出現的 JavaScript,也是一種集成在瀏覽器中的語言,但是它可以在一般的網頁中嵌入程序邏輯,這一點是和 Java Applet 完全不同的方式,卻最終獲得了成功。
JavaScript 是由原 Netscape Communications 公司開發的,通過 JavaScript,用戶點擊網頁上的鏈接和按鈕時,不光可以進行頁面的跳轉,還可以改寫頁面的內容。這樣的功能十分便利,因此 Netscape Navigator 之外的很多瀏覽器都集成了 JavaScript。
隨著瀏覽器的不斷競爭和淘汰,當主流瀏覽器全部支持 JavaScript 時,情況便發生了變化。像 Google 地圖這樣的產品,整體的框架是由 HTML 組成的,但實際顯示的部分卻是通過 JavaScript 來從服務器獲取數據并顯示出來,這樣的手法從此開始流行起來。
在 JavaScript 中與服務器進行異步通信的 API 叫做 XMLHttpRequest,因此從它所衍生出的手法便被稱為 Ajax(Asynchronous JavaScript and XML,異步 JavaScript 與 XML)。在美國有一種叫做 Ajax 的廚房清潔劑,說不定是從那個名字模仿而來的。
性能顯著提升
目前,客戶端編程語言中 JavaScript 已成為一個強有力的競爭者,伴隨著 JavaScript 重要性的不斷提高,對 JavaScript 引擎的投資也不斷增加,使 JavaScript 的性能得到了顯著改善。改善 JavaScript 性能的主要技術,除了和 Java 相同的 JIT 和 GC 之外,還有特殊化(Specialization)技術。
與 Java 不同,JavaScript 是一種動態語言,不帶有變量和表達式的類型信息,針對類型進行優化是非常困難的,因此性能和靜態語言相比有著先天的劣勢,而特殊化就是提高動態語言性能的技術之一。
JavaScript 函數:
function fact(n) {
if (n == 1) return 1;
return n * fact(n-1);
}
我們設想如上所示的這樣一個 JavaScript 函數。這個函數是用于階乘計算的,大多數情況下,其參數 n 應該都是整數。由于 JIT 需要統計運行時信息,因此 JavaScript 解釋器也知道參數 n 大多數情況下是整數。
于是,當解釋器對 fact 函數進行 JIT 編譯時,會生成兩個版本的函數:一個是 n 為任意對象的通用版本,另一個是假設 n 為整數的高速版本。當參數 n 為整數時(即大多數情況下),就會運行那個高速版本的函數,便實現了與靜態語言幾乎相同的運行性能。
除此之外,最新的 JavaScript 引擎中還進行了其他大量的優化,說 JavaScript 是目前最快的動態語言應該并不為過。
JavaScript 在客戶端稱霸之后,又開始準備向服務器端進軍了。JavaScript 的存在感在將來應該會越來越強吧。
服務器端的 Ruby
客戶端編程的最大問題,就是必須要求每一臺客戶端都安裝相應的軟件環境。在 Java 和 JavaScript 誕生的 20 世紀 90 年代后半,互聯網用戶還只局限于一部分先進的用戶,然而現在互聯網已經大大普及,用戶的水平構成也跟著變得復雜起來,讓每一臺客戶端都安裝相應的軟件環境,就會大大提高軟件部署的門檻。
而相對的,在服務器端就沒有這樣的制約,可以選擇最適合自己的編程語言。
在 Ruby 誕生的 1993 年,互聯網還沒有現在這樣普及,因此 Ruby 也不是一開始就面向 Web 服務器端來設計的。然而,從 WWW 黎明期開始,為了實現動態頁面而出現了通用網關接口(Common Gateway Interface,CGI)技術,而 Ruby 則逐漸在這種技術中得到了應用。
所謂 CGI,是通過 Web 服務器的標準輸入輸出與程序進行交互,從而生成動態 HTML 頁面 的接口。只要可以對標準輸入輸出進行操作,那么無論任何語言都可以編寫 CGI 程序,這不得不歸功于 WWW 設計的靈活性,使得動態頁面可以很容易地編寫出來,也正是因為如此,使得 WWW 逐漸風靡全世界。
在 WWW 中,來自 Web 服務器的請求信息是以文本的方式傳遞的,反過來,返回給 Web 服務器的響應信息也是以文本(HTML)方式傳遞的,因此擅長文本處理的編程語言就具有得天獨厚的優勢。于是,腳本語言的時代到來了。以往只是用于文本處理的腳本語言,其應用范圍便一下子擴大了。
早期應用 CGI 的 Web 頁面大多是用 Perl 來編寫的,而作為 “Better Perl” 的 Ruby 也隨之逐步得到越來越多的應用。
Ruby on Rails 帶來的飛躍
2004 年,隨著 Ruby on Rails 的出現,使得 Web 應用程序的開發效率大幅提升,也引發了廣泛的關注。當時,已經出現了很多 Web 應用程序框架,而 Ruby on Rails 可以說是后發制人的。 Ruby on Rails 的特性包括:
- ?完全的 MVC 架構?
- 不使用配置文件(尤其是 XML)?
- 堅持簡潔的表達
- 積極運用元編程
- 對 Ruby 核心的大膽擴展
基于這些特性,Ruby on Rails 實現了很高的開發效率和靈活性,得到了廣泛的應用。可以說,Ruby 能擁有現在的人氣,基本上都是 Ruby on Rails 所作出的貢獻。
目前,作為服務器端編程語言,Ruby 的人氣可謂無可撼動。有一種說法稱,以硅谷為中心的 Web 系創業公司中,超過一半都采用了 Ruby。
但這也并不是說,只要是服務器端環境,Ruby 就一定可以所向披靡。在規模較大的企業中,向網站運營部門管理的服務器群安裝軟件也并不容易。實際上,在某個大企業中,曾經用 Ruby on Rails 開發了一個面向技術人員的 SNS,只用很短的時間就完成搭建了,但是等到要正式上線的時候,運營部門就會以 “這種不知道哪個的家伙開發的,也沒經過第三方安全認證的 Ruby 解釋器之類的軟件,不可以安裝在我們數據中心的主機上面” 這樣的理由來拒絕安裝,這真是相當頭疼。
不過,開發部門的工程師們并沒有氣餒,而是用 Java 編寫的 Ruby 解釋器 JRuby,將開發好的 SNS 轉換為 jar 文件,從而使其可以在原 Sun Microsystems 公司的應用程序服務器 GlassFish 上運行。當然,JVM 和 GlassFish 都已經在服務器上安裝好了,這樣一來運營方面也就沒有理由拒絕了。多虧了 JRuby,結局皆大歡喜。
JRuby 還真是在關鍵時刻大顯身手呢。
服務器端的 Go
Go 是一種新興的編程語言,但它出身名門,是由著名 UNIX 開發者羅勃 · 派克和肯 · 湯普遜開發的,因此受到了廣泛的關注。
Go 的誕生背景源于 Google 公司中關于編程語言的一些問題。在 Google 公司中,作為優化編程環境的一環,在公司產品開發中所使用的編程語言,僅限于 C/C++、Java、Python 和JavaScript。實際上也有人私底下在用 Ruby,不過正式產品中所使用的語言僅限上述 4 種。
這 4 種語言在使用上遵循著一定的分工:客戶端語言用 JavaScript,服務器端語言用腳本系的 Python,追求大規模或高性能時用 Java,文件系統等面向平臺的系統編程用 C/C++。在這些語言中,Google 公司最不滿意的就是 C/C++ 了。
和其他一些編程語言相比,C/C++ 的歷史比較久,因此不具備像垃圾回收等最近的語言所提供的編程輔助功能。因此,由于開發效率一直無法得到提高,便產生了設計一種 “更好的” 系統編程語言的需求。而能夠勝任這一位置的,正是全新設計的編程語言 Go。
Go 具有很多特性,(從我的觀點來看)比較重要的有下列幾點:
- ?垃圾回收
- 支持并行處理的 Goroutine
- Structural Subtyping(結構子類型)
關于最后一點 Structural Subtyping,我們會在后面對類型系統的講解中進行說明。
靜態與動態
剛才我們已經將這 4 種語言,從客戶端、服務器端的角度進行了分類。接下來我們再從動態、靜態的角度來看一看這幾種語言。
正如剛才所講過的,所謂靜態,就是無需實際運行,僅根據程序代碼就能確定結果的意思;而所謂動態,則是只有到了運行時才能確定結果的意思。
不過,無論任何程序,或多或少都包含了動態的特性。如果一個程序完全是靜態的話,那就意味著只需要對代碼進行字面上的分析,就可以得到所有的結果,這樣一來程序的運行就沒有任何意義了。例如,編程計算 6 的階乘,如果按照完全靜態的方式來編寫的話,應該是下面這樣的:
puts "720"
不過,除非是個玩具一樣的演示程序,否則不會開發出這樣的程序來。在實際中,由于有了輸入的數據,或者和用戶之間的交互,程序才能在每次運行時都能得到不同的要素。
因此,作為程序的實現者,編程語言也多多少少都具備動態的性質。所謂動態還是靜態,指的是這種語言對于動態的功能進行了多少限制,或者反過來說,對動態功能進行了多少積極的強化,我們所探討的其實是語言的這種設計方針。
例如,在這里所列舉的 4 種編程語言都是面向對象的語言,而面向對象的語言都會具備被稱為多態(Polymorphism)或者動態綁定的動態性質。即,根據存放在變量中的對象的實際性質,自動選擇一種合適的處理方式(方法)。這樣的功能可以說是面向對象編程的本質。
屬于動態的編程語言,其動態的部分,主要是指運行模式和類型。這兩者是相互獨立的概念,但采用動態類型的語言,其運行模式也具有動態的傾向;反之也是一樣,在靜態語言中,運行模式在運行時的靈活性也會受到一定的限制。
動態運行模式
所謂動態運行模式,簡單來說,就是運行中的程序能夠識別自身,并對自身進行操作。對程序自身進行操作的編程,也被稱為元編程(Metaprogramming)。
在 Ruby 和 JavaScript 中,元編程是十分自然的,比如查詢某個對象擁有哪些方法,或者在運行時對類和方法進行定義等等,這些都是理所當然的事。
另一方面,在 Java 中,類似元編程的手法,是通過 “反射 API” 來實現的。雖然對類進行取出、操作等功能都是可以做到的,但并非像 Ruby 和 JavaScript 那樣讓人感到自由自在,而是 “雖然能做到,但一般也不會去用” 這樣的感覺吧。
Go 也是一樣。在 Go 中,通過利用 reflect 包可以獲取程序的運行時信息(主要是類型),但是(在我所理解的范圍內)無法實現進一步的元編程功能。而之所以沒有采用比 Java 更進一步的動態運行模式,恐怕是因為這(可能)在系統編程領域中必要性不大,或者是擔心對運行速度產生負面影響之類的原因吧。
何謂類型
從一般性的層面來看,類型2指的是對某個數據所具有的性質所進行的描述。例如,它的結構是怎樣的,它可以進行哪些操作,等等。動態類型的立場是數據擁有類型,且只有數據才擁有類型;而靜態類型的立場是數據擁有類型,而存放數據的變量、表達式也擁有類型,且類型是在編譯時就固定的。
然而,即便是靜態類型,由于面向對象語言中的多態特性,也必須具備動態的性質,因此需要再追加一條規則,即實際的數據(類型),是靜態指定的類型的子類型。所謂子類型(Subtype),是指具有繼承關系,或者擁有同一接口,即靜態類型與數據的類型在系統上 “擁有同一性質” 。
靜態類型的優點
動態類型比較簡潔,且靈活性高,但靜態類型也有它的優點。由于在編譯時就已經確定了類型,因此比較容易發現 bug。當然,程序中的 bug 大多數都是與邏輯有關的,而單純是類型錯誤而導致的 bug 只是少數派。不過,邏輯上的錯誤通常也伴隨著編譯時可以檢測到的類型不匹配,也就是說,通過類型錯誤可以讓其他的 bug 顯露出來。
除此之外,程序中對類型的描述,可以幫助對程序的閱讀和理解,或者可以成為關于程序行為的參考文檔,這可以說是一個很大的優點。
此外,通過靜態類型,可以在編譯時獲得更多可以利用的調優信息,編譯器便可以生成更優質的代碼,從而提高程序的性能。然而,通過 JIT 等技術,動態語言也可以獲得與原生編譯 的語言相近的性能,這也說明,在今后靜態語言和動態語言之間的性能差距會繼續縮小。
動態類型的優點
相對而言,動態類型的優點,就在于其簡潔性和靈活性了。
說得極端一點的話,類型信息其實和程序運行的本質是無關的。就拿階乘計算的程序來說,無論是用顯式聲明類型的 Java 來編寫,還是用非顯式聲明類型的 Ruby 來編寫, 其算法都是毫無區別的。然而,由于多了關于類型的描述,因此在 Java 版中,與算法本質無關的代碼的分量也就增加了。
Java 編寫的階乘程序 :
class Sample {
private static int fact(int n) {
if (n == 1) return 1;
return n * fact(n - 1);
}
public static void main(String[] argv) {
System.out.println("6!="+fact(6));
}
}
Ruby 編寫的階乘程序:
def fact(n)
if n == 1
1
else
n * fact(n - 1)
end
end
print "6!=", fact(6), "\n"
---
而且,類型也帶來了更多的制約。如上所示的程序對 6 的階乘進行了計算,但如果這個數字繼續增大,Java 版對超過 13 的數求階乘的話,就無法正確運行了。Java 的程序中,fact
方法所接受的參數類型顯式聲明為 int
型,而 Java 的 int 為 32 位,即可以表示到接近 20 億的整數。如果階乘的計算結果超出這個范圍,就會導致溢出。
當然,由于 Java 擁有豐富的庫資源,用 BigInteger
類就可以實現無上限的大整數計算,但這就需要對上面的程序做較大幅度的改動。而由于計算機存在 “int 的幅度為 32 位” 這一限制,就使得階乘計算的靈活性大大降低了。
另一方面,Ruby 版中則沒有這樣的制約,就算是計算 13 的階乘,甚至是 200 的階乘,都可以直接計算出來,而無需擔心如 int 的大小、計算機的限制等問題。
其實這里還是有點小把戲的。同樣是動態語言,用 JavaScript 來計算 200 的階乘就會輸出 Infinity(無窮大)。其實,JavaScript 的數值是浮點數,因此無法像 Ruby 那樣支持大整數的計算。也就是說,要不受制約地進行計算,除了類型的性質之外,庫的支持也是非常重要的。
有鴨子樣的就是鴨子
在動態語言中,一種叫做鴨子類型(Duck Typing)的風格被廣泛應用。鴨子類型這個稱謂,據說是從下面這則英語童謠來的:
- If it walks like a duck and quacks like a duck, it must be a duck.
如果像鴨子一樣走路,像鴨子一樣呱呱叫,則它一定是一只鴨子
從這則童謠中,我們可以推導出一個規則,即如果某個對象的行為和鴨子一模一樣,那無論它真正的實體是什么,我們都可以將它看做是一只鴨子。也就是說,不考慮某個對象到底是哪一個類的實例,而只關心其擁有怎樣的行為(擁有哪些方法),這就是鴨子類型。因此,在程序中,必須排除由對象的類所產生的分支。
這是由 “編程達人” 大衛 · 托馬斯(Dave Thomas)所提出的。
例如,假設存在 log_puts(out, mesg)
這樣一個方法,用來將 mesg
這個字符串輸出至 out
這個輸出目標中。out
需要指定一個類似 Ruby 中的 IO
對象,或者是 Java 中的 ReadStream
這樣的對象。在這里,本來是向文件輸出的日志,忽然想輸出到內存的話,要怎么辦呢?比如說我想將日志的輸出結果合并成一個字符串,然后再將它取出。
在 Java 等靜態語言中,out
所指定的對象必須擁有共同的超類或者接口,而無法選擇一個完全無關的對象作為輸出目標。要實現這樣的操作,要么一開始就事先準備這樣一個接口,要么重寫原來的類,要么準備一個可以切換輸出目標的包裝對象(wrapper object)。無論如何,如果沒有事先預計到需要輸出到內存的話,就需要對程序進行大幅的改動了。
如果是采用了鴨子類型風格的動態語言,就不容易產生這樣的問題。只要準備一個和 IO 對象具有同樣行為的對象,并將其指定為 out
的話,即便不對程序進行改動,log_puts
方法能夠成 功執行的可能性也相當大。實際上,在 Ruby 中,確實存在和 IO
類毫無繼承關系,但和 IO 具有同樣行為的 StringIO
類,用來將輸出結果合并成字符串。
動態類型在編譯時所執行的檢查較少,這是一個缺點,但與此同時,程序會變得更加簡潔,對于將來的擴展也具有更大的靈活性,這便是它的優點。
Structural Subtyping
在 4 種語言中最年輕的 Go,雖然是一種靜態語言,但卻吸取了鴨子類型的優點。Go 中沒有所謂的繼承關系,而某個類型可以具有和其他類型之間的可代換性,也就是說,某個類型的變量中是否可以賦予另一種類型的數據,是由兩個類型是否擁有共同的方法所決定的。例如,對于 “A 型” 的變量,只要數據擁有 A 型所提供的所有方法,那么這個數據就可以賦值給該變量。像這樣,以類型的結構來確定可代換性的類型關系,被稱為結構子類型(Structural Subtyping);另一方面,像 Java 這樣根據聲明擁有繼承關系的類型具有可代換性的類型關系,被稱為名義子類型(Nominal Subtyping)。
在結構子類型中,類型的聲明是必要的,但由于并不需要根據事先的聲明來確定類型之間的關系,因此就可以實現鴨子類型風格的編程。和完全動態類型的語言相比,雖然增加了對類型的描述,但卻可以同時獲得鴨子類型帶來的靈活性,以及靜態編譯所帶來了類型檢查這兩個優點,可以說是一個相當劃算的交換。
小結
在這里,我們對 Ruby、JavaScript、Java、Go 這 4 種語言,從服務器端、客戶端,以及靜態、動態這兩個角度來進行了對比。這 4 種語言由于其不同的設計方針,而產生出了不同的設計風格,大家是否對此有了些許了解呢?
不僅僅是語言,其實很多設計都是權衡的結果。新需求、新環境,以及新范式,都催生出新的設計。而學習現有語言的設計及其權衡的過程,也可以為未來的新語言打下基礎。