相關閱讀 |
>>> 技術話題—商業文明的嶄新時代 >>> | 簡體 傳統 |
本文翻譯時間較早。歡迎指出任何誤失。謝謝。
感謝以下人士的支持和反饋(按字母先后順序):
Don Box、C.R. Manning、Joe Nalewabau、John Osborn、Thomas Rhode、Daryl Richter。
本文以C#提供的新編程方式以及它對兩個近鄰Java和C++的改進為中心。C#在很多方面采用和Java類似的方式來改進C++,因此,我不打算重復諸如單根對象層次優點之類的東西。正文以對C#和Java的相似點概述開始,然后著重探究C#的新特性。
背景
2000年6月,微軟同時宣布了.NET平臺和一個名為C#的新編程語言。C#是一個很好地融合簡單性、表達力以及性能的強類型面向對象語言。.NET平臺以公共語言運行時(類似于Java虛擬機)和一個可被多種語言(它們可以通過編譯成中間語言從而可以協同工作)共用的庫為中心。C#和.NET有那么一些共生關系:C#的一些特性和.NET協作得很好,反之亦然(盡管.NET的目標是和多種語言很好地協作)。本文主要關注于C#,但視需要偶爾也會提及.NET。C#的設計借鑒了多種語言,但最主要的還是Java和C++。它是由Anders Hejlsberg(大名鼎鼎的Delphi語言設計師)和Scott Wiltamuth共同設計的。
目錄
C#和Java
屬性
索引器
委托
事件
枚舉
集合和foreach語句
結構
類型一致
操作符重載
多態
接口
版本處理
參數修飾符
特性(attribute)
選擇語句
預定義類型
字段修飾符
跳轉語句
程序集、名字空間和訪問級別
指針運算
多維數組
構造器和析構器
托管運行環境
庫
互操作性
結論
1.C#和Java
下面是C#和Java共有的特性列表,目的都是為了改進C++。這些特性雖非本文重點,但了解它們之間的相似之處還是非常重要的。
編譯為機器獨立、語言獨立的代碼,運行在托管運行環境中
采用垃圾收集機制,同時摒棄了指針(C#中,指針被限制在標為unsafe的代碼內使用)
強有力的反射能力
沒有頭文件,所有代碼都在包或裝程序集里,不存在類聲明的循環依賴問題
所有的類都派生自object,且必須用new關鍵字分配于堆上
當進入標以鎖定/同步代碼時,通過在對象上加鎖來支持多線程
接口支持:多繼承接口、單繼承實現
內部類
類繼承時無需指定訪問級別 [譯注:在C++中,你可以這么做:class cls2: private cls1{};等等]
沒有全局函數或常量,一切都必須屬于類
數組和字符串都保存長度記數并具邊界檢查能力
永遠使用“.”操作符,不再有“->”、“::”操作符
null和boolean/bool是關鍵字
所有的值在使用前必須被初始化
if語句不能使用整數作為判斷條件
try語句塊后可以跟finally子句
2.屬性
對于Delphi和Visual Basic用戶來說,屬性是個熟悉的概念。屬性的目的在于將獲取器/設置器(getter/setter)的概念正式化,這是一種被廣泛使用的模式,在RAD(快速應用開發)工具中尤其如此。
以下是你可能在Java或C++里寫的典型代碼:
foo.setSize(getSize() + 1);
label.getFont().setBold(true);
同樣 的代碼在C#里會變成:
foo.size++;
label.font.bold = true;
C#代碼對于使用foo和label的用戶來說更加直觀、可讀性更好。在屬性的實現方面,差不多同樣簡單:
Java/C++:
public int getSize()
{
return size;
}
public void setSize (int value)
{
size = value;
}
C#:
public int Size
{
get {return size;}
set {size = value;}
}
特別是對于可讀寫的屬性,C#提供了一個處理此概念的更清爽的方式。在C#中,get和set方法是內在的,而在Java和C++中則需人為維護。C#的處理方式有諸多優點。它鼓勵程序員按照屬性的方式去思考 — 把這個屬性標為可讀寫的和只讀的哪個更自然?或者根本不應該為屬性?如果你想改變你的屬性的名稱,你只要檢查一處就可以了(我曾看到過中間隔了幾百行代碼的獲取器和設置器里對同一個數據成員/字段的獲取器和設置器])。注釋也只要一處就可以了,這也避免了彼此同步的問題。IDE是可以幫助做這個事的(事實上,我建議微軟開發人員這么做),但應該牢記一個基本編程原理:盡力做好模擬我們問題空間的抽象。而一個支持屬性的語言將有助于獲得更好的抽象。
作者注:關于屬性的此項優點的一個反對意見認為:當采用這種語法時,你搞不清是在操縱一個字段還是屬性。然而,在Java(當然也包括C#)中,幾乎所有真正復雜一點的類都不會有public字段。字段一般都只具有盡可能小的訪問級別(private/protected,或語言所定義的 默認的),并且只通過獲取器和設置器方法暴露,這也意味著你可以獲得優美的語法。讓IDE解析代碼也是完全可行的:可以采用不同的顏色高亮顯示屬性,或提供代碼完成信息以表明它是否是一個屬性。我們還應該看到,如果一個類設計良好,這個類的用戶將只關心該類的接口(或 規范),而不是其內部實現。另外一個可能的爭論是屬性不夠有效率。事實上,好的編譯器可以內聯僅返回某個字段的獲取器,這和直接訪問字段一樣快。說到底,即使使用字段比獲取器/設置器來的有效,使用屬性還有如下好處:日后可以改變屬性 相關聯的字段,而不會影響依賴于該屬性的代碼。
3.索引器
C#通過提供索引器,可以像處理數組那樣處理對象。特別是屬性,每一個元素都以一個get或set方法暴露。
public class Skyscraper
{
Story[] stories;
public Story this [int index]
{
get { return stories [index]; }
set
{
if (value != null)
{
stories [index] = value;
}
}
}
//...
}
Skyscraper empireState = new Skyscraper (/*...*/);
empireState [102] = new Story ("The Top One", /*...*/);
譯注:索引器最大的好處是使代碼看上去更自然,更符合實際的思維模式。
4.委托
委托可以被認為是類型安全的、面向對象的函數指針,它可以擁有多個方法。委托處理的問題在C++中可以用函數指針處理,而在Java中則可以用接口處理。它通過提供類型安全和支持多方法改進了函數指針方式;它通過可以進行方法調用而無需內部類適配器或額外的代碼去處理多方法調用問題而改進了接口方式。委托最重要 的用途是事件處理,下一節將通過一個例子加以介紹。
5.事件
C#提供了對事件的直接支持。盡管事件處理一直是編程的基本部分,但令人驚訝的是,大多數語言在正式化這個概念方面所做的努力都微乎其微。如果看看現今主流框架是如何處理事件的,我們可以舉出如下例子:Delphi的函數指針(稱為閉包)和Java的內部類適配器,當然還有Windows API消息系統。C#使用delegate和event關鍵字提供了一個清爽的事件處理方案。我認為描述這個機制的最佳辦法是舉個例子來說明聲明、觸發和處理事件的過程:
// 委托聲明定義了可被調用的方法簽名
public delegate void ScoreChangeEventHandler (int newScore, ref bool cancel);
// 產生事件的類
public class Game
{
// 注意使用關鍵字
public event ScoreChangeEventHandler ScoreChange;
int score;
// 屬性Score
public int Score
{
get { return score; }
set
{
if (score != value)
{
bool cancel = false;
ScoreChange (value, ref cancel);
if (! cancel)
score = value;
}
}
}
}
// 處理事件的類
public class Referee
{
public Referee (Game game)
{
// 監視game中的score的分數改變
game.ScoreChange += new ScoreChangeEventHandler (game_ScoreChange);
}
// 注意這個方法簽名和ScoreChangeEventHandler的方法簽名要匹配
private void game_ScoreChange (int newScore, ref bool cancel)
{
if (newScore < 100)
System.Console.WriteLine ("Good Score");
else
{
cancel = true;
System.Console.WriteLine ("No Score can be that high!");
}
}
}
// 測試類
public class GameTest
{
public static void Main ()
{
Game game = new Game ();
Referee referee = new Referee (game);
game.Score = 70; // 譯注:輸出 Good Score
game.Score = 110; // 譯注:輸出 No Score can be that high!
}
}
在GameTest里,我們分別創建了一個game和一個監視game的referee,然后,然后我們改變game的Score去看看referee對此有何反應。在這個系統里,game不知道referee的任何知識,任何類都可以監聽并對game的score變化產生反應。關鍵字event隱藏了除了+=和-=之外的所有委托方法。這兩個操作符允許你添加(或移去)處理該事件的多個事件處理器。
譯注:我們以下例說明后面這句話的意思:
public class Game
{
public event ScoreChangeEventHandler ScoreChange;
protected void OnScoreChange()
{
if (ScoreChange != null) ScoreChange(30, ref true); // 在類內,可以這么使用
}
}
在這個類外,ScoreChange就只能出現在運算符+=和-=的左邊
你可能首先會在圖形用戶界面框架里遇到這個系統。game好比是用戶界面的某個控件,它根據用戶輸入觸發事件,而referee則類似于一個窗體,它負責處理該事件。
作者注:委托第一次被微軟Visual J++引入也是Anders Hejlsberg設計的,同時它也是造成Sun和微軟在技術和法律方面爭端的起因之一。James Gosling,Java的設計者,對Anders Hejlsberg曾有過一個故作謙虛聽起來也頗為幽默的評論,說他因為和Delphi藕斷絲連的感情應該叫他“方法指針先生”。在研究Sun對委托的爭執后,我覺得稱呼Gosling為“一切都是一個類先生”好像公平些:) 過去的這幾年里,在編程界,“做努力模擬現實的抽象”已經被很多人代之以“現實是面向對象的,所以,我們應該用面向對象的抽象來模擬它”。
Sun和微軟關于委托的爭論可以在這兒看到:
http://www.Javasoft.com/docs/white/delegates.html
http://msdn.microsoft.com/visualj/technical/articles/delegates/truth.asp
6.枚舉
枚舉使你能夠指定一組對象,例如:
聲明:
public enum Direction {North, East, West, South};
使用:
Direction wall = Direction.North;
這真是個優雅的概念,這也是C#為什么會決定保留它的原因,但是,為什么Java卻選擇了拋棄?在Java中,你不得不這么做:
聲明:
public class Direction
{
public final static int NORTH = 1;
public final static int EAST = 2;
public final static int WEST = 3;
public final static int SOUTH = 4;
}
使用:
int wall = Direction.NORTH;
看起來好像Java版的更富有表達力,但事實并非如此。它不是類型安全的,你可能一不小心會把任何int型的值賦給wall而編譯器不會發出任何抱怨。坦白地說,在我的Java編程經歷里,我從未因為該處非類型安全而花費太多的時間寫一些額外的東西來捕捉錯誤。但是,能擁有枚舉是一件快事。C#帶給你的一個驚喜是,當你調試程序時,如果你在使用枚舉變量的地方設置斷點,調試器將自動譯解direction并給你一個可讀 性良好的信息,而不是一個你不得不自己譯解的數值:
聲明:
public enum Direction {North=1, East=2, West=4, South=8};
使用:
Direction direction = Direction.North | Direction.West;
if ((direction & Direction.North) != 0)
//....
如果你在if語句上設置斷點,你將得到一個你可讀的direction而不是數值5。
譯注:將這個例子略作修改,會更有助于理解:
聲明:
public enum Direction {North=1, East=2, West=4, South=8, Middle = 5 /*注意此處代碼*/};
使用:
Direction direction = Direction.North | Direction.West;
if ((direction & Direction.North) != 0)
//....
如果你在if語句上設置斷點,你將得到一個可讀性好的direction(即Middle)而不是數值5。
作者注:枚舉被Java拋棄的原因極有可能是因為它可以用類來代替。正如我上面提到的,單單用類我們不能夠象用別的概念一樣更好地表達某個特性。Java的“如果它可以用類處理,那就不引入一個新的結構”的哲學的優點何在?看起來最大的優點是簡單 — 較短的學習曲線,并且無需程序員去考慮做同一件事的多種方式。實際上,Java語言在很多方面都以簡化為目標來改進C++,例如不用指針,不用頭文件,以及單根對象層次等。所有這些簡化的共性是它們實際上使得編程 — 唔 — 簡單了,可是,因為沒有我們剛才提到的枚舉、屬性和事件等等,它反而使你的代碼變得更加復雜了。
7.集合和foreach語句
C#提供一個for循環的捷徑,而且它還促進了集合類更為一致:
在Java或C++中:
1.while (!collection.isEmpty())
{
Object o = collection.get();
collection.next();
//...
2.for (int i = 0; i < array.length; i++) //...
在 C#中:
1.foreach (object o in collection) //...
2. foreach (int i in array) //...
C#的for循環將工作于集合對象上(數組實現一個集合)。集合對象有一個GetEnumerator()方法,該方法返回一個Enumerator對象。Enumerator對象有一個MoveNext()方法和一個Current屬性。
8.結構
把C#的結構視為使語言的類型系統更為優雅而不僅僅是一種“如果你需要的話可以利用之編寫出真正有效率的代碼”的概念,會更好一些。
在C++中,結構和類(對象)都可分配在棧或堆上。在C#中,結構永遠創建在棧上,類(對象)則永遠創建在堆上。使用結構實際上可以生成更有效率的代碼:
public struct Vector
{
public float direction;
public int magnitude;
}
Vector[] vectors = new Vector [1000];
這將把1000個Vector分配在一塊空間上,這比我們將Vector聲明為類并使用for循環去實例化1000個獨立的Vector來得有效率得多。
int[] ints = new ints[1000]; // 譯注:此處代碼有誤,應為int[] ints = new int[1000];
C#完全允許你擴展內建在語言中的基本類型集。實際上,C#所有的基本類型都以結構方式實現的。int只不過是System.Int32結構的別名,long不過是System.Int64結構的別名等等。這些基本類型當然可被編譯器特別處理,但是語言本身并無區別[譯注:意思是語言自身對處理所有類型提供了一致的方法]。在下一節中,我們 將會看到C#是如何做到這一點的。
9.類型一致
大多數語言都有基本類型(int、long等等)。高級類型最終是由基本類型構成的。能以同樣的方式處理基本類型和高級類型通常來說是有用處的。例如,倘若集合可以象包容sting那樣 而包容int,是有意義的。為此,Smalltalk通過犧牲些許效率像處理string或Form一樣來處理int和long。Java試圖避免這個效率損失,它 像C和C++那樣處理基本類型,但又為每一個基本類型提供了相應的包裝類 — int被包裝為Integer,double被包裝為Double。C++模板參數可接受任何類型,只要該類型提供了模板 所定義的操作的實現。
C#對該問題提供了一個不同的解決方案。在上一節里,我介紹了C#中的結構,指出基本類型不過是結構的一個別名而已。既然結構擁有所有對象類型擁有的方法, 那么代碼就可以這么寫:
int i = 5;
System.Console.WriteLine(i.ToString());
假如我們希望像使用對象那樣使用結構,C#會為你將該結構裝箱為對象,當你再次需要使用結構時,可以通過拆箱來實現:
Stack stack = new Stack ();
stack.Push (i); // 裝箱
int j = (int) stack.Pop(); // 拆箱
拆箱不僅是類型轉換的需要,它也是一個無縫地處理結構和類之間關系的方式。你要清楚裝箱是做了創建包裝類的工作。另外,CLR可以為被裝箱的對象提供額外的優化 處理。
譯注:可以這么認為,在C#中,對于任何值(結構)類型,都存在如下的包裝類:
class T_Box // T代表任何值類型
{
T Value;
T_Box(T t){Value = t;}
}
當裝箱時,比如:
int n = 1;
object box = n;
概念上相當于:
int n = 1;
object box = new int_Box(i);
當拆箱時,比如:
object box = 1;
int n = (int)box;
概念上相當于:
object box = new int_Box(1);
int n = ((int_Box)box).Value;
作者注:C#的設計者在設計過程中應該考慮過模板。我懷疑未采用模板有兩個原因:第一個是混亂,模板可能很難和面向對象的特性融合在一起,它為程序員的帶來了太多的(混亂)設計可能性,而且它很難和反射 機制協作。第二點是,倘若.NET庫(例如集合類)沒有使用模板的話,模板將不會太有用。不過,果真.NET類使用了它們,那將有20多種使用.NET類的語言不得不也要能和模板協作,這在技術上是非常難以實現的。
注意到模板(泛型)已被Java社團考慮納入Java語言規范是一件有意思的事。或許每個公司都會各唱各的調,Sun說“.NET患了最小公分母綜合癥”,而微軟則說“Java不支持多 種語言”。
(8月10日致歉)看了一篇對Anders Hejlsberg的專訪后(http://windows.oreilly.com/news/hejlsberg_0800.html),我感覺模板似已浮出地平線。不過C#第一版還沒有, 原因正在于我們上面提到的種種困難。看到IL規范是如此寫法使得IL碼可以展現模板(采用一種非破壞的方式以讓反射機制可以很好地工作)而字節碼則不可以 ,是一件很有趣的事。在此,我還給出一個關于Java社團考慮要加入泛型功能的鏈接:http://jcp.org/jsr/detail/014.jsp
10.操作符重載
利用操作符重載機制,程序員可以創建讓人感覺自然的好似簡單類型(如int、long等等)的類。C#實現了一個C++操作符重載的限制版,可以使諸如復數類操作符重載這樣的精辟的例子表現良好。
在C#中,操作符==是對象類的非虛方法(操作符不可以為虛的),它是按引用比較的。當你構建一個類時,你可以定義你自己的==操作符。如果你在集合中使用你的類,你應該實現IComparable接口。這個接口有一個叫CompareTo(object)方法,如果“this”大于、小于或等于這個object,它應該相應返回正數、負數或0。假如你希望用戶能夠用優雅的語法使用你的類, 那么你可以選擇定義<、<=、>=、>方法。數值類型(int、long等等)實現了IComparable接口。
下面是一個如何處理等于和比較操作的簡單例子:
public class Score : IComparable
{
int value;
public Score (int score)
{
value = score;
}
public static bool operator == (Score x, Score y)
{
return x.value == y.value;
}
public static bool operator != (Score x, Score y)
{
return x.value != y.value;
}
public int CompareTo (object o)
{
return value - ((Score)o).value;
}
}
Score a = new Score (5);
Score b = new Score (5);
Object c = a;
Object d = b;
按引用比較a和b:
System.Console.WriteLine ((object)a == (object)b); // 結果為false
比較a和b的值:
System.Console.WriteLine (a == b); // 結果為true
按引用比較c和d:
System.Console.WriteLine (c == d); // 結果為false
比較c和d的值:
System.Console.WriteLine (((IComparable)c).CompareTo (d) == 0); // 結果為true
你還可以向Score類添加<、<=、>=、>操作符。C#在編譯期保證邏輯上要成對出現的操作符(!=和==、>和<、>=和<=)必須一起被定義。
11.多態
面向對象的語言使用虛方法表達多態。這就意味著派生類可以有和父類具有同樣簽名的方法,并且父類可以調用派生類的方法。在Java中,默認情況下方法就是虛的。在C#中,必須使用virtual關鍵字才能使方法被父類調用。
在C#中,還需要override關鍵字以指明一個方法將覆寫(或實現一個抽象方法)其父類的方法:
class B
{
public virtual void foo () {}
}
class D : B
{
public override void foo () {}
}
試圖覆寫一個非虛方法將會導致一個編譯錯誤,除非對該方法加上new關鍵字,以指明該方法意欲隱藏父類的方法。
class N : D
{
public new void foo () {}
}
N n = new N ();
n.foo(); // 調用N的foo
((D)n).foo(); // 調用D的foo
((B)n).foo(); // 調用D的foo
和C++、Java相比,C#的override關鍵字使得閱讀源代碼時可以清晰地看出哪些方法是覆寫的。不過,使用虛方法有利有弊。首先,避免使用虛方法可以略微提高 運行速度,其次,可以清楚地知道哪些方法會被覆寫。然而,利也可能是弊。相較而言,Java中默認忽略final修飾符,C++中默認忽略virtual修飾符。Java中的默認選項使你的程序略微損失一些效率,而在C++中,它可能妨礙了擴展性,雖然這對基類的實現者來說是不可預料的。
12.接口
C#中的接口和Java中的接口差不多,但有更大的彈性。類可以隨意地顯式實現某個接口:
public interface ITeller
{
void Next ();
}
public interface IIterator
{
void Next ();
}
public class Clark : ITeller, IIterator
{
void ITeller.Next () {}
void IIterator.Next () {}
}
這給實現接口的類帶來了兩個好處。其一,一個類可以實現若干接口而不必担心命名沖突問題。其二,如果某方法對一般用戶沒有用,類能夠隱藏該方法。對顯式實現的方法的調用,需將對象轉型為相應的接口:
Clark clark = new Clark();
((ITeller)clark).Next();
13.版本處理
解決版本問題已成為.NET框架一個主要考慮,這些考慮的大多數都體現于程序集中。在C#中,可以在同一個進程中運行同一程序集的不同版本,這種能力 給人留下了深刻的印象。
當代碼的新版本(尤其是.NET庫)被創建時,C#可以防止軟件失敗。C#語言參考中詳細地描述了該問題。我用一個例子簡要講解如下:
在Java中,假定我們部署一個稱為D的類,它是從一個通過VM發布的叫B的類派生下來的。類D有一個叫foo的方法,而它在B發布時,B還沒有這個方法。后來,對類B做了升級,B也包括了一個叫foo的方法,新的VM現在安裝在使用類D的機器上了。現在,使用D的軟件可能會發生故障了,因為類B的新實現可能會導致一個對D的虛函數調用,這就執行了一個類B始料未及的動作[譯注:因Java中方法 默認是虛的]。在C#中,類D的foo方法應該聲明為不用override修飾符的(這真正表達了程序員的意愿),因此,運行時(runtime)知道讓類D的foo方法隱藏類B的foo方法,而不是覆寫它。
在此引用C#參考手冊中一句有意思的話:“C#處理版本問題,是通過要求開發人員明確自己的意圖而實現的”。盡管使用override是一個表達意圖的辦法,但編譯器也能自動生成 — 通過在編譯期檢查方法是否在執行(而不是聲明)一個覆寫。這就意味著,你仍然能夠擁有象Java一樣的語言(Java不用virtual和override關鍵字),并且仍然能夠正確處理版本問題。
參見字段修飾符部分 。
14.參數修飾符
(1)ref參數修飾符
C#(和Java相比)可以讓你按引用傳遞參數。描述這一點的最明顯的例子是通用swap方法。不像C++,在C#中,不但在聲明時,調用時也要加上ref指示符:
public class Test
{
public static void Main ()
{
int a = 1;
int b = 2;
swap (ref a, ref b);
}
public static void swap (ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
}
(2)out參數修飾符
out關鍵字是對ref參數修飾符的自然補充。ref修飾符要求參數在傳入方法之前必須被賦值,而out修飾符則明確表明當方法返回時需顯式給參數賦值。
(3)params參數修飾符
params修飾符可以加在方法的最后的參數上,意思是方法將接受任意數量的指定類型的參數。例如:
public class Test
{
public static void Main ()
{
Console.WriteLine (add (1, 2, 3, 4).ToString());
}
public static int add (params int[] array)
{
int sum = 0;
foreach (int i in array)
sum += i;
return sum;
}
}
作者注:在學習Java時,一件非常令人詫異的事情是發現Java不能按引用傳遞參數,盡管不久以后,你很少再想這個功能,并且寫代碼時也不需要它了。當我第一次閱讀C#規范的時候,我常想,“他們 干嗎要加上這個功能,沒有它我也能寫代碼”。經過反思以后,我意識到這其實并不是說明某些功能是否有用的問題,更多是說明了沒有它你就另需別的條件才能實現的問題。
當考慮到C++是怎么做的時候,Java是干了件好事,它簡化了參數如何傳遞的問題。在C++中,方法的參數和方法調用通過傳值、引用、指針使得代碼變得不必要的復雜。C#顯式傳遞引用,不管是方法聲明時還是調用時。它大大地減少了混亂[譯注:比方說,在C++中,有時你并不知道你是在使用一個對象還是一個對象引用,本節后有示例],并達到了和Java同樣的目標,但C#的方式更有表達力。顯然這是C#的主旨 — 它不把程序員圈在一個圈里,使他們必須繞一個大彎子才能做成某件事。還記得Java嗎?Java指南 對如何解決傳遞引用的問題建議如下:你應該傳遞一個具有1個元素的數組以便保存你的值,或者另做一個類以保存這個值。
譯注:
class ParentCls
{
public:
virtual void f(){printf("ParentCls\t");}
};
class ChildCls : public ParentCls
{
public:
virtual void f(){printf("ChildCls\t");}
};
void Test1(ParentCls pc) {pc.f();}
void Test2(ParentCls& pc) {pc.f();}
int main(int argc, char* argv[])
{
ChildCls cc;
// 只看調用處,我們不知道使用的是引用還是對象,但運行結果迥異!
Test1(cc); // 輸出ParentCls
Test2(cc); // 輸出ChildCls
return 0;
}
15.特性(attribute)
C#和Java的編譯代碼里都包括類似于字段訪問級別的信息。C#擴展了這個能力,對類中的任何元素,比如類、方法、字段甚至是獨立參數,你都可以編譯自定義的信息,并可以于運行時獲取這些信息。這兒有一個非常簡單的使用特性的類 的例子:
[AuthorAttribute ("Ben Albahari")]
class A
{
[Localizable(true)]
public string Text
{
get {return text;}
//...
}
}
Java使用一對/** */和@標簽注釋以包含類和方法的附加信息,但這些信息(除了@deprecated[譯注:Java1.1版本及以后])并未build到字節碼中。C#使用預定義的特性Obsolete特性,編譯器可以警告你,排除廢代碼(就象@deprecated),并用Conditional特性使得可以 進行條件編譯。微軟新的XML庫使用特性來表達字段如何序列化到XML中,這就意味著你可以很容易地把一個類序列化到XML中,并可以再次重建它。另一個對特性的恰當應用是創建真正有威力的類瀏覽工具。C#語言規范詳盡地解釋了怎樣創建和使用特性。
16.switch語句
C#中的switch語句可以使用整型、字符、枚舉或字符串(C++和Java不可以)。在Java和C++中,如果你在任何一個case語句里忽略了一個break語句,你就有其它case語句被執行的危險。我想不通為什么這個很少需要并容易出錯的行為在Java和C++中都成了默認行為,我也很高興看到C#不是這個樣子。
譯注: C#不允許從一個case標簽貫穿到另一個case標簽。果真需要如此,可以使用goto case或goto default實現。
17.預定義類型
C#基本類型基本上和Java的差不多,除了前者還加入了無符號的類型。C#中有sbyte、byte、short、ushort、int、uint、long、ulong、char、float和double。唯一令人感到驚奇的地方是這兒有一個16個字節[譯注:原文誤寫為12個字節]的浮點型數值類型decimal,它可以充分利用最新的處理器。
譯注:補充一下,盡管decimal占用128位,但它的取值范圍比float(32位)、double(64位)小得多,而其精度則比后兩者要高得多,可以滿足精度要求很高的財務計算等。
18.字段修飾符
C#中字段修飾符基本上Java相同。為了表示不可被修改的字段,C#使用const和readonly修飾符。const字段修飾符如同Java的final字段修飾符,該字段的實際值被編譯成IL代碼的一部分。只讀字段在運行時計算值。對標準C#庫來說,這 允許在不會破壞你已經部署的代碼的前提下進行升級。
19.跳轉語句
可能除了臭名卓著的goto語句外,這兒沒有更多令人驚訝的東西。然而,這和我們 記憶中麻煩多多的20年前basic的goto語句大不相同。一個goto語句必須指向一個標簽[譯注:goto語句必須在該標簽的作用域內。只允許使用goto語句將控制權傳遞出一個嵌套的作用域,而不能將控制權傳遞進一個嵌套域]或是switch語句里的一個選擇支[譯注:即所謂的goto case語句]。指向標簽的用法和continue差不多。Java里的標簽自由度大一些[譯注:Java中的break和continue語句后可跟標簽]。在C#中,goto語句可以指向其作用域的任意一個地方,這個作用域是指同一個方法或finally程序塊[譯注:如果goto語句出現在finally語句塊內,則goto語句的目的地也必須在同一個finally語句塊內]。C#中的continue語句和Java中的基本等價,但在C#中不可以指向一個標簽。
譯注:Java把goto作為保留字,但并未實現它。
20.程序集、名字空間和訪問級別
在C#中,你可以把你源代碼中的組件(類、結構、委托和枚舉等)組織到文件、名字空間和程序集中。
名字空間不過是長類名的語法上的小蜜糖而已。例如,用不著這么寫Genamics.WinForms.Grid,你可以這樣聲明類Grid并將其包含起來:
namespace Genamics.WinForms
{
public class Grid
{
//....
}
}
對于使用Grid的類,你可以用using關鍵字導入[譯注:即using Genamics.WinForms],而不必用其完整類名字Genamics.WinForms.Grid。
程序集是從項目文件編譯出來的exe或dll。.NET運行時使用可配置的特性和版本法則,把它們創建到程序集,這大大簡化了部署 :不需要寫注冊表,只要把程序集拷到相關目錄中去即可。程序集還可以形成一個類型邊界,從而解決類名沖突問題。同一程序集的多個版本可以共存于同一進程中。每一個文件都可以包含多個類、多個名字空間。一個名字空間可以橫跨若干個程序集。如此以來,系統將可獲得更大的自由度。
C#中有五種訪問級別:private、internal、protected、internal protected和public。private和public和Java中意思一樣。在C#中,沒有標明訪問級別的就是private,而非包(package)范圍的。internal訪問被局限在程序集中而不是名字空間(這和Java更相似)中。Internal protected等價于Java的protected。protected等價于Java的private protected,后者已被Java廢棄。
21.指針運算
在C#中,指針運算可以使用在被標為unsafe修飾符的方法里。當指針指向一個可被垃圾收集的對象時,編譯器強迫你使用fixed關鍵字將對象固定住,這是因為垃圾收集器是靠移動對象來回收內存的。如果正當你使用原始指針時,它所指的對象卻被移動了,那么你的指針將指向垃圾。我認為這兒用unsafe這個關鍵字是個好的選擇 — 它不鼓勵開發人員使用指針,除非他們真的想這么做。
22.多維數組
C#可以創建交錯數組[譯注:交錯數組是元素為數組的數組,其元素的維度和大小可以不同]和多維數組。交錯數組和Java的數組非常類似。多維數組使得可以更有效、更準確地表達特定問題。以下是這種數組的一個例子:
int [,,] array = new int [3, 4, 5]; // 創建一個數組
int [1,1,1] = 5; // 譯注:此行代碼有誤,應為array[1,1,1] = 5;
使用交錯數組:
int [][][] array = new int [3][4][5]; // 譯注:此行代碼有誤,應為:int [][][] array = new int[3][][];
int [1][1][1] = 5; // 譯注:此行代碼有誤,應為array[1][1][1] = 5;
若和結構聯合使用,C#提供的高效率使得數組成為圖形和數學領域的一個好的選擇。
23.構造器和析構器
你可以指定可選的構造器參數:
class Test
{
public Test() : this (0, null) {}
public Test(int x, object o) {}
}
你也可以指定靜態構造器:
class Test
{
static int[] ascendingArray = new int [100];
static Test()
{
for (int i = 0; i < ascendingArray.Length; i++)
ascendingArray [i] = i;
}
}
析構器的命名采用C++的命名約定,使用~符號。析構器只能應用于引用類型,值類型不可以,并且不可被重載。析構器不可被顯式調用,這是因為對象的生命期被垃圾收集器所管制。在對象占用的內存被回收前,對象繼承層次中的每一個析構器都會被調用。
盡管和C++的命名相似,但C#中的析構器更象Java中的finalize方法。這是因為它們都是被垃圾收集器調用而不是顯式地被程序員調用。而且,就象Java的finalize,它們不能保證在各種情況下都肯定被調用(這常常使第一次發現這一點的每一個人都感到震驚)。如果你已習慣于采用確定性的析構編程模式 (你知道什么時候對象的析構器被調用),那么,當你轉移到Java或C#時,你必須適應這種不同的編程模型。微軟推薦的和實現的、貫穿于整個.NET框架的是Dipose模式。你要為那些需要管理的外部資源(如圖形句柄或數據庫連接)的類定義一個Dispose()方法。對于分布式編程,.NET框架提供一個約定的基本模型,以改進DCOM的引用計數問題。
24. 托管運行環境
對[C#/IL碼/CLR]和[Java/字節碼/JVM]進行比較是不可避免的也是正當的。我想,最好的辦法是首先搞清楚為什么會創造出這些技術來。
用C和C++寫程序,一般是把源代碼編譯成匯編語言代碼[譯注:應該是機器碼],它只能運行在特定的處理器和特定的操作系統上。編譯器需要知道目標處理器,因為不同的處理器指令集不同。編譯器也要知道目標操作系統,因為不同的操作系統關于如何執行工作以及怎樣實現象內存分配等基本C/C++概念不同。C/C++這種模型獲得了巨大的成功(你所使用的大多數軟件可能都是這樣編譯的),但也有其局限性:
程序無豐富的接口以和其它程序進行交互(微軟的COM就是為了克服這個限制而創建的)
程序不能以跨平臺的形式分發
不能將程序限制在一個安全操作的沙箱中運行
為了解決這些問題,Java使用了Smalltalk采用過的方式,即編譯成字節碼,運行在虛擬機里。在被編譯前,字節碼維持程序的基本結構。這就使得Java程序和其它程序進行各種交互成為可能。字節碼也是機器中立的,這也意味著同樣的class文件可以運行于不同的平臺。最后,Java語言沒有顯式內存操作(通過指針)的事實使得它很適合于編寫“沙箱程序”。
最初的虛擬機利用解釋器來把字節碼指令流轉換為機器碼,但這個過程慢得可怕,以致于對那些關注性能的程序員來說從來都沒有吸引力。如今,絕大多數JVM都利用JIT編譯器,在進入類框架范圍之前和方法體執行之前,基本編譯成機器碼。在它運行前,還可以將Java程序轉換為匯編語言,可以避免啟動時間和即時編譯的內存負担。和編譯Visual C++程序相比,這個過程并不需要移去程序對運行時的依賴。Java運行時(這個術語隱藏在術語Java虛擬機下之下)將處理關于程序運行的很多至關重要的方面,比如垃圾收集和安全管理。運行時也被認為是托管運行環境。
盡管術語有點含糊不清,盡管從不用解釋器,但.NET基本模型也是使用如上所述方式。.NET重要的改進將來自于IL自身設計的改進。Java可以匹敵的唯一方式是修改字節碼規范以達到嚴格的兼容。我不想討論這些改進的細節,這應該留給那些極個別既了解字節碼也了解IL碼的開發人員去討論。99%的像我這樣的開發人員都不打算去研究IL代碼規范,這兒列出了一些意欲改進字節碼的IL設計決策:
提供更好的類型中立(有助于實現模板)
提供更好的語言中立
運行前永遠都編譯成匯編語言,從不解釋
能夠向類、方法等加入附加的聲明性信息。參見15.特性
目前,CLR還提供多操作系統支持,在其它領域還提供了對JVM更好的互操作支持。參見26.互操作能力。
25.庫
如果沒有庫,語言是沒什么用的。C#以沒有核心庫著稱,但它利用了.NET框架庫(它們中的一些就是用C#創建的)。本文著重于講述C#語言的特別之處,而不是.NET的 — 那應該另文說明。簡單地說,.NET庫包括豐富的線程、集合、XML、ADO+、ASP+、GDI+以及WinForm庫[譯注:現在這些+多已變成了.NET]。有些庫是跨平臺的,有些則依賴于Windows,請閱讀下一 節關于平臺支持的討論。
26.互操作能力
我認為把互操作分成三個部份論述是比較合適的:語言互操作、平臺互操作和標準互操作。Java長于平臺互操作,C#長于語言互操作。而在標準互操作方面,二者都各有長短。
(1)語言互操作
和其它語言集成能力只存在集成度和難易程度的區別。JVM和CLR都允許你使用多種語言編寫代碼,只要 將其編譯成字節碼或IL碼即可。然而,.NET平臺做了大量的工作 — 不僅僅能夠把其它語言代碼編譯成IL碼,它還使得多種語言可以自由共享和擴展彼此的庫。例如,Eiffel或Visual Basic程序員可以導入C#類,覆寫其虛方法;C#對象也可以使用Visual Basic方法(多態)。如果你懷疑的話,VB.NET已經被大幅升級,它已具有現代面向對象特性(當然付出了和VB6兼容性的損失)。
第三方.NET語言通常可被插入Visual Studio.NET環境中,假如需要的話,可以使用同樣的RAD框架。這就克服了使用其它語言的是“二等公民”的印象。
C#提供了P/Invoke[譯注:Platform Invocation Service,平臺調用服務],這比Java的JNI和C代碼交互起來要簡單得多(不需要dll)。這個特性很象J/direct,后者是微軟Visual J++的一個特性。
(2)平臺互操作
一般而言,這意味著操作系統互操作,但在過去的幾年里,IE自身已經越來越象個平臺了。
C#代碼運行在一個托管運行環境里。這是使C#能夠運行在不同操作系統之上的重要一步。然而,一些.NET庫是基于Windows的,特別是WinForms庫,它依賴于多如牛毛的Windows API。有個從Windows API移植到Unix系統項目,但目前還沒有啟動,而且微軟也沒有明確暗示要這么做。
然而,微軟并沒有忽視平臺互操作。.NET庫提供了編寫HTML/DHTML解決方案的擴展能力。對于可以用HTML/DHTML來實現的客戶端來說,C#/.NET是個不錯的選擇。對于跨平臺的需要更為復雜的客戶界面的項目而言,Java是個好的選擇。Kylix — Delphi的一個版本,允許同樣的代碼既可以在Windows上也可以在Linux上編譯,或許將來也會成為跨平臺解決方案的一個好選擇。
(3)標準互操作
幾乎所有標準,例如數據庫系統、圖形庫、internet協議和對象通訊標準如COM和CORBA,都可以經由C#訪問。由于微軟在制訂其中大多數標準 方面擁有權力或發揮了很大的作用,因此他們對這些標準的支持處于一個很有利的位置。微軟當然會因為商業上的動機(我沒有說他們是否公正)而對和他們競爭的東西(比如CORBA — COM的競爭對手,以及OpenGL — DirectX的競爭對手)提供較少的標準支持。類似地,Sun的商業動機(再一次,我沒有說他們是否公正)意味著Java也不會盡其所能地支持微軟的標準。
由于C#對象被實現為.NET對象,因此它可以自動暴露為COM對象,這樣,C#就既可以暴露COM對象也可以使用COM對象,從而我們可以將COM代碼和C#項目集成起來。.NET是一個有能力最終替代COM的框架,但是,已經有那么多已部署的COM組件,我相信,不等.NET取代掉COM,它就被下一波技術取代了。但無論如何,我們希望.NET能有一個長久而有趣的歷史!
27.結論
至此,我希望已為你展示了C#與Java、C++概念上的比較。總的來說,比起Java,我認為C#提供了更好的表達力并且更適于編寫性能要求嚴格的代碼,而且它同樣具有Java的優雅和簡單,這也是它們比C++更具吸引力之處。
Ben Albahari 著 榮耀 譯 2011-02-22 18:28:19
稱謂:
内容: