我們建立的開發網站kalistick算是個Java專營店了,從一開始的第一版起,全部產品使用的都是Java代碼,但我們並不偏執,從2008年開始,我們也開始分析和使用C#。我注意到Java開發者大都在看待C#的時候多少帶些輕蔑,就好像這是一隻微軟仿造的山寨貓,蠢人和軟蛋們纔會用它。我在博客中開始清除這些觀念和誤解,展示C#的種種好處。
Java和C#的相同之處比不同處要多得多:兩種語言都是寫在大括號裏的,就像C和C++,類型都是靜態、強類型和顯式形態,兩種語言都是基於類的面嚮對象語言,兩者用的都是運行時編譯的思路,並且很好的使用了垃圾處理。
所以這篇文章裏,我要重點談談它們的相同點,以及C#的巧妙之處。
51CTO推薦專題:Visual Studio進化史
統一類型系統(Unified type system)
在Java中,原始數據類型(byte、int、bool、float、char等)和其他的類不同,它們並不算是面向對象,也不和引用類型共享相同的祖先類,但它們是有自己的包裝類的,用來代表自己並且用來插入到對象結構中(例如int使用Integer類),這樣做可以提高性能。
在另一邊,C#的統一類型系統卻都是從一個公用的根類型System.Object類中衍生而來的,即使是原始數據類型。所有的數據都要用到對象方法(ToString、Equal、GetHashCode等),所以你會碰上像3.ToString()這樣的表達式,這種把方法混合到後綴,就帶來了dsl風格的語句:
- TimeSpan workingTime = 7.Hours() + 30.Minutes();
這麼做的美妙之處在於當開發者把數據類型當做值來使用時,它們能夠和Java的原始類型一樣高效,只有在想要把它們當做對象使用時,系統才需要使用boxing/unboxing來分配堆內存。
顯式虛方法(Explicit virtual method)
在Java中,默認所有的方法都是虛方法(雖然這些方法可以使用final封裝起來而不允許覆蓋),而C#則不同,如果想在C#中寫一個虛方法,必須先要用virtual關鍵字顯式聲明一下。
有幾種原因決定了這樣的選擇,首先是性能上的考慮:虛方法都有一個懸在頭上的性能問題,因爲它們不是正常的內聯,需要通過vtable來進行調用,這種做法並不直接(Sun的JVM可以內聯上最經常調用的虛方法)。第二個也是更重要的原因就是版本問題:C#的設計思路是向後兼容,因此不同版本類庫中的基類和衍生類是可以進化發展和保持兼容的。例如,C#能夠完全支持基類中新加入的成員和衍生類中的成員同名,而不會導致無法預料的錯誤。最後一點是可讀性:開發者的編程意圖能夠非常明顯的讀出來。在Java中,如果開發者不寫出Override annotation的話,你不會知道他到底是不是想要重寫這個方法。
- class Meme
- {
- public virtual void Spread() {}
- }
- class ThreeHundred : Meme
- {
- public override void Spread()
- {
- Console.Write("This is sparta!");
- }
- }
- class Dbz: Meme
- {
- // Not a method override
- public void Spread()
- {
- Console.Write("It's over nine thousaaannnd!");
- }
- }
真正的泛型(True Generic)
關於泛型,Java和C#顯示出語法上的相似性,但真正深入理解之後你會發現這兩種語言在泛型處理上的差別很大。
Java的泛型是在編譯器中處理的,運行時並不關心泛型類型。Java在編譯中使用叫做類型擦除轉換的泛型類與方法:所有的泛型類型都被它們的原始版本替換,並且會在客戶代碼中插入cast和類型檢查,生成的字節代碼中並不包含任何泛型類型或參數的引用。Java的泛型是讓你在語法編寫上嚐到甜頭,但不會讓你的應用執行起來更有效。
而C#的泛型並不全是語言上的功能,它是放置在CLR(Common Language Runtime, 相當於JVM)中的。在編譯時需要進行泛型類型檢查驗證,但指定類型會推遲到類裝載時生成。代碼調用時的泛型是完全編譯的,而且可以假設泛型在類型上是安全的,這被稱爲泛型的具體化。和Java不同,C#不需要插入cast或者類型檢查。泛型類型和方法可以通過引用(class、delegate、interface等)和值類型(primitive type、struct、enum等)來創建。
C#中泛型的使用能夠帶來效率的提高(不需要cast和值類型的boxing/unboxing),還能夠提高深層次的安全驗證和反映能力。
- public void AwesomeGenericMethod(T t) where T : new()
- {
- T newInstance = new T (); // Causes a type creation error in Java
- T[] array = new T [0]; // Causes generic array creation error in Java
- T defaultValue = default(T);
- Type type = typeof(T);
- List list = new List ();
- }
- // Generic with same name but a different number of generic type
- public void AwesomeGenericMethod(T t, U u) where T : new()
- {
- }
Oracle的Java平臺總架構師Mark Reinhold在Devoxx 2011大會上曾經探討過給Java添加泛型的具體化問題,但這項功能還沒有規劃進Java的下一個主要版本中。
告別被檢查異常(checked exception)
Java和C#的異常檢查工作差不多一樣,二者唯一的主要區別是:Java中包含了checked exception這樣的異常。在Java裏你可以在方法聲明中拋出ExceptionType,這樣做可以強迫任何調用方法的函數來處理異常。這個想法在紙面上說說很好,但實際使用中卻很煩人,而且帶來了新問題。
版本問題:在新版本的方法聲明中加入一個checked exception會破壞客戶代碼,就像給一個接口添加方法一樣。比如在版本1中你創建了一個foo方法,聲明拋出異常A和B,在版本2中你添加了一些新功能,拋出異常D,這就是一個破壞性變化,因爲現有的調用程序不能處理這個異常。
擴展性問題:在大規模的應用項目中,相互依賴的工作是非常多的,因此拋出的異常會多的難以統計,開發者經常會繞開掉這個功能,通過拋出泛型異常或者使用空的catch塊。
checked exception背後的思路是了不起的,但是尤其在大項目中,它有點太強迫性了。這就是C#爲什麼不使用checked exception的原因,其他主流語言也一樣:留給開發者自己處理。
訪問器和修改器
Java的訪問器和修改器(getAddress、setAddress、isValid等)使用命名慣例。而在C#中,訪問器和修改器是內置的,自身帶有屬性,不需要再寫getter和setter,所有的工作看上去都是直來直去,即使內部並不是這樣的機制(許多其他語言也是這樣)。
- class Meme
- {
- // A private backing field is created by the compiler
- public string CatchPhrase { get; set;}
- public string URL { get; set;}
- }
- Meme meme = new Meme();
- meme.CatchPhrase = "Rick roll'd";
- meme.URL = "http://www.youtube.com/watch?v=EK2tWVj6lXw";
- // Equivalent in Java
- class Meme
- {
- private String catchPhrase;
- private String url;
- public String getCatchPhrase()
- {
- return catchPhrase;
- }
- public void setCatchPhrase(String catchPhrase)
- {
- this.catchPhrase = catchPhrase;
- }
- public String getUrl()
- {
- return url;
- }
- public void setUrl(String url)
- {
- this.url = url;
- }
- }
當你聲明一個屬性爲自動執行時,編譯器會創建一個私有的匿名域,只有這個屬性的get和set訪問器可以讀取。這帶來了兼容性,即使是在類的內部這個域也總是通過訪問器使用,這看上去幹淨簡練。
C#中有一類訪問器是Java中沒有的:索引器(indexer),它就像帶有參數的get和set。C#中的collection類比如Dictionary(和Java Map相類似)使用indexer。
- var keywordsMapping = new Dictionary<string, string>();
- keywordsMapping["super"] = "base";
- keywordsMapping["boolean"] = "bool";
- Console.Write("Java => C# : {0} => {1}", "super", keywordsMapping["super"]);
你可能會說,沒問題吧,這不就是寫了一個初始化函數嗎?
因爲經常要創建對象,然後初始化,這些可以用構造器來完成,要不然在創建對象之後你就要調用不同的set方法。
而對象的索引器可以在創建對象時就把值分配給對象的各種可以訪問的域或屬性,這樣就不需要調用構造器了。
- Meme leeroy = new Meme {
- CatchPhrase = "Leeroy Jenkins",
- URL = "http://www.youtube.com/watch?v=LkCNJRfSZBU"
- };
在collection類中也可以使用。
- List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
- Dictionary<string, string> keywordsMapping = new Dictionary<string, string>() {
- {"super", "base"},
- {"boolean", "bool"},
- {"import", "using"}
- };
逐字字符串(Verbatim string)
從字符串中把字符分解出來是非常痛苦的工作,尤其是混合着不同含義的正則表達式。C#的逐字字符串允許反斜槓、製表符、引號和換行符作爲字符串的一部分,不再需要轉義字符。
- string pattern = @"d{3}-d{3}-d{4}";
- string input = @"Multiline string
- 325-532-4521";
- Regex.IsMatch(input, pattern, RegexOptions.Multiline);
總結
通過本文我想說C#不僅和Java很相像,而且它能夠讓開發者的生活變得更輕鬆,能夠實在的減輕他們的負擔(其他語言也一樣),即使這是一隻山寨貓,那麼它做的也是相當不錯。
實際上Java開發者們也做出了相似的迴應,有些運行在JVM上的腳本語言例如Groovy就提供了這裏說到的大多數功能,但Java本身還略顯頑固,沒有做出改變。