C#(C-Sharp)是Microsoft的新程式設計語言,被譽為“C/C++家族中第一種面向組件的語言”。然而,不管它自己宣稱的是什麼,許多人認為C#更像是Java的一種複製,或者是Microsoft用來替代Java的產品。事實是否是這樣的呢? 本文的比較結果表明,C#不止是Java的同胞那麼簡單。如果你是一個Java開發人員,想要學習C#或者瞭解更多有關C#的知識,那麼本文就是你必須把最初10分鐘投入於其中的所在。 一、C#、C++和Java C#的語言規範由Microsoft的Anders Hejlsberg與Scott Wiltamuth編寫。在當前Microsoft天花亂墜的宣傳中,對C#和C++、Java作一番比較總是很有趣的。考慮到當前IT媒體的輿論傾向,如果你早就知道C#更接近Java而不是C++,事情也不值得大驚小怪。對於剛剛加入這場討論的讀者,下面的表1讓你自己作出判斷。顯然,結論應該是:Java和C#雖然不是孿生子,但C#最主要的特色卻更接近Java而不是C++。 表1:比較C#、C++和Java最重要的功能 功能 C# C++ Java 繼承 允許繼承單個類,允許實現多個介面 允許從多個類繼承 允許繼承單個類,允許實現多個介面 介面實現 通過“interface”關鍵詞 通過抽象類別 通過“interface”關鍵詞 記憶體管理 由運行時環境管理,使用垃圾收集器 需要手工管理 由運行時環境管理,使用垃圾收集器 指標 支援,但只在很少使用的非安全模式下才支援。通常以引用取代指標 支援,一種很常用的功能。 完全不支援。代之以引用。 原始碼編譯後的形式 .NET中繼語言(IL) 可執行代碼 位元組碼 單一的公用基類 是 否 是 異常處理 異常處理 返回錯誤 異常處理。 瞭解表1總結的重要語言功能之後,請繼續往下閱讀,瞭解C#和Java的一些重要區別。 二、語言規範的比較 2.1、單一資料型別 單一資料型別(Primitive)在C#中稱為實值型別,C#預定義的單一資料型別比Java多。例如,C#有unit,即不帶正負號的整數。表2列出了所有C#的預定義資料類型: 表2:C#中的實值型別 類型 說明 object 所有類型的最終極的基類 string 字串類型;字串是一個Unicode字元的序列 sbyte 8位帶正負號的整數 short 16位帶正負號的整數 int 32位帶正負號的整數 long 64位帶正負號的整數 byte 8位不帶正負號的整數 ushort 16位不帶正負號的整數 uint 32位不帶正負號的整數 ulong 64位不帶正負號的整數 float 單精確度浮點數類型 double 雙精確度浮點數類型 bool 布爾類型;bool值或者是true,或者是false char 字元類型;一個char值即是一個Unicode字元 decimal 有28位有效數位高精度小數類型 2.2、常量 忘掉Java中的static final修飾符。在C#中,常量可以用const關鍵詞聲明。 public const int x = 55; 此外,C#的設計者還增加了readonly關鍵詞。如果編譯器編譯時間未能確定常量值,你可以使用readonly關鍵詞。readonly域只能通過初始化器或類的建構函式設定。 2.3、公用類的進入點 在Java中,公用類的進入點是一個名為main的公用靜態方法。main方法的參數是String對象數組,它沒有傳回值。在C#中,main方法變成了公用靜態方法Main(大寫的M),Main方法的參數也是一個String對象數組,而且也沒有傳回值,如下面的原型聲明所示: public static void Main(String[] args) 但是,C#的Main方法不局限於此。如果不向Main方法傳遞任何參數,你可以使用上述Main方法的一個重載版本,即不帶參數列表的版本。也就是說,下面的Main方法也是一個合法的進入點: public static void Main() 另外,如果你認為有必要的話,Main方法還可以返回一個int。例如,下面代碼中的Main方法返回1: using System; public class Hello { public static int Main() { Console.WriteLine("Done"); return 1; } } 與此相對,在Java中重載main方法是不合法的。 2.4、switch語句 在Java中,switch語句只能處理整數。但C#中的switch語句不同,它還能夠處理字元變數。請考慮下面用switch語句處理字串變數的C#代碼: using System; public class Hello { public static void Main(String[] args) { switch (args[0]) { case "老闆": Console.WriteLine("早上好!我們隨時準備為您效勞!"); break; case "僱員": Console.WriteLine("早上好!你可以開始工作了!"); break; default: Console.WriteLine("早上好!祝你好運!"); break; } } } 與Java中的switch不同,C#的switch語句要求每一個case塊或者在塊的末尾提供一個break語句,或者用goto轉到switch內的其他case標籤。 2.5、foreach語句 foreach語句枚舉集合中的各個元素,為集合中的每一個元素執行一次代碼塊。請參見下面的例子。 using System; public class Hello { public static void Main(String[] args) { foreach (String arg in args) Console.WriteLine(arg); } } 如果在運行這個執行檔案的時候指定了參數,比如“Hello Peter Kevin Richard”,則程式的輸出將是下面幾行文字: Peter Kevin Richard 2.6、C#沒有>>>移位操作符 C#支援uint和ulong之類的無符號變數類型。因此,在C#中,右移操作符(即“>>”)對於無符號變數類型和帶符號變數類型(比如int和long)的處理方式不同。右移uint和ulong丟棄低位並把空出的高位設定為零;但對於int和long類型的變數,“>>”操作符丟棄低位,同時,只有當變數值是正數時,“>>”才把空出的高位設定成零;如果“>>”操作的是一個負數,空出的高位被設定成為1。 Java中不存在無符號的變數類型。因此,我們用“>>>”操作符在右移時引入負號位;否則,使用“>>”操作符。 2.7、goto關鍵詞 Java不用goto關鍵詞。在C#中,goto允許你轉到指定的標籤。不過,C#以特別謹慎的態度對待goto,比如它不允許goto轉入到語句塊的內部。在Java中,你可以用帶標籤的語句加上break或continue取代C#中的goto。 2.8、聲明數組 在Java中,數組的聲明方法非常靈活,實際上有許多種聲明方法都屬於合法的方法。例如,下面的幾行代碼是等價的: int[] x = { 0, 1, 2, 3 }; int x[] = { 0, 1, 2, 3 }; 但在C#中,只有第一行代碼合法,[]不能放到變數名字之後。 2.9、包 在C#中,包(Package)被稱為名稱空間。把名稱空間引入C#程式的關鍵詞是“using”。例如,“using System;”這個語句引入了System名稱空間。 然而,與Java不同的是,C#允許為名稱空間或者名稱空間中的類指定別名: using TheConsole = System.Console; public class Hello { public static void Main() { TheConsole.WriteLine("使用別名"); } } 雖然從概念上看,Java的包類似於.NET的名稱空間。然而,兩者的實現方式不同。在Java中,包的名字同時也是實際存在的實體,它決定了放置.java檔案的目錄結構。在C#中,物理的包和邏輯的名稱之間是完全分離的,也就是說,名稱空間的名字不會對物理的打包方式產生任何影響。在C#中,每一個原始碼檔案可以從屬於多個名稱空間,而且它可以容納多個公用類。 .NET中包的實體稱為程式集(Assembly)。每一個程式集包含一個manifest結構。manifest列舉程式集所包含的檔案,控制哪些類型和資源被顯露到程式集之外,並把對這些類型和資源的引用映射到包含這些類型與資源的檔案。程式集是自包含的,一個程式集可以放置到單一的檔案之內,也可以分割成多個檔案。.NET的這種封裝機制解決了DLL檔案所面臨的問題,即臭名昭著的DLL Hell問題。 2.10、預設包 在Java中,java.lang包是預設的包,它無需顯式匯入就已經自動包含。例如,要把一些文本輸出到控制台,你可以使用下面的代碼: System.out.println("Hello world from Java"); C#中不存在預設的包。如果要向控制台輸出文本,你使用System名稱空間Console對象的WriteLine方法。但是,你必須顯式匯入所有的類。代碼如下: using System; public class Hello { public static void Main() { Console.WriteLine("Hello world from C#"); } } 2.11、物件導向 Java和C#都是完全物件導向的語言。在物件導向編程的三大原則方面,這兩種語言接近得不能再接近。 繼承:這兩種語言都支援類的單一繼承,但類可以實現多個介面。所有類都從一個公用的基類繼承。 封裝與可見度:無論是在Java還是C#中,你都可以決定類成員是否可見。除了C#的internal存取修飾詞之外,兩者的可見度機制非常相似。 多態性:Java和C#都支援某些形式的多態性機制,且兩者實現方法非常類似。 2.12、可訪問性 類的每個成員都有特定類型的可訪問性。C#中的存取修飾詞與Java中的基本對應,但多出了一個internal。簡而言之,C#有5種類型的可訪問性,如下所示: public:成員可以從任何代碼訪問。 protected:成員只能從衍生類別訪問。 internal:成員只能從同一程式集的內部訪問。 protected internal:成員只能從同一程式集內的衍生類別訪問。 private:成員只能在當前類的內部訪問。 2.13、衍生類別 在Java中,我們用關鍵詞“extends”實現繼承。C#採用了C++的類派生文法。例如,下面的代碼顯示了如何派生父類Control從而建立出新類Button: public class Button: Control { . . } 2.14、最終類 由於C#中不存在final關鍵詞,如果想要某個類不再被派生,你可以使用sealed關鍵詞,如下例所示: sealed class FinalClass { . . } 2.15、介面 介面這個概念在C#和Java中非常相似。介面的關鍵詞是interface,一個介面可以擴充一個或者多個其他介面。按照慣例,介面的名字以大寫字母“I”開頭。下面的代碼是C#介面的一個例子,它與Java中的介面完全一樣: interface IShape { void Draw(); } 擴充介面的文法與擴充類的文法一樣。例如,下例的IRectangularShape介面擴充IShape介面(即,從IShape介面派生出IRectangularShape介面)。 interface IRectangularShape: IShape { int GetWidth(); } 如果你從兩個或者兩個以上的介面派生,父介面的名字列表用逗號分隔,如下面的代碼所示: interface INewInterface: IParent1, IParent2 { } 然而,與Java不同,C#中的介面不能包含域(Field)。 另外還要注意,在C#中,介面內的所有方法預設都是公用方法。在Java中,方法聲明可以帶有public修飾符(即使這並非必要),但在C#中,顯式為介面的方法指定public修飾符是非法的。例如,下面的C#介面將產生一個編譯錯誤。 interface IShape { public void Draw(); } 2.16、is和as操作符 C#中的is操作符與Java中的instanceof操作符一樣,兩者都可以用來測試某個對象的執行個體是否屬於特定的類型。在Java中沒有與C#中的as操作符等價的操作符。as操作符與is操作符非常相似,但它更富有“進取心”:如果類型正確的話,as操作符會嘗試把被測試的對象引用轉換成目標類型;否則,它把變數引用設定成null。 為正確理解as操作符,首先請考慮下面這個例子中is操作符的運用。這個例子包含一個IShape介面,以及兩個實現了IShape介面的類Rectangle和Circle。 using System; interface IShape { void draw(); } public class Rectangle: IShape { public void draw() { } public int GetWidth() { return 6; } } public class Circle: IShape { public void draw() { } public int GetRadius() { return 5; } } public class LetsDraw { public static void Main(String[] args) { IShape shape = null; if (args[0] == "rectangle") { shape = new Rectangle(); } else if (args[0] == "circle") { shape = new Circle(); } if (shape is Rectangle) { Rectangle rectangle = (Rectangle) shape; Console.WriteLine("Width : " + rectangle.GetWidth()); } if (shape is Circle) { Circle circle = (Circle) shape; Console.WriteLine("Radius : " + circle.GetRadius()); } } } 編譯好代碼之後,使用者可以輸入“rectangle”或者“circle”作為Main方法的參數。如果使用者輸入的是“circle”,則shape被執行個體化成為一個Circle類型的對象;反之,如果使用者輸入的是“rectangle”,則shape被執行個體化成為Rectangle類型的對象。隨後,程式用is操作符測試shape的變數類型:如果shape是一個矩形,則shape被轉換成為Rectangle對象,我們調用它的GetWidth方法;如果shape是一個圓,則shape被轉換成為一個Circle對象,我們調用它的GetRadius方法。 如果使用as操作符,則上述代碼可以改成如下形式: using System; interface IShape { void draw(); } public class Rectangle: IShape { public void draw() { } public int GetWidth() { return 6; } } public class Circle: IShape { public void draw() { } public int GetRadius() { return 5; } } public class LetsDraw { public static void Main(String[] args) { IShape shape = null; if (args[0] == "rectangle") { shape = new Rectangle(); } else if (args[0] == "circle") { shape = new Circle(); } Rectangle rectangle = shape as Rectangle; if (rectangle != null) { Console.WriteLine("Width : " + rectangle.GetWidth()); } else { Circle circle = shape as Circle; if (circle != null) Console.WriteLine("Radius : " + circle.GetRadius()); } } } 在上面代碼的粗體部分中,我們在沒有測試shape物件類型的情況下,就用as操作符把shape轉換成Rectangle類型的對象。如果shape正好是一個Rectangle,則shape被轉換成為Rectangle類型的對象並儲存到rectangle變數,然後我們調用它的GetWidth方法。如果這種轉換失敗,則我們進行第二次嘗試。這一次,shape被轉換成為Circle類型的對象並儲存到circle變數。如果shape確實是一個Circle對象,則circle現在引用了一個Circle對象,我們調用它的GetRadius方法。 2.17、庫 C#沒有自己的類庫。但是,C#共用了.NET的類庫。當然,.NET類庫也可以用於其他.NET語言,比如VB.NET或者JScript.NET。值得一提的是StringBuilder類,它是對String類的補充。StringBuilder類與Java的StringBuffer類非常相似。 2.18、垃圾收集 C++已經讓我們認識到手工管理記憶體是多麼缺乏效率和浪費時間。當你在C++中建立了一個對象,你就必須手工地拆除這個對象。代碼越複雜,這個任務也越困難。Java用垃圾收集器來解決這個問題,由垃圾收集器搜集不再使用的對象並釋放記憶體。C#同樣採用了這種方法。應該說,如果你也在開發一種新的OOP語言,追隨這條道路是一種非常自然的選擇。C#仍舊保留了C++的記憶體手工管理方法,它適合在速度極端重要的場合使用,而在Java中這是不允許的。 2.19、異常處理 如果你聽說C#使用與Java相似的異常處理機制,你不會為此而驚訝,對吧?在C#中,所有的異常都從一個名為Exception的類派生(聽起來很熟悉?)另外,正如在Java中一樣,你還有熟悉的try和catch語句。Exception類屬於.NET System名稱空間的一部分。 三、Java沒有的功能 C#出生在Java成熟之後,因此,C#擁有一些Java(目前)還沒有的絕妙功能也就不足為奇。 3.1、列舉程式 列舉程式即enum類型(Enumerator,或稱為計數器),它是一個相關常量的集合。精確地說,enum型別宣告為一組相關的符號常量定義了一個類型名字。例如,你可以建立一個名為Fruit(水果)的列舉程式,把它作為一個變數值的類型使用,從而把變數可能的取值範圍限制為列舉程式中出現的值。 public class Demo { public enum Fruit { Apple, Banana, Cherry, Durian } public void Process(Fruit fruit) { switch (fruit) { case Fruit.Apple: ... break; case Fruit.Banana: ... break; case Fruit.Cherry: ... break; case Fruit.Durian: ... break; } } } 在上例的Process方法中,雖然你可以用int作為myVar變數的類型,但是,使用列舉程式Fruit之後,變數的取值範圍限制到了Applet、Banana、Cherry和Durian這幾個值之內。與int相比,enum的可讀性更好,自我說明能力更強。 3.2、結構 結構(Struct)與類很相似。然而,類是作為一種參考型別在堆中建立,而結構是一種實值型別,它儲存在棧中或者是嵌入式的。因此,只要謹慎運用,結構要比類快。結構可以實現介面,可以象類一樣擁有成員,但結構不支援繼承。 然而,簡單地用結構來取代類可能導致慘重損失。這是因為,結構是以值的方式傳遞,由於這種傳遞方式要把值複製到新的位置,所以傳遞一個“肥胖的”結構需要較大的開銷。而對於類,傳遞的時候只需傳遞它的引用。 下面是一個結構的例子。注意它與類非常相似,只要把單詞“struct”替換成“class”,你就得到了一個類。 struct Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } 3.3、屬性 C#類除了可以擁有域(Field)之外,它還可以擁有屬性(Property)。屬性是一個與類或對象關聯的命名的特徵。屬性是域的一種自然擴充——兩者都是有類型、有名字的類成員。然而,和域不同的是,屬性不表示儲存位置;相反,屬性擁有存取器(accessor),存取器定義了讀取或者寫入屬性值時必須執行的代碼。因此,屬性提供了一種把動作和讀取、寫入對象屬性值的操作關聯起來的機制,而且它們允許屬性值通過計算得到。 在C#中,屬性通過屬性聲明文法定義。屬性聲明文法的第一部分與域聲明很相似,第二部分包括一個set過程和/或一個get過程。例如,在下面的例子中,PropertyDemo類定義了一個Prop屬性。 public class PropertyDemo { private string prop; public string Prop { get { return prop; } set { prop = value; } } } 如果屬性既允許讀取也允許寫入,如PropertyDemo類的Prop屬性,則它同時擁有get和set存取過程。當我們讀取屬性的值時,get存取過程被調用;當我們寫入屬性值時,set存取過程被調用。在set存取過程中,屬性的新值在一個隱含的value參數中給出。 與讀取和寫入域的方法一樣,屬性也可以用同樣的文法讀取和寫入。例如,下面的代碼執行個體化了一個PropertyDemo類,然後寫入、讀取它的Prop屬性。 PropertyDemo pd = new PropertyDemo(); pd.Prop = "123"; // set string s = pd.Prop; // get 3.4、以引用方式傳遞單一資料型別的參數 在Java中,當你把一個單一資料型別的值作為參數傳遞給方法時,參數總是以值的方式傳遞——即,系統將為被調用的方法建立一個參數值的副本。在C#中,你可以用引用的方式傳遞一個單一資料型別的值。此時,被調用的方法將直接使用傳遞給它的那個值——也就是說,如果在被呼叫者法內部修改了參數的值,則原來的變數值也隨之改變。 在C#中以引用方式傳遞值時,我們使用ref關鍵詞。例如,如果編譯並運行下面的代碼,你將在控制台上看到輸出結果16。注意i值被傳遞給ProcessNumber之後是如何被改變的。 using System; public class PassByReference { public static void Main(String[] args) { int i = 8; ProcessNumber(ref i); Console.WriteLine(i); } public static void ProcessNumber(ref int j) { j = 16; } } C#中還有一個允許以引用方式傳遞參數的關鍵詞out,它與ref相似。但是,使用out時,作為參數傳遞的變數在傳遞之前不必具有已知的值。在上例中,如果整數i在傳遞給ProcessNumber方法之前沒有初始化,則代碼將出錯。如果用out來取代ref,你就可以傳遞一個未經初始化的值,如下面這個修改後的例子所示。 using System; public class PassByReference { public static void Main(String[] args) { int i; ProcessNumber(out i); Console.WriteLine(i); } public static void ProcessNumber(out int j) { j = 16; } } 經過修改之後,雖然i值在傳遞給ProcessNumber方法之前沒有初始化,但PassByReference類能夠順利通過編譯。 3.5、C#保留了指標 對於那些覺得自己能夠恰到好處地運用指標並樂意手工進行記憶體管理的開發人員來說,在C#中,他們仍舊可以用既不安全也不容易使用的“古老的”指標來提高程式的效能。C#提供了支援“不安全”(unsafe)代碼的能力,這種代碼能夠直接操作指標,能夠“固定”對象以便臨時地阻止垃圾收集器移動對象。無論從開發人員還是使用者的眼光來看,這種對“不安全”代碼的支援其實是一種安全功能。“不安全”的代碼必須用unsafe關鍵詞顯式地標明,因此開發人員不可能在無意之中使用“不安全”的代碼。同時,C#編譯器又和執行引擎協作,保證了“不安全”的代碼不能偽裝成為安全的程式碼。 using System; class UsePointer { unsafe static void PointerDemo(byte[] arr) { . . } } C#中的unsafe代碼適合在下列情形下使用:當速度極端重要時,或者當對象需要與現有的軟體(比如COM對象或者DLL形式的C代碼)互動時。 3.6、代理 代理(delegate)可以看作C++或者其他語言中的函數指標。然而,與函數指標不同的是,C#中的代理是物件導向的、型別安全的、可靠的。而且,函數指標只能用來引用靜態函數,但代理既能夠引用靜態方法,也能夠引用執行個體方法。代理用來封裝可調用方法。你可以在類裡面編寫方法並在該方法上建立代理,此後這個代理就可以被傳遞到第二個方法。這樣,第二個方法就可以調用第一個方法。 代理是從公用基類System.Delegate派生的參考型別。定義和使用代理包括三個步驟:聲明,建立執行個體,調用。代理用delegate聲明文法聲明。例如,一個不需要參數且沒有傳回值的代理可以用如下代碼聲明: delegate void TheDelegate(); 建立代理執行個體的文法是:使用new關鍵詞,並引用一個執行個體或類方法,該方法必須符合代理指定的特徵。一旦建立了代理的執行個體,我們就可以用調用方法的文法調用它。 3.7、封裝和解除封裝 在物件導向的程式設計語言中,我們通常使用的是對象。但為了提高速度,C#也提供了單一資料型別。因此,C#程式既包含一大堆的對象,又有大量的值。在這種環境下,讓這兩者協同工作始終是一個不可迴避的問題,你必須要有一種讓引用和值進行通訊的方法。 在C#以及.NET運行時環境中,這個“通訊”問題通過封裝(Boxing)和解除封裝(Unboxing)解決。封裝是一種讓實值型別看起來象參考型別的處理過程。當一個實值型別(單一資料型別)被用於一個要求或者可以使用對象的場合時,封裝操作自動進行。封裝一個value-type值的步驟包括:分配一個對象執行個體,然後把value-type值複製到對象執行個體。 解除封裝所執行的動作與封裝相反,它把一個參考型別轉換成實值型別。解除封裝操作的步驟包括:首先檢查並確認對象執行個體確實是給定value-type的一個經過封裝的值,然後從對象執行個體複製出值。 Java對該問題的處理方式略有不同。Java為每一種單一資料型別提供了一個對應的類封裝器。例如,用Integer類封裝int類型,用Byte類封裝byte類型。 【結束語】本文為你比較了C#和Java。這兩種語言很相似,然而,說C#是Java的複製或許已經大大地言過其實。物件導向、中繼語言這類概念並不是什麼新東西。如果你準備設計一種物件導向的新語言,而且它必須在一個受管理的安全環境內運行,你難道不會搞出與C#差不多的東西嗎? |