本文來源于我在InfoQ中文站翻譯的文章,原文地址是:http://www.infoq.com/cn/news/2013/01/C-Language
來自Couchbase的Damien Katz認為C依然是非常適合于后端編程的一門語言,然而有的開發者則覺得C有太多的瑕疵,他們支持C++或是Java,還有一些人連這兩種語言也不喜歡。
在最近一篇題為The Unreasonable Effectiveness of C的博文中,CouchDB的創建者Damien Katz表示C依然是非常適合于后端編程的一門語言,雖然現在已經有了很多更加現代化的語言,如C++、Java、甚至是Erlang或是Ruby,但他還是非常支持C語言。Katz并不是認為C就是比其他任何語言都要好,但在“重點考慮性能與可靠性等場景下,C是很難被打敗的”——這段話援引自Damien Katz隨后的一個帖子,旨在澄清自己的立場。
雖然一開始使用Erlang編寫了CouchDB的很多代碼,但在花費了“2+個人月來處理Erlang VM中的一個崩潰問題”之后,Katz感到非常不爽。
我們浪費了大量時間追蹤核心Erlang實現中的一些問題,不敢保證發生什么以及原因,我們覺得也許問題出現在我們自己的插件C代碼中,希望我們自己能夠發現并修復問題。但事實并非如此,這是核心Erlang中的一個競態條件Bug。我們只能通過Erlang的代碼查看器來找到這一問題。對于那些對計算機進行了過多抽象的編程語言來說,這是個很基本的問題。
出于這一點以及性能因素,Katz決定逐步重寫,“將Couchbase的代碼換成C,并將其作為大多數新特性的首選實現語言”。有趣的是,C證明了“當我們遇到問題、調試與修復問題時,C更具備可預測性。長遠來看,C的生產力更高”。
Katz列出了對于后端來說,C要優于更高層次語言如C++、Java等的若干原因:
- 表現力——“C的語法與語義非常強大且具有極強的表現力。憑借C,我們既能預測高層算法,又能預測低層的硬件。它的語義非常簡單,語法足夠強大,能夠極大降低認知上的負擔,讓程序員專注在重要的事情上”。
- 簡單——“C是一種弱、靜態類型語言,其類型系統非常簡單。我們所說的弱最后會變成一個優點:C APIs的“表面”是非常簡單且小巧的。相對于大多數框架來說,C的一個明顯趨勢與文化就是創建小型的庫,對簡單類型進行輕量級的抽象”。
- 速度與內存使用——“C是速度最快的語言,無論是部分還是完整的基準都表明了這一點。它不僅僅運行時是最快的,其內存使用與啟動時間也是效率最高的。如果需要在空間與時間上進行折衷,那么C并不會對你隱藏任何細節信息,我們可以很容易地做出估計”。
- 更快的開發周期——“對于開發者效率與生產力來說最為重要的就是‘構建、運行與調試’周期。周期越快,開發的交互性就越好,你就更容易處在任務的流態上。相對于所有主流的靜態語言來說,C擁有最為快速的開發交互性”。
- 調試——"對于純C代碼來說,你可以查看調用堆棧、變量、參數、線程局部變量、全局變量,基本上可以查看到內存中的一切。這是非常有用的,特別是當你遇到了問題,這個問題在運行的服務器進程中出現了多日,并且無法重現的情況下更是如此。如果在更為高層的語言中失去了這個上下文,那么你就等著痛苦去吧"。
- 跨平臺——“有一個標準化的應用二進制接口(ABI),可為現有的所有操作系統、語言與平臺所支持。它無需運行時,也不需要其他額外內容。這意味著你使用C所編寫的代碼并非僅僅可由C代碼中的調用者所調用,還可以由現有的所有庫、語言與環境所調用”。
Katz也認為C有“很多瑕疵”:
沒有范圍檢查,不小心就會導致內存出現問題、存在野指針與內存/資源泄露情況、對并發的附加支持、沒有模塊、沒有命名空間。錯誤處理非常麻煩且冗長。很容易就會搞出一堆錯誤,調用堆棧也找不到了,惡意輸入會控制你的進程。閉包?哈哈
Katz對C的強烈偏愛源自突破Couchbase性能極限的需求以及調試問題(問題是C插件與Erlang VM聯合使用所導致的)。他并不認為C++、Go或是D能夠替換C,但他認為Rust可能會成為“夢想之語言”,只要它能實現“類似于C的性能而且能夠做到與Erlang安全的并發和內建的健壯性”。
Katz的帖子在Reddit與Hacker News上可謂是一石激起千層浪,有很多開發者談到了C的優點,也有人建議其他語言。robinei加入到了字符串操作與錯誤檢測激戰當中:
我總想回到C(從C++等語言中),當我真的這么做了時,我發現不少地方通常都是很簡單的,感覺真棒!
但接下來我需要進行字符串操作,或是這類笨拙的方法。
這時會出現很多分配操作,每一個都需要一個顯式的free搞得我太痛苦了。我嘗試通過arena allocators樹來解決這個問題,就像Go中的slice-strings,但最終C還是缺乏一些語法工具(命名空間前綴函數),這導致結果變得非常笨拙(將一切都分配到arenas中也是非常痛苦的)。
我發現由于要進行顯式的錯誤檢測,源代碼文件長度增加了一倍(這種情況不常發生,但在諸如sqlite等一些庫中,任何操作都有可能失敗)。
還有很多方面導致我精疲力竭,我覺得非常不滿意。
綜上所述,我從哪兒來的還是回哪兒去吧,通常是C++。
madhadron提出了“更加現實的C”:
C能夠在PDP-11上很直接地編譯成快速的機器碼。
C的標準庫就是個笑話。它的缺點,特別是字符串相關的處理,是過去40年眾多安全漏洞的罪魁禍首。
C的工具根本不值得吹噓,特別是與同輩的Smalltalk和Lisp相比。人們所使用的大多數C調試器都是命令行。根本沒法和Squeak或是Allegro Common Lisp的標準調試器相比。
聲明快速的C構建/調試/運行周期令人沮喪。表面上看起來很快,因為C++在這個領域是失敗的。如果你想知道如何加快構建/調試/運行周期,那么請看看Turbo Pascal吧。
你可以通過標準ABI在所有Unix上調用C,雖然這么說沒錯,但原因卻是因為C的普遍存在性而已。
geophile對上述內容持不同看法:
C/C++/Java,這是程序員視角的石頭/剪刀/布。
多年以前,我從C開始,我發現自己用宏和庫為聲明與函數提供了很多很有用的組合。我發明了對象,同時也發現了C++。
長久以來,我一直是個快樂的C++用戶,很早就開始了(還是cfront時代)。但我被語言的復雜性搞崩潰了,特別是特性之間微妙的交互,我厭倦了內存管理,渴望Java,而它又適時地出現了。
我很開心。在學習語言時,我肯定我漏掉了某些東西。每個對象都在堆中么?真是如此么?真的沒有辦法將一個對象在物理上嵌入到另一個對象中么?但其他一切都很棒,我不介意這一點。
現在我在編寫一些系統,這些系統會占用很多內存,包含成千上萬的對象,有些對象很小,有些則很大。每個對象的代價快要搞死我了。GC調優是個夢魘,我正在實現子分配模式。我編寫了微基準,比較普通對象與序列化為字節數組的對象。由于C++已經變得太恐怖了,比令我焦頭爛額的早期版本還要復雜,因此我又渴望C了。
現在的我不再喜歡任何語言了。
對于某些人來說,C看起來瑕疵太多,已經不適應現代的生產力要求了,但還有不少人依然能夠很好地使用C,盡管它有很多怪癖。開發者社區還是應該避免語言之爭,而是更好地權衡每一種語言,根據項目需求與自身技能選擇最合適的語言。畢竟,沒有一種語言是完美的。
此文在InfoQ英文站也引來了眾多讀者的討論,下面摘取部分讀者的評論供大家參考。
讀者Mark Peskin說到:
嗯,我喜歡C。它很簡單,沒有C++那些龐大且有瑕疵的面向對象特性。然而,使用C編寫大型、可擴展、模塊化的企業應用需要大量規則,這些規則在大型的軟件企業中幾乎是無法維護的。我認為C的一個問題是它會引誘聰明的開發者編寫高度優化的代碼,充滿了memcpy()與指針運算,以此“打敗編譯器”,這對于其他開發者來說幾乎無法理解(如果不信,你去讀讀BSD內核代碼吧)。
總的來說,如果有很多開發者在開發大型、復雜且長期的項目,那么我認為你最好使用Java(或是Scala等語言)。將C用在那些偶爾出現的場景中吧,這時你可能真的需要本地代碼,使用基于消息的系統將二者集成起來(JNI不行)。C++?還是算了吧。
讀者Josh Long說到:
回應Katz關于C的問題。
我同意Mark的觀點,在某種程度上,也認可Damien的看法。
Damien提到的一點是我們很少在C中看到真正大型、全面的框架,比如APIs。如果構建小型、某個方面的API(通過一些typedefs/structs與函數作為“契約”),那么我們可以很輕松地將庫“導出”并重用。我覺得這是C最適合之處。我從來不會因為性能問題而使用C,但使用C實現某些功能則是更為輕松的事情(內核編程、嵌入式編程、處理硬件、所依賴的APIs并未在更高層次的語言如Java、Ruby、Python等進行過抽象的功能)。
我還盡量不使用C編寫具有完整功能的系統,只是因為它對于大型項目來說不具備“可伸縮性”。使用C編寫的大型項目最終的結果都是重新編寫了很多東西(比如說對象與命名空間等)。恕我直言,真的沒有多少領域需要從頭到尾都用C不可。當然了,一些例外是系統級組件,比如說操作系統(Linux)或是UI,如GNOME。但對于應用來說,使用更高層次的語言,在高層次語言與平臺之間存在縫隙之處再使用低層次APIs進行集成是更容易的做法。Java存在很多這類“縫隙”,但隨著APIs逐步成為很多不同操作系統上的常客后,在過去10年間,有些已經被逐步解決了:事件驅動的IO、文件系統通知、文件權限與元數據等等。
Mark為集成C庫與模塊提出了很好的解決方案。他認為JNI不行,建議使用消息。我是消息的忠實粉絲。本質來說,成功使用消息與成功使用JNI都需要同樣的東西:你需要徹底簡化導出API。
在使用JNI時,絕不要將任何復雜的C類型“泄漏”到我的Java API中,反之亦然,并且總是通過數值類型與char* -> jstrings進行通信。即便我所公開的本地代碼是用C++編寫的,我也依然會使用C風格的JNI(而不是C++),因為這種規范化有利于互操作。如果保持C API表面的簡潔性,并且避免線程,那么通過Java JNI、CPython或是MRI Ruby等可以將其作為本地擴展。
一旦完成了這個過程,接下來通過消息公開C API就變得很簡單了,因為根據定義,兩個系統之間的消息負載不可能比C庫還要復雜。當然了,如果使用消息,這意味著要么使用C編寫消息代碼,要么將C庫公開到更高層的語言上,并在那里實現消息。消息好的一面是能夠將高層語言代碼與C代碼進行隔離,這么做會比Java代碼要薄一些。我依然不會直接將使用C APIs編寫的代碼鏈接到我的應用中。如果C代碼掛掉了,那么消息系統就會接收請求,直到運行著C代碼的另一個節點能夠進行處理為止。另一方面,如果真的因為性能原因而使用C,那么消息至少會引入一個網絡傳輸,更不必說系統中的另一個組件了,這么做就抵消了使用C編碼所帶來的優勢了。在這種情況下,我們可以編寫穩定、行為良好的JNI或是本地擴展,但還是需要保持表面的小巧,并且理解前置與后置條件才行。沒有線程。不要在C與Java之間傳遞指向復雜對象的指針。請確保你自己清楚誰來負責清理內存,什么時候清理。
總而言之,忘掉C++吧。
讀者Bernd Kolb說到:
或許你想要看看mbeddr(mbeddr.com)。
mbeddr旨在更好地支持嵌入式軟件開發(但并不僅僅限于此),針對基于C語言與IDE的可擴展版本的小型與大型系統。現有擴展包括前置和后置的接口、組件、狀態機與物理單元,以及對需求追蹤和產品線變化的支持。基于這些抽象,mbeddr還支持基于模型檢測與SMT處理的形式驗證。
通過這種方式,在大型項目與團隊中,C也可以做到“可伸縮”。此外,我們還可以引入現代的編程規則。通過擴展mbeddr,你甚至可以為消息等添加基礎信息。
查看英文原文:Is C Still A Suitable Language Today?