Top Ten Traps in C# for C++ Programmers中文版(轉)
最後更新:2017-02-28
來源:互聯網
上載者:User
c++|中文 【譯序: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
】