語言設計的藝術

出版圈郭志敏發表於2011-11-04

作者:崔康

利用QCon杭州2011大會的間歇期,讀完了《松本行弘的程式世界》的最後幾章,合上書還覺得意猶未盡,想再多翻幾遍。眾所周知,松本行弘是Ruby的發明者,這本書是他的技術文集,主要章節在過去幾年先後發表在日本的技術雜誌上。坦白的說,我對Ruby語言本身沒有深入的研究和實踐,所以閱讀本書的目的是想從一個旁觀者的角度瞭解程式語言在設計方面的各種考量、各種語言的特性對比以及對程式設計開發的影響。毫無疑問,這本書滿足了我的需求。不想說太多空泛的話,在這裡分享一下自己的閱讀心得和一些思考。

經常在技術論壇中看到類似於“xx語言和yy語言哪一個更好?”“zz語言有沒有前途?”的提問,然後眾說紛紜,群情激昂。有一種觀點認為:程式語言都一樣,學會了其中一種,另外的都觸類旁通。這種說法有一定道理,各種語言在不少方面都存在共性,畢竟都是“語言”。不過,語言之間的差距也非常大,這也是程式語言層出不窮的原因。大部分程式語言都是“圖靈完備”的,這意味著彼此可以實現等價的程式。不過,語言的選擇在一定程度上決定著開發效率。由於語言適用的領域各不相同,而且語言的生態系統(依附的廠商、整合開發環境、虛擬機器、社群推廣、第三方函式庫支援等)也許比語言本身更有影響力,所以我們只是從一般的角度來分析語言在設計上的考量。

物件導向

物件導向的設計方法已經深入人心,大多數現代語言都支援物件導向程式設計。多型性、資料抽象和繼承是物件導向程式設計的三個基本原則。從資料抽象角度來說,Ruby直接提供了棧等資料結構的支援,並隱藏了實現的細節。多型性和繼承密不可分。目前在程式語言中存在多種繼承方法,有多重繼承、單一繼承和Mix-in繼承。對於開發人員來說,哪種方式更有效率呢?單一繼承(如Smalltalk)的優點是:繼承關係是單純的樹結構,類之間的關係不會發生混亂,實現起來也較簡單,缺點是:無法通過繼承來共享多重程式程式碼,導致程式碼的冗餘。多重繼承(如C++)的優點是:可以繼承多個類的功能,擴充套件了單一繼承。缺點是:類之間的關係會變得複雜,一個類可能有多個父類,這些父類又有自己的父類,繼承關係不如單一繼承清晰,繼承的優先順序和功能可能存在衝突。開發人員既想利用多重繼承的優 點,又想避免它帶來的麻煩,所以需要引入受限制的多重繼承。Java提供的解決方案是——介面。Java只允許開發人員繼承(extends)單個父類, 但是可以實現(implements)多個介面。仔細想想,Java提供的這種解決方案是實現了“類規範”(即介面的方法宣告)的多重繼承,可以滿足多型性的要求。但是,如果開發人員需要複用類的實現程式碼呢?如何完成“類實現”(類的實現程式碼)的多重繼承呢?Ruby的設計者松本行弘在評估了各種語言在這方面的優劣之後,借鑑了Lisp語言的Mix-in繼承模式。這種模式的規則是:通常的繼承用單一繼承實現;第二個以及兩個以上的父類必須是Mix-in類。Mix-in類的特徵是:不能單獨生成例項;不能繼承普通類。這種繼承模式可以保證類的層次結構和單一繼承一樣的樹結構,同時又可以實現功能共享。開發人員可以把想要共享的程式碼放在Mix-in類中,然後把Mix-in類插入到繼承結構中,從而滿足“類實現”的多重繼承。開發人員利用支援Mix-in的語言做物件導向程式設計時會更加方便。值得注意的是,松本行弘在設計Ruby的時候,Mix-in模式並不流行,但是他堅持了自己的判斷,在Ruby中採用了該模式,時至今日,Mix-in受到越來越多開發人員的歡迎。松本行弘的前瞻性建立在對各種語言深入的分析和評估的基礎之上,具有紮實的依據,我們開發人員在預研某些技術時,不妨借鑑其做法。

超程式設計

超程式設計支援在程式語言特性中佔有重要的地位,開發人員可能對反射等概念比較瞭解,“在程式碼中動態分析、生成程式碼”的超程式設計能力對於基於程式語言的開發框架來說很重要,如果語言自身提供了強大的超程式設計支援,框架的開發者會事半功倍。Ruby提供了attr_accessor方法,支援開發人員動態生成訪問變數的方法。Ruby的反射功能可以獲取、更改各種範圍內的變數值,而且能夠獲取、刪除類方法,以及其他一些分析功能,當開發人員希望實現“通用程式設計”的模式或者後面提到的猴子補丁時,這些超程式設計功能會提供有效的支援。相比之下,C、C++語言可能實現起來就比較困難,雖然存在巨集定義等低效的辦法。

高階函式

高階函式的使用同樣可以提高開發效率,在函式模板化、容器迭代器等方面有著重要的應用。高階函式在C語言中採用了傳遞函式指標的形式來實現,但是存在侷限性,即實現函式間的資訊傳遞只有兩種方法,要麼明確地傳遞引數,要麼使用全域性變數。這種限制導致程式碼編寫的低效。為了解決此問題,Ruby和Javascript語言引入了閉包的概念,即函式(塊)可以引用外部的區域性變數。通常的外部變數在方法執行結束時就不存在了,但是如果被包括進了閉包,那麼在閉包存在期間,外部區域性變數也會一直存在(當然,閉包也會引起潛在的記憶體洩露問題)。Ruby中的塊結構是高階函式的一種特殊形式,程式碼塊可以作為引數傳遞給方法,在被呼叫的方法中可以執行傳遞過來的程式碼塊,執行後程式的控制權返還給方法,塊中最後執行的表示式的值是塊的值,這個值可以返回給方法。塊結構的經典應用是對集合物件(容器)的處理,比如迴圈執行、條件排序、條件搜尋等,開發人員只需把塊結構傳遞給容器方法,就可以方便的執行塊結構中的表示式並返回結果。之前C++和Java等容器類的迭代器,使用別的類物件來處理容器元素,屬於外部迭代器。Ruby通過塊結構和閉包實現了內部迭代器,不用額外生成物件。Ruby中的集合方法非常豐富,包括all、any、find、map、min、max、select、sort、inject等,這樣的設計能夠讓對資料結構和演算法有要求的開發人員操作起來更加簡潔和高效。

設計模式

提到設計模式,大家可能首先想到了著名的“四人幫”,還有他們歸納的23個設計模式。設計模式在軟體開發中的作用不言而喻,開發人員會有意無意的借鑑這些模式,語言的支援程度對開發效率的影響不可小覷。Ruby的語言庫很豐富,提供了多種設計模式的支援。比如singleton庫支援singleten模式、delegate庫支援proxy模式、塊結構支援iterator模式、clone方法支援prototype模式、observer庫支援observer模式,當然語言庫的設計也利用了不少設計模式,比如上面講到的容器集合方法Enumerable模組使用了Template Method模式來支援開發人員通過塊結構來指定所需的演算法。語言的設計者一方面要利用設計模式來優化語言庫的結構,另一方面也會通過語言庫自身來幫助開發人員在程式設計實踐時更方便地使用設計模式。

猴子補丁

程式語言對於猴子補丁的支援對軟體開發同樣重要。猴子補丁可以解釋為,不改變原始碼而對功能進行追加和變更。軟體開發過程中,有一個著名的開放-封閉原則(open-closed principle):對模組擴充套件必須開放,對修改必須封閉。模組是可以擴充套件的,比如追加新的資料結構或者功能,能夠滿足未來的需求。修改是封閉的,指被引用的模組內部細節發生變化時,對外介面應當是穩定的。猴子補丁能夠遵循該原則,它的主要目的包括追加和變更功能、修補程式錯誤等。Ruby這樣的語言提供了開放類,也就是說類定義之後也能任意的追加新內容,不僅如此,Ruby還提供了若干類操作方法,undef可以取消之前本類或者父類定義的方法,alias可以給方法起一個別名,開發人員可以在重新定義的方法中用別名來呼叫原來的方法,從而給原來的方法增加新功能,include可以把其他模組的功能包含進來。Ruby提供的這些方法使猴子補丁的實現過程更容易,對比Java等靜態語言,讀者可以發現Ruby語言在這方面處理靈活,開發效率更高。

資料型別與文字編碼

語言的型別定義和編碼是開發人員接觸的基本知識,在日常工作中應用廣泛。因此,語言在設計時對資料型別的內部實現是否具有擴充套件性和前瞻性就非常重要。比如語言選擇的文字編碼,有UCS(Universal Character Set)方式和CSI(Character Set Independent)方式。UCS方式指輸入輸出時,語言把文字資料變成統一的文字集(如UTF-8),內部對文字資料進行統一處理。UCS的方式得到各種程式語言的青睞。而CSI方式則指不對各種文字集和編碼方式做任何變換,原封不動的進行處理。UCS和CSI方式的優缺點都很明顯,這裡不過多討論。舉例來說,Java採用UCS方式,內部字元編碼為UTF-16,所以Java的char型別是16位。Java語言誕生時,Unicode僅限於16位,可以猜想這也是其設計者選擇UTF-16編碼方式的原因之一。但時過境遷,如今Unicode標準採用21位表示一個文字,所以Java的API需要升級才能處理變化之後的Unicode字元,開發人員可能需要作出相應的變更。Ruby採用CSI方式,提供了若干編碼方式的支援。我們不能籠統的說孰優孰劣,但是語言對字元型別和函式的設計對開發人員的效率有著直接的影響。同樣的,整數、浮點數的型別定義也是語言設計的考察點。相比某些語言對數字型別的位數限制,Ruby則提供了支援擴充套件的整數型別Bignum、浮點數型別BigDecimal,還有能夠表示分數的Rational類。

函數語言程式設計

函數語言程式設計是與物件導向程式設計相提並論的程式設計方法,最近越來越受到關注,它的最大優點在於,程式可以按照數學的形式以及宣告的形式來編寫。支援函數語言程式設計的語言能夠幫助開發人員把工作重點放在描述演算法上,而不是具體的實現操作。像Lisp、Erlang和Ruby都支援函數語言程式設計,不少語言是各種結構化程式設計、物件導向程式設計和函數語言程式設計的混合體,開發人員可以根據需要選擇高效的程式設計方式。說起這個話題,筆者不禁想起技術專家老趙,他經常會在講座前拿容器的集合方法為例對比Java和C#的程式碼實現,強調宣告式程式設計和Lambda表示式的好處,Ruby這樣的語言在設計時對此有所考慮,並選擇了有益的實現。

虛擬機器和垃圾回收

虛擬機器的誕生從某種程度上解決了語言在軟體開發中的跨平臺問題,虛擬機器對開發人員隱藏了作業系統級別的差異,語言庫的API介面保持一致。除了少數傳統語言,許多現代語言都使用了自動垃圾回收機制。開發人員無需手動處理物件的記憶體,省去了不少精力。當然,自動垃圾回收並不意味著沒有記憶體洩露,錯誤的程式碼會讓垃圾回收器無法釋放廢棄的物件。除此之外,垃圾回收器的效能也值得關注,如今垃圾回收演算法多種多樣,適用於不用的業務場景,開發人員需要關注。語言所依賴的虛擬機器其可靠性和效能如何,是考量的一個因素,Node.js的創始人就是因為對Ruby虛擬機器的效能不滿意而選擇了C和Javascript作為Node.js的實現語言。

動態型別與靜態型別

靜態型別和動態型別之爭一直在持續。靜態型別的優點在於編譯時能夠發現型別不匹配的錯誤,方便做優化,提高程式執行速度。缺點是作為輔助資訊的資料型別一定程度上影響了開發人員對程式本質的關注,而且不夠靈活。動態型別的優點在於原始碼變得很簡潔,可以靈活的處理未指定型別的變數,包括只關心行為的Duck Typing。缺點是在多數情況下,執行速度遜於靜態型別語言,而且不執行程式就難以檢測出錯誤。Java是靜態語言,Ruby是動態語言,它們各有千秋,都有廣泛的應用,但是目前看來,動態語言簡潔的程式設計風格受到越來越多開發者的歡迎。谷歌最近推出的Web程式語言Dart則是兼顧了兩者,開發人員可以根據自己的偏好和專案的階段來選擇是否為變數指定靜態資料型別,按照其說法,專案初期採用動態型別快速構建,後期通過靜態型別使程式更穩定和模組化,為開發人員提供了一種新思路。

小結

有關語言設計的討論涉及到很多方面,我們無法一一分析。比如,語言對正規表示式的支援程度如何?異常處理的設計是否合理?資料持久化是否方便?並行處理的能力如何?這些方面在分析語言的優缺點時也需要謹慎的考慮。同時,語言的生態系統也是考量的重要因素,整合開發環境、社群和廠商的支援、普及程度等都是開發人員在選擇程式語言時無法迴避的問題。很多時候,以如此細緻、理性的標準來決定使用哪門語言是沒有意義的,因為開發人員受到環境的各種限制,老闆的命令、公司的成見、團隊的意見、硬體的配置、IDE的支援、學習曲線的陡峭程度等等因素,都會影響開發者對程式語言的取捨。那麼,我們學習松本行弘的設計思想有什麼意義呢?在語言學有一個Sapir-Whirf假說,認為語言可以影響說話者的思想。計算機語言同樣可以影響開發人員的思考方式和由此產生的程式碼。Ruby語言在設計方面的考量和選擇,能夠幫助開發人員以更廣闊的視角和更高的層次來看待軟體開發中遇到的問題,解決的思路可以更加多樣化。即使開發者的語言各種各樣,Ruby的思想在軟體開發的設計、編碼方面仍然有寶貴的參考價值,《松本行弘的程式世界》是一個學習的切入點。

本文來自

相關文章