在對任何編程語言的討論中,爭議較大的一個問題就是類型模型。類型決定可以使用哪些種類的工具,并影響到應用程序的設計。很多開發人員將類型與生產率或可維護性聯系起來(我就是其中的一個)。典型的 Java 開發人員通常都特別樂于維護 Java 語言的類型模型的地位,強調 Java 語言可采用更好的開發工具,在編譯時捕捉某些種類的 bug(例如類型不兼容和拼寫錯誤),以及性能等方面的優勢。
如果您想理解一種新的編程語言,甚至一系列語言,那么通常應該從類型策略著手。在本文中,您將看到 Java 之外的一些語言中的類型模型。我首先簡要介紹任何語言設計者在類型模型中必須考慮的一些決策,著重介紹靜態類型和動態類型的一些不同的決策。我將展示一些不同極端的例子 —— Objective Caml 中的靜態類型和 Ruby 中的動態類型。我還將談到 Java 語言的類型限制,以及如何突破 Java 類型的限制快速編程。
![]() |
|
類型策略
至少可以從三個角度來看待類型:
下面兩個例子很好地闡釋了其中兩個角度的內涵。假設您編譯下面這段 Java 代碼:
|
會收到如下錯誤消息:
|
執行以下 Ruby 代碼:
|
會收到以下錯誤消息:
|
這兩種語言都傾向于強類型,因為當您試圖使用一個它們期望之外的類型結構的對象時,它們都會拋出錯誤消息。Java 類型策略在編譯時給出錯誤消息,因為它執行靜態類型檢查。而 Ruby 則是在運行時給出錯誤消息,因為 Ruby 支持動態類型。換句話說,Java 在編譯時將對象綁定到類型。而 Ruby 則是在運行時每當更改對象的時候將對象綁定到類型。由于我是在 Java 代碼中,而不是在 Ruby 中聲明變量的,因此可以看到 Java 語言的顯式類型與 Ruby 的隱式類型的工作方式不同。
在這三個角度中,靜態類型與動態類型對于語言的特征有最大的影響,因此接下來我將重點解釋這兩種策略各自的優點。
靜態類型的優點
在靜態類型語言中,程序員(通過聲明或根據約定)或編譯器(根據結構和語法線索)將一種類型指定給一個變量,然后那個類型就不會改變。靜態類型通常需要額外的成本,因為靜態類型語言(例如 Java 語言)通常是顯式類型的。這意味著必須聲明所有的變量,然后編譯代碼。成本也伴隨著收益:早期的錯誤檢測。靜態類型在最基層為編譯器提供多得多的信息。更多信息所帶來的好處就是,可以更早地捕捉到某些類型的錯誤,而動態類型語言只有到運行時才能檢測到這些錯誤。如果您一直等到運行時才捕捉這些 bug,那么其中一些將進入生產環境。也許這正是動態類型語言受到最多指責的一個方面。
另一種觀點則認為現代軟件開發團隊通常會運行自動測試,動態語言的支持者聲稱,即使是最簡單的自動測試也可以捕捉到大多數的類型錯誤。而動態語言的支持者所能提供的對編譯時錯誤檢測不利的最好論據是,早期檢測所帶來的好處相對于成本來說是得不償失的,因為不管是否使用動態類型,最終都要進行測試。
一種有趣的折中方法是在靜態類型語言中使用隱式類型,從而減少類型的成本。開放源代碼語言 Objective Caml (OCaml) 是靜態類型語言 Lisp 的衍生物,它既能提供很好的性能,又不會犧牲生產率。OCaml 使用隱式類型,因此可以編寫下面這樣的采用靜態類型的代碼:
|
OCaml 返回:
|
根據表達式中的語法線索,OCaml 推斷出 x
的類型。4
是 int
型,7
也是 int
型,因此 x
也必定是 int
型。隱式類型語言可以擁有 Java 語言所具有的所有類型安全性,甚至更多。不同之處在于您需要提供的信息量,以及在閱讀程序時可用的信息量。很多喜歡靜態類型的人更偏愛隱式類型。他們寧愿讓編譯器來做這種事情,而不愿意被迫重復地在代碼中輸入變量的類型。
隱式類型系統一個較大的優點是,不需要為函數的參數聲明類型,因為編譯器會從傳入的值推斷出參數的類型。因此同一個方法可以有多種用途。
![]() |
|
并不是只有編譯器才能利用靜態類型所提供的附加信息。IDE 可以通過靜態類型為重構提供更好的支持。幾年前,一種革命性的思想改變了開發環境的工作方式。在 IDEA 和 Eclipse 中,您的代碼看上去像一個文本視圖,但是開發環境實際上正在編輯 Abstract Syntax Tree (AST)。因此,當需要重新命名一個方法或者類的時候,開發環境很容易通過在 AST 中精確定位找到方法或類被引用的每個地方。如今,如果沒有通過靜態類型簡化的優秀的重構,我們很難想像用 Java 語言編程。在我探索 Ruby 的時候,相對于其他任何工具或特性,我更懷念 IDEA。
靜態類型還有其他一些優點,在這里我不會詳細描述。靜態類型可以提供更好的安全性,而且顯然還可以提高代碼的可讀性。靜態類型還可以提供更多的信息,使得編譯器更容易進行優化,從而提高性能。但是靜態類型贏得開發人員青睞的最大原因是更容易檢測錯誤,而且有更多可用的工具。
動態類型的優點
Ruby 專家 Dave Thomas 將動態類型稱作duck typing(見參考資料),這有兩層意思。第一層意思是說,這種語言不真正實現類型 —— 它利用鴨子理論 解決這個問題。第二層意思是說,如果什么東西走起來像鴨子,叫起來也像鴨子,那么它很可能就是一只鴨子。在編程語言的上下文中,duck typing 意味著如果一個對象對于某種類型的方法有反應,那么事實上就可以把它看作那種類型。這樣的特性可以導致一些有趣的優化。
大多數偏愛動態類型的開發人員除了強調早期錯誤檢測會帶來不必要的成本外,還提到動態類型語言具有很好的可表達性和生產率。很簡單,您通??梢杂酶俚年P鍵詞表達更多的思想。作為一名新的 Ruby 擁護者,我深信動態語言更能提高生產率,雖然我不能比常見的靜態語言的支持者拿出更多具體的證據來。但是,從我開始編寫更多的 Ruby 代碼起,我就感覺到自己的生產率有了明顯的提高。誠然,我仍然會看到靜態類型的優點,尤其是在工具集方面,但是我逐漸認識到了靜態類型的缺點。
當我開始用 Ruby 編寫代碼時,我受到的最大改變是產生和使用元編程結構的能力。如果您從頭開始一直關注跨越邊界 系列統的話,您就知道元編程,或者說編寫用于編寫程序的程序,是 Ruby on Rails 的一大推動力量,更一般地說,是特定于領域的語言的一大推動力量。用 Ruby 編程時,我通常會編寫更大的構建塊,或者用更大的塊進行構建。我發現,與使用 Java 編程相比,我可以用更多類型的可重用塊擴展我的程序。就像在 Java 編程中,您可以用新的類來擴展程序。還可以添加方法和數據到已有的類中,因為類是開放式的。您可以使用 mix-in(后面 運行時綁定 會講到)來添加核心功能到已有的類中。還可以在任何時候根據需要改變一個對象的定義。我還是一名 Ruby 編程新手,這些功能用到的不多,但是當我真正開始使用它們時,結果令人大吃一驚。
例如,為了添加一個攔截器,只需重新命名一個方法,并為原有的方法創建一個新的實現。為了攔截 new
,可以編寫以下代碼:
|
您不需要 AspectJ 庫、字節碼增強或一大堆的庫。您可以直接編寫所需的攔截器。
動態類型在原始代碼行方面也可以節省精力。由于動態語言幾乎都是類型推斷式的,所以您不需要花多大力氣來表達基本思想。變量無需聲明即可直接使用。您也不必表達參數類型的所有可能排列,只需輸入一組名稱。您的代碼可以更加具有多態性 —— 任何對一種類型的方法有反應的對象都可以看作這種類型 —— 所以通??梢员绕渌Z言更精簡地表達思想。代碼中的耦合也可以變得更松散。當您想改變某個東西的類型時,這種變化所波及的范圍很有限,所以不需要在更多的地方作出相應的更改。
![]() |
|
生產率提高的最后一個原因是減少了編譯環節。很多動態類型語言是解釋性的,所以在編寫程序后可以立即看到變化。即使沒有慣用的調試器,在 Ruby 中探索庫和應用程序代碼的行為也更為容易,因為您可以打開一個解釋器,通??梢灾苯釉谡{試會話中打開(我在 本系列的上一篇文章 中就展示了這一點),然后隨意探索。
但是……
然而,編譯不只是支持靜態類型。靜態類型的支持者還認為可以獲得更好的性能。很多靜態語言,例如 Java 代碼、C 和 C++,都被稱作系統語言,因為它們是構建操作系統、設備驅動程序和其他高性能系統代碼的最常用的語言。這又經常導致動態語言的支持者指責靜態語言總是太低級,用它們來編寫應用程序生產率很低 —— 但那是一種很狹隘的觀點。OCaml 語言是一種很高級的語言,支持面向對象程序設計、函數式程序設計(如 Lisp 或 Erlang)或傳統的結構化程序設計。其類型模型是靜態的,很多人說它的性能甚至比 C++ 的性能還好(參見 參考資料)。使用 OCaml 時,靜態類型導致的開銷很小,因為這種語言是類型推斷式的。雖然付出了這一點成本,但可以得到非常好的性能,編譯時類型檢查,以及一個非常高級的語言。即使是 duck typing 最頑固的支持者也不得不承認那些優點。
![]() ![]() |
![]()
|
Java 語言中的類型限制
Java 開發人員充分利用靜態類型。他們有最好的開發工具,這些工具帶有代碼完成和重構等功能,這些都傾向于靜態類型?,F在開始利用測試優先開發的很多 Java 程序員獲得了更大的穩定性,因為編譯器可以捕捉與類型相關的 bug。新的類型特性,例如泛型,增強了類型模型,并為編譯器提供更多的信息。但 Java 開發人員常常對動態類型的優點一無所知。
運行時綁定
動態類型的靈活性比您想像的更重要。在某些方面,Java 開發人員試圖通過使用更多的 XML(這樣可以推遲到運行時進行綁定)和字符串(這樣可以表示很多不同的類型)來突破靜態類型的限制。Ruby 中的配置通常采用 Ruby 代碼的形式,而 Java 編程中的配置通常采用 XML 的形式??紤] Spring 框架(參見 參考資料):為了配置一個一般的 Spring bean,您使用 XML。您必須提供一個有效的 Java 類名,并為每個變量設置屬性。例如,持久引擎(如 Hibernate)需要一個會話工廠(參見 參考資料)。用 Java 語法配置一個數據訪問對象很輕松:
|
問題是,這行代碼是在編譯時綁定的,這就太靜態了。為了測試,您常常需要用其他東西,例如一個模擬的數據訪問對象來替換會話工廠或數據訪問對象。所以,您不必像前面那樣硬編碼這個例子,而是使用一個 Spring 之類的框架,以 XML 來配置項目,如下所示(摘自名為 petclinic 的 Spring Framework 例子):
|
Spring 框架是目前 Java 社區中最重要、最有影響力的框架之一,因為它使您可以延遲綁定,并使系統主要元素之間的耦合性更為松散。而且,您不需要關心繼承就可以去耦。在 Java 編程中,尤其是在編寫越來越多的 POJO(plain old Java object)的時候,使用繼承時必須特別小心,因為在 Java 語言中只有一次這樣的機會。
在動態語言,例如 Ruby 中,解決方案就截然不同。首先,我傾向于使用一個 mix-in 來實現持久性。所有關聯只在 mix-in 中發現一次??梢园岩粋€ mix-in 想像成一個接口,其背后有一個實現。換句話說,通過 mix-in,可以添加多個功能到同一個對象中,而不必使用多重繼承。實際上,Active Record 通過繼承一個公共基類來解決這個問題,這個公共基類混合了多種功能:
|
在 Ruby 中,您不必關心繼承,因為使用開放的類(允許動態添加功能)和模塊(允許混入其他功能),您可以隨意添加更多的功能到對象中。那么緊密耦合呢?如果您想按 Java 的方式實現該類,那么可以看到:
|
initialize()
方法中的代碼看上去像一開始的屬于禁忌的 Java 版本,因為它在編譯時將數據訪問對象綁定到會話工廠。但這是一種動態類型語言,所以不必把自己關在一個小天地里。為了測試,總可以動態地改變類的定義。您可以在之后打開已有的類:
|
![]() ![]() |
![]()
|
結束語
從某種意義上講,作為某種編程語言的用戶,您就是那種語言的類型策略的奴隸。而作為一名 Java 程序員,您應該盡量用一種擁護類型的方式編寫 Java 代碼。最大限度地利用類型,并依靠社區來通過框架獲得更好的元編程支持,而不是自己進行元編程,這些都是發揮自身優勢的好方法。有很多 Java 框架都支持用于持久性(Hibernate 和 JDO)、事務(Spring 和 EJB)、模型-視圖-控制器(WebFlow 和 RIFE)以及編程模型(AspectJ)的元編程。
但是有時候需要放棄您所選擇的語言的類型,不管您是在編寫需要附加描述以獲得更好可讀性的代碼,還是試圖延遲類型綁定,都可以這樣。Java 語言非常強大,您可以利用很多現成的項目:
您還有一個選擇。通過理解其他語言中的類型策略,可以識別不適合 Java 策略的問題。當需要訪問 Java 平臺 而不是 Java 語言 時,可以使用其他語言的 JVM 實現(參見 參考資料)。
在本系列的下一篇文章中,您將看到 Ruby on Rails 中的測試。您可以看到 Rails 與 Java 語言共有的一些思想,有些方面 Rails 占優,而在某些方面 Java 又具有明顯優勢。到那時,請繼續關注 “跨越邊界” 的后續文章。
原文轉自:http://www.anti-gravitydesign.com