C++程式設計人員容易犯的10個C#錯誤(轉)

BSDLite發表於2007-08-11
C++程式設計人員容易犯的10個C#錯誤(轉)[@more@]我們知道,C#的語法與C++非常相似,實現從C++向C#的轉變,其困難不在於語言本身,而在於熟悉.NET的可管理環境和對.NET框架的理解。儘管C#與C++在語法上的變化是很小的,幾乎不會對我們有什麼影響,但有些變化卻足以使一些粗心的C++程式設計人員時刻銘記在心。在本篇文章中我們將討論C++程式設計人員最容易犯的十個錯誤。

陷阱1: 沒有明確的結束方法

幾乎可以完全肯定地說,對於大多數C++程式設計人員而言,C#與C++最大的不同之處就在於碎片收集。這也意味著程式設計人員再也無需擔心記憶體洩露和確保刪除所有沒有用的指標。但我們再也無法精確地控制殺死無用的物件這個過程。事實上,在C#中沒有明確的destructor。

如果使用非可管理性資源,在不使用這些資源後,必須明確地釋放它。對資源的隱性控制是由Finalize方法(也被稱為finalizer)提供的,當物件被銷燬時,它就會被碎片收集程式呼叫收回物件所佔用的資源。finalizer應該只釋放被銷燬物件佔用的非可管理性資源,而不應牽涉到其他物件。如果在程式中只使用了可管理性資源,那就無需也不應當執行Finalize方法,只有在非可管理性資源的處理中才會用到Finalize方法。由於finalizer需要佔用一定的資源,因此應當只在需要它的方法中執行finalizer。直接呼叫一個物件的Finalize方法是絕對不允許的(除非是在子類的Finalize中呼叫基礎類的Finalize。),碎片收集程式會自動地呼叫Finalize。

從語法上看,C#中的destructor與C++非常相似,但其實它們是完全不同的。C#中的destructor只是定義Finalize方法的捷徑。因此,下面的二段程式碼是有區別的:

~MyClass()

{ // 需要完成的任務

}

MyClass.Finalize() {// 需要完成的任務

base.Finalize();

}


錯誤2:Finalize和Dispose使用誰?

從上面的論述中我們已經很清楚,顯性地呼叫finalizer是不允許的,它只能被碎片收集程式呼叫。如果希望儘快地釋放一些不再使用的數量有限的非可管理性資源(如檔案控制程式碼),則應該使用IDisposable介面,這一介面有個Dispose方法,它能夠幫你完成這個任務。Dispose是無需等待Finalize被呼叫而能夠釋放非可管理性資源的方法。

如果已經使用了Dispose方法,則應當阻止碎片收集程式再對相應的物件執行Finalize方法。為此,需要呼叫靜態方法GC.SuppressFinalize,並將相應物件的指標傳遞給它作為引數,Finalize方法就能呼叫Dispose方法了。據此,我們能夠得到如下的程式碼:

public void Dispose()

{

// 完成清理操作

// 通知GC不要再呼叫Finalize方法

GC.SuppressFinalize(this);

}

public override void Finalize() {

Dispose(); base.Finalize();

}

對於有些物件,可能呼叫Close方法就更合適(例如,對於檔案物件呼叫Close就比Dispose更合適),可以透過建立一個private屬性的Dispose方法和public屬性的Close方法,並讓Close呼叫Dispose來實現對某些物件呼叫Close方法。

由於不能確定一定會呼叫Dispose,而且finalizer的執行也是不確定的(我們無法控制GC會在何時執行),C#提供了一個Using語句來保證Dispose方法會在儘可能早的時間被呼叫。一般的方法是定義使用哪個物件,然後用括號為這些物件指定一個活動的範圍,當遇到最內層的括號時,Dispose方法就會被自動呼叫,對該物件進行處理。

using System.Drawing;

class Tester

{

public static void Main()

{

using (Font theFont = new Font("Arial", 10.0f))

{

//使用theFont物件

} // 編譯器將呼叫Dispose處理theFont物件

Font anotherFont = new Font("Courier",12.0f);

using (anotherFont)

{

// 使用anotherFont物件

} // 編譯器將呼叫Dispose處理anotherFont物件 }

}



在本例的第一部分中,Font物件是在Using語句中建立的。當Using語句結束時,系統就會呼叫Dispose,對Font物件進行處理。在本例的第二部分,Font物件是在Using語句外部建立的,在決定使用它時,再將它放在Using語句內,當Using語句結束時,系統就會呼叫Dispose。Using語句還能防止其他意外的發生,保證系統一定會呼叫Dispose。

錯誤3:C#中的值型變數和引用型變數是有區別的

與C++一樣,C#也是一種強型別程式語言。C#中的資料型別被分為了二大類:C#語言本身所固有的資料型別和使用者自定義資料型別,這一點也與C++相似。

此外,C#語言還把變數分為值型別和引用型別。除非是被包含在一個引用型別中,值型別變數的值保留在棧中,這一點與C++中的變數非常相似。引用型別的變數也是棧的一種,它的值是堆中物件的地址,與C++中的指標非常地相似。值型別變數的值被直接傳遞給方法,引用型變數在被作為引數傳遞給方法時,傳遞的是索引。類和介面可以建立引用類變數,但需要指出的是,結構資料型別是C#的一種內建資料型別,同時也是一種值型的資料型別。

錯誤4:注意隱性的資料型別轉換

Boxing和unboxing是使值型資料型別被當作索引型資料型別使用的二個過程。值型變數可以被包裝進一個物件中,然後再被解包回值型變數。包括內建資料型別在內的所有C#中的資料型別都可以被隱性地轉化為一個物件。包裝一個值型變數就會生成一個物件的例項,然後將變數複製到例項中。

Boxing是隱性的,如果在需要索引型資料型別的地方使用了值型資料型別的變數,值型變數就會隱性地轉化為索引型資料型別的變數。Boxing會影響程式碼執行的效能,因此應當儘量避免,尤其是在資料量較大的時候。

如果要將一個打包的物件轉換回原來的值型變數,必須顯性地對它進行解包。解包需要二個步驟:首先對物件例項進行檢查,確保它們是由值型的變數被包裝成的;第二步將例項中的值複製到值型變數中。為了確保解包成功,被解包的物件必須是透過打包一個值型變數的值生成的物件的索引。

using System;

public class UnboxingTest

{

public static void Main()

{

int i = 123; //打包

object o = i; // 解包(必須是顯性的)

int j = (int) o;

Console.WriteLine("j: {0}", j); }

}

如果被解包的物件是無效的,或是一個不同資料型別物件的索引,就會產生InvalidCastException異外。

錯誤5:結構與物件是有區別的

C++中的結構與類差不多,唯一的區別是,在預設狀態下,結構的訪問許可權是public,其繼承許可權也是public。一些C++程式設計人員將結構作為資料物件,但這只是一個約定而非是必須這樣的。在C#中,結構只是一個使用者自定義的資料型別,並不能取代類。儘管結構也支援屬性、方法、域和運算子,但不支援繼承和destructor。

更重要的是,類是一種索引型資料型別,結構是值型資料型別。因此,結構在表達無需索引操作的物件方面更有用。結構在陣列操作方面的效率更高,而在集合的操作方面則效率較低。集合需要索引,結構必須打包才適合在集合的操作中使用,類在較大規模的集合操作中的效率更高。

錯誤6:虛方法必須被明確地覆蓋

在C#語言中,程式設計人員在覆蓋一個虛方法時必須顯性地使用override關健字。假設一個Window類是由A公司編寫的,ListBox和RadioButton類是由B公司的和程式設計人員在購買的A公司編寫的Window類的基礎上編寫的,B公司的程式設計人員對包括Window類未來的變化情況在內的設計知之甚少。如果B公司的一位程式設計人員要在ListBox上新增一個Sort方法:

public class ListBox : Window

{ public virtual void Sort() {"}

}


在A公司釋出新版的Window類之前,這不會有任何問題。如果A公司的程式設計人員也在Window類中新增了一個Sort方法。

public class Window

{ // " public virtual void Sort() {"}

}

在C++中,Windows類中的Sort方法將成為ListBox類中Sort方法的基礎方法,在希望呼叫Windows類中的Sort方法時,ListBox類中的Sort方法就會被呼叫。在C#中,虛擬函式總是被認為是虛擬排程的根。也就是說,一旦C#發現一個虛擬的方法,就不會再在虛擬鏈中查詢其他虛擬方法。如果ListBox再次被編譯,編譯器就會生成一個警告資訊:

"class1.cs(54,24): warning CS0114: 'ListBox.Sort()' hides

inherited member 'Window.Sort()'.



要使當前的成員覆蓋原來的方法,就需要新增override關健字,或者新增new關健字。

要消除警告資訊,程式設計人員必須搞清楚他想幹什麼。可以在ListBox類中的Sort方法前新增new,表明它不應該覆蓋Window中的虛方法:

public class ListBox : Window {

public new virtual void Sort() {"}



這樣就可以清除警告資訊。如果程式設計人員確實希望覆蓋掉Window中的方法,就必須使用override關健字來顯性地表明其意圖。

錯誤7:類成員變數的初始化

C#中的初始化與C++中不同。假設有一個帶有private性質的成員變數age的Person類,Employee是由繼承Person類而生成的,它有一個private性質的salaryLevel成員變數。在C++中,我們可以在Employee的構造器的初始化部分初始化salaryLevel,如下面的程式碼所示:

Employee::Employee(int theAge, int theSalaryLevel):

Person(theAge) // 初始化基礎類

salaryLevel(theSalaryLevel) // 初始化成員變數

{

// 構造器的程式碼

}



這種方法在C#中是非法的。儘管仍然可以初始化基礎類,但象上面的程式碼那樣對成員變數初始化就會引起編譯錯誤。在C#中,我們可以在定義成員變數時的同時對它進行初始化:

Class Employee : public Person

{ // 成員變數的定義

private salaryLevel = 3; // 初始化

}



注意:必須明確地定義每個變數的訪問許可權。

錯誤8:布林型變數與整型變數是兩回事兒

if( someFuncWhichReturnsAValue() )

在C#中,布林型變數與整型變數並不相同,因此下面的程式碼是不正確的:

if( someFuncWhichReturnsAValue() )



if someFuncWhichReturnsAValue返回零表示false,否則表示true的想法已經行不通了。這樣的好處是原來存在的將賦值運算與相等相混淆的錯誤就不會再犯了。因此下面的程式碼:

if ( x = 5 )



在編譯時就會出錯,因為x=5只是把5賦給了X,而不是一個布林值。

錯誤9:switch語句中會有些語句執行不到

在C#中,如果一個switch語句執行了一些操作,則程式就可能不能執行到下一個語句。因此,儘管下面的程式碼在C++中是合法的,但在C#中卻不合法:

switch (i)

{

case 4:

CallFuncOne(); case 5: // 錯誤,不會執行到這裡

CallSomeFunc();

}

要實現上面程式碼的目的,需要使用一個goto語句:

switch (i)

{

case 4: CallFuncOne();

goto case 5; case 5:

CallSomeFunc();

}

如果case語句不執行任何程式碼,則所有的語句都會被執行。如下面的程式碼:

switch (i)

{

case 4: // 能執行到 case 5: // 能執行到

case 6: CallSomeFunc();

}

錯誤10:C#中的變數要求明確地賦值

在C#中,所有的變數在使用前都必須被賦值。因此,可以在定義變數時不對它進行初始化,如果在把它傳遞給一個方法前,必須被賦值。

如果只是透過索引向方法傳遞一個變數,並且該變數是方法的輸出變數,這是就會帶來問題。例如,假設有一個方法,它返回當前時間的小時、分、秒,如果象下面這樣編寫程式碼:

int theHour;

int theMinute;

int theSecond;

timeObject.GetTime( ref theHour, ref theMinute, ref theSecond)

如果在使用theHour、theMinute和theSecond這三個變數之前沒有對它們進行初始化,就會產生一個編譯錯誤:

Use of unassigned local variable 'theHour'

Use of unassigned local variable 'theMinute'

Use of unassigned local variable 'theSecond'

我們可以透過將這些變數初始化為0或其他對方法的返回值沒有影響的值,以解決編譯器的這個小問題:

int theHour = 0;

int theMinute = 0;

int theSecond = 0;

timeObject.GetTime( ref theHour, ref theMinute, ref theSecond)

這樣就有些太麻煩了,這些變數傳遞給GetTime方法,然後被改變而已。為了解決這一問題,C#專門針對這一情況提供了out引數修飾符,它可以使一個引數無需初始化就可以被引用。例如,GetTime中的引數對它本身沒有一點意義,它們只是為了表達該方法的輸出。在方法中返回之前,Out引數中必須被指定一個值。下面是經過修改後的GetTime方法:

public void GetTime(out int h, out int m, out int s) {

h = Hour;

m = Minute;

s = Second;

}

下面是新的GetTime方法的呼叫方法:

timeObject.GetTime( out theHour, out theMinute, out theSecond);

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-947630/,如需轉載,請註明出處,否則將追究法律責任。

相關文章