Top Ten Traps in C# for C++ Programmers中文版(轉)
2024-07-21 02:22:20
供稿:網友
【譯序:c#入門文章。請注意:所有程序調試環境為microsoft visual studio.net 7.0 beta2和 microsoft .net framework sdk beta2。限于譯者時間和能力,文中倘有訛誤,當以英文原版為準】
在最近發表于《msdn magazine》(2001年7月刊)上的一篇文章里,我講了“從c++轉移到c#,你應該了解些什么?”。在那篇文章里,我說過c#和c++的語法很相似,轉移過程中的困難并非來自語言自身,而是對受管制的.net環境的適應和對龐大的.net框架的理解。
我已經編輯了一個c++和c#語法不同點的列表(可在我的web站點上找到這個列表。在站點上,點擊books可以瀏覽《programming c#》,也可以點擊faq看看)。正如你所意料的,很多語法上的改變是小而瑣細的。有一些改變對于粗心的c++程序員來說甚至是隱蔽的陷阱,本文將集中闡述十個危險的陷阱。
陷阱一.非確定性終結和c#析構器
理所當然,對于大多數c++程序員來說,c#中最大的不同是垃圾收集。這就意味你不必再擔心內存泄漏以及確保刪除指針對象的問題。當然,你也就失去了精確控制銷毀對象時機的能力。實際上,c#中并沒有顯式的析構器。
如果你在處理一個未受管制的資源,當你用完時,你需要顯式地釋放那些資源。可通過提供一個finalize方法(稱為終結器)隱式控制資源,當對象被銷毀時,它將被垃圾收集器調用。
終結器只應該釋放對象攜帶的未受管制的資源,而且也不應該引用別的對象。注意:如果你只有一些受管制的對象引用那你用不著也不應該實現finalize方法—它僅在需處理未受管制的資源時使用。因為使用終結器要付出代價,所以,你只應該在需要的方法上實現(也就是說,在使用代價昂貴的、未受管制的資源的方法上實現)。
永遠不要直接調用finalize方法(除了在你自己類的finalize里調用基類的finalize方法外【譯注:此處說法似乎有誤,參見下面譯注!】),垃圾收集器會幫你調用它。
c#的析構器在句法上酷似c++的析構器,但它們本質不同。c#析構器僅僅是聲明finalize方法并鏈鎖到其基類的一個捷徑【譯注:這句話的意思是,當一個對象被銷毀時,從最派生層次的最底層到最頂層,析構器將依次被調用,請參見后面給出的完整例子】。因此,以下寫法:
~myclass()
{
//do work here
}
和如下寫法具有同樣效果:
myclass.finalize()
{
// do work here
base.finalize();//
}
【譯注:上面這段代碼顯然是錯誤的,首先應該寫為:
class myclass
{
void finalize()
{
// do work here
base.finalize();//這樣也不可以!編譯器會告訴你不能直接調用基類的finalize方法,它將從析構函數中自動調用。關于原因,請參見本小節后面的例子和陷阱二的有關譯注!
}
}
下面給出一個完整的例子:
using system;
class rytestparcls
{
~rytestparcls()
{
console.writeline("rytestparcls's destructor");
}
}
class rytestchldcls: rytestparcls
{
~rytestchldcls()
{
console.writeline("rytestchldcls's destructor");
}
}
public class rytestdstrcapp
{
public static void main()
{
rytestchldcls rtcc = new rytestchldcls();
rtcc = null;
gc.collect();//強制垃圾收集
gc.waitforpendingfinalizers();//掛起當前線程,直至處理終結器隊列的線程清空該隊列
console.writeline("gc completed!");
}
}
以上程序輸出結果為:
rytestchldcls's destructor
rytestparcls's destructor
gc completed!
注意:在clr中,是通過重載system.object的虛方法finalize()來實現虛方法的,在c#中,不允許重載該方法或直接調用它,如下寫法是錯誤的:
class rytestfinalclass
{
override protected void finalize() {}//錯誤!不可重載system.object方法。
}
同樣,如下寫法也是錯誤的:
class rytestfinalclass
{
public void selffinalize() //注意!這個名字是自己取的,不是finalize
{
this.finalize()//錯誤!不能直接調用finalize()
base.finalize()//錯誤!不能直接調用基類finalize()
}
}
class rytestfinalclass
{
protected void finalize() //注意!這個名字和上面不一樣,同時,它也不是override的,這是可以的,這樣,你就隱藏了基類的finalize。
{
this.finalize()//自己調自己,當然可以,但這是個遞歸調用你想要的嗎?j
base.finalize()//錯誤!不能直接調用基類finalize()
}
}
對這個主題的完整理解請參照陷阱二。】
陷阱二.finalize和dispose
顯式調用終結器是非法的,finalize方法應該由垃圾收集器調用。如果是處理有限的、未受管制的資源(比如文件句柄),你或許想盡可能快地關閉和釋放它,那你應該實現idisposable接口。這個接口有一個dispose方法,由它執行清除動作。類的客戶負責顯式調用該dispose方法。dispose方法允許類的客戶說“不要等finalize了,現在就干吧!”。
如果提供了dispose方法,你應該禁止垃圾收集器調用對象的finalize方法—既然要顯式進行清除了。為了做到這一點,應該調用靜態方法gc.suppressfinalize,并傳入對象的this指針,你的finalize方法就能夠調用dispose方法。
你可能會這么寫:
public void dispose()
{
// 執行清除動作
// 告訴垃圾收集器不要調用finalize
gc.suppressfinalize(this);
}
public override void finalize()
{
dispose();
base.finalize();
}
【譯注:以上這段代碼是有問題的,請參照我在陷阱一中給的例子。微軟站點上有一篇很不錯的文章(gozer the destructor),說法和這兒基本一致,但其代碼示例在microsoft visual studio.net 7.0 beta2和 microsoft .net framework sdk beta2都過不了,由于手頭沒有beta1比對,所以,現在還不能確定是文章的筆誤,還是因為beta1和beta2的不同而導致,還是我沒有準確地理解這個問題。比如下面這個例子(來自gozer the destructor)在beta2環境下無法通過:
class x
{
public x(int n)
{
this.n = n;
}
~x()
{
system.console.writeline("~x() {0}", n);
}
public void dispose()
{
finalize();//此行代碼在beta2環境中出錯!編譯器提示,不能調用finalize,可考慮調用idisposable.dispose(如可用)
system.gc.suppressfinalize(this);
}
private int n;
};
class main
{
static void f()
{
x x1 = new x(1);
x x2 = new x(2);
x1.dispose();
}
static void main()
{
f();
system.gc.collect();
system.gc.waitforpendingfinalizers();
}
};
而該文聲稱會有如下輸出:
~x() 1
~x() 2
why?】
對于某些對象來說,你可能寧愿讓你的客戶調用close方法(例如,對于文件對象來說,close比dispose更妥貼)。那你可以通過創建一個private的dispose方法和一個public的close方法,并且在close里調用dispose。
因為你并不能肯定客戶將調用dispose,并且終結器是不確定的(你無法控制什么時候運行gc),c#提供了using語句以確保盡可能早地調用dispose。這個語句用于聲明你正在使用什么對象,并且用花括號為這些對象創建一個作用域。當到達“}”j時,對象的dispose方法將被自動調用:
using system.drawing;
class tester
{
public static void main()
{
using (font thefont = new font("arial", 10.0f))
{
// 使用thefont
} // 編譯器為thefont調用dispose
font anotherfont = new font("courier",12.0f);
using (anotherfont)
{
// 使用 anotherfont
} // 編譯器為anotherfont調用dispose
}
}
在上例的第一部份,thefont對象在using語句內創建。當using語句的作用域結束,thefont對象的dispose方法被調用。例子第二部份,在using語句外創建了一個anotherfont對象,當你決定使用anotherfont對象時,可將其放在using語句內,當到達using語句的作用域尾部時,對象的dispose方法同樣被調用。
using 語句還可保護你處理未曾意料的異常,不管控制是如何離開using語句的,dispose都會被調用,就好像那兒有個隱式的try-catch-finally程序塊。
陷阱三.c#區分值類型和引用類型
和c++一樣,c#是一個強類型語言。并且象c++一樣,c#把類型劃分為兩類:語言提供的固有(內建)類型和程序員定義的用戶自定義類型【譯注:即所謂的udt】。
除了區分固有類型和用戶自定義類型外,c#還區分值類型和引用類型。就象c++里的變量一樣,值類型在棧上保存值(除了嵌在對象中的值類型)。引用類型變量本身位于棧上,但它們所指向的對象則位于堆上,這很象c++里的指針【譯注:這其實更象c++里的引用j】。當被傳遞給方法時,值類型是傳值(做了一個拷貝)而引用類型則按引用高效傳遞。
類和接口創建引用類型【譯注:這個說法有點含糊,不能直接創建接口類型的對象,也并不是每一種類類型都是可以的,但可以將它們派生類的實例的引用賦給它們(說到“類類型”,不由得想起關于“型別”一詞的風風雨雨j)】,但要謹記(參見陷阱五):和所有固有類型一樣,結構也是值類型。
【譯注:可參見陷阱五的例子】
陷阱四.警惕隱式裝箱
裝箱和拆箱是使值類型(如整型等)能夠象引用類型一樣被處理的過程。值被裝箱進一個對象,隨后的拆箱則是將其還原為值類型。c#里的每一種類型包括固有類型都是從object派生下來并可以被隱式轉換為object。對一個值進行裝箱相當于創建一個對象,并將該值拷貝入該對象。
裝箱是隱式進行的,因此,當需要一個引用類型而你提供的是值類型時,該值將會被隱式裝箱。裝箱帶來了一些執行負擔,因此,要盡可能地避免裝箱,特別是在一個大的集合里。
如果要把被裝箱的對象轉換回值類型,必須將其顯式拆箱。拆箱動作分為兩步:首先檢查對象實例以確保它是一個將被轉換的值類型的裝箱對象,如果是,則將值從該實例拷貝入目標值類型變量。若想成功拆箱,被拆箱的對象必須是目標值類型的裝箱對象引用。
using system;
public class unboxingtest
{
public static void main()
{
int i = 123;
//裝箱
object o = i;
// 拆箱 (必須顯式進行)
int j = (int) o;
console.writeline("j: {0}", j);
}
}
如果被拆箱的對象為null或是一個不同于目標類型的裝箱對象引用,那將拋出一個invalidcastexception異常。【譯注:此處說法有誤,如果正被拆箱的對象為null,將拋出一個system.nullreferenceexception而不是system.invalidcastexcepiton】
【譯注:關于這個問題,我在另一篇譯文(a comparative overview of c#中文版(上篇))里有更精彩的描述j】
陷阱五.c#中結構是大不相同的
c++中的結構幾乎和類差不多。在c++中,唯一的區別是結構【譯注:指成員】缺省來說具有public訪問(而不是private)級別并且繼承缺省也是public(同樣,不是private)的。有些c++程序員把結構當成只有數據成員的對象,但這并不是語言本身支持的約定,而且這種做法也是很多oo設計者所不鼓勵的。
在c#中,結構是一個簡單的用戶自定義類型,一個非常不同于類的輕量級替代品。盡管結構支持屬性、方法、字段和操作符,但結構并不支持繼承或析構器之類的東西。
更重要的是,類是引用類型,而結構是值類型(參見陷阱三)。因此,結構對表現不需要引用語義的對象就非常有用。在數組中使用結構,在內存上會更有效率些,但若在集合里,就不是那么有效率了—集合需要引用類型,因此,若在集合中使用結構,它就必須被裝箱(參見陷阱四),而裝箱和拆箱需要額外的負擔,因此,在大的集合里,類可能會更有效。
【譯注:下面是一個完整的例子,它同時還演示了隱式類型轉換,請觀察一下程序及其運行結果j
using system;
class rytestcls
{
public rytestcls(int aint)
{
this.intfield = aint;
}
public static implicit operator rytestcls(ryteststt rts)
{
return new rytestcls(rts.intfield);
}
private int intfield;
public int intproperty
{
get
{
return this.intfield;
}
set
{
this.intfield = value;
}
}
}
struct ryteststt
{
public ryteststt(int aint)
{
this.intfield = aint;
}
public int intfield;
}
class ryclsstttestapp
{
public static void processcls(rytestcls rtc)
{
rtc.intproperty = 100;
}
public static void processstt(ryteststt rts)
{
rts.intfield = 100;
}
public static void main()
{
rytestcls rtc = new rytestcls(0);
rtc.intproperty = 200;
processcls(rtc);
console.writeline("rtc.intproperty = {0}", rtc.intproperty);
ryteststt rts = new ryteststt(0);
rts.intfield = 200;
processstt(rts);
console.writeline("rts.intfield = {0}", rts.intfield);
ryteststt rts2= new ryteststt(0);
rts2.intfield = 200;
processcls(rts2);
console.writeline("rts2.intfield = {0}", rts2.intfield);
}
}
以上程序運行結果為:
rtc.intproperty = 100
rtc.intfield = 200
rts2.intfield = 200
】