C++編程人員容易犯的10個(gè)C#錯(cuò)誤
發(fā)布時(shí)間:2008-08-07 閱讀數(shù): 次 來源:網(wǎng)樂原科技
我們知道, C#的語(yǔ)法與C++非常相似,實(shí)現(xiàn)從C++向C#的轉(zhuǎn)變,其困難不在于語(yǔ)言本身,而在于熟悉.NET的可管理環(huán)境和對(duì).NET框架的理解。
盡管C#與C++在語(yǔ)法上的變化是很小的,幾乎不會(huì)對(duì)我們有什么影響,但有些變化卻足以使一些粗心的C++編程人員時(shí)刻銘記在心。在本篇文章中我們將討論C++編程人員最容易犯的十個(gè)錯(cuò)誤。
陷阱1: 沒有明確的結(jié)束方法
幾乎可以完全肯定地說,對(duì)于大多數(shù)C++編程人員而言,C#與C++最大的不同之處就在于碎片收集。這也意味著編程人員再也無需擔(dān)心內(nèi)存泄露和確保刪除所有沒有用的指針。但我們?cè)僖矡o法精確地控制殺死無用的對(duì)象這個(gè)過程。事實(shí)上,在C#中沒有明確的destructor。
如果使用非可管理性資源,在不使用這些資源后,必須明確地釋放它。對(duì)資源的隱性控制是由Finalize方法(也被稱為finalizer)提供的,當(dāng)對(duì)象被銷毀時(shí),它就會(huì)被碎片收集程序調(diào)用收回對(duì)象所占用的資源。
finalizer應(yīng)該只釋放被銷毀對(duì)象占用的非可管理性資源,而不應(yīng)牽涉到其他對(duì)象。如果在程序中只使用了可管理性資源,那就無需也不應(yīng)當(dāng)執(zhí)行Finalize方法,只有在非可管理性資源的處理中才會(huì)用到Finalize方法。由于finalizer需要占用一定的資源,因此應(yīng)當(dāng)只在需要它的方法中執(zhí)行finalizer。
直接調(diào)用一個(gè)對(duì)象的Finalize方法是絕對(duì)不允許的(除非是在子類的Finalize中調(diào)用基礎(chǔ)類的Finalize。),碎片收集程序會(huì)自動(dòng)地調(diào)用Finalize。
從語(yǔ)法上看,C#中的destructor與C++非常相似,但其實(shí)它們是完全不同的。C#中的destructor只是定義Finalize方法的捷徑。因此,下面的二段代碼是有區(qū)別的:
~MyClass()
{
// 需要完成的任務(wù)
}
MyClass.Finalize()
{
// 需要完成的任務(wù)
base.Finalize();
}
錯(cuò)誤2:Finalize和Dispose使用誰?
從上面的論述中我們已經(jīng)很清楚,顯性地調(diào)用finalizer是不允許的,它只能被碎片收集程序調(diào)用。如果希望盡快地釋放一些不再使用的數(shù)量有限的非可管理性資源(如文件句柄),則應(yīng)該使用IDisposable界面,這一界面有個(gè)Dispose方法,它能夠幫你完成這個(gè)任務(wù)。Dispose是無需等待Finalize被調(diào)用而能夠釋放非可管理性資源的方法。
如果已經(jīng)使用了Dispose方法,則應(yīng)當(dāng)阻止碎片收集程序再對(duì)相應(yīng)的對(duì)象執(zhí)行Finalize方法。為此,需要調(diào)用靜態(tài)方法GC.SuppressFinalize,并將相應(yīng)對(duì)象的指針傳遞給它作為參數(shù),F(xiàn)inalize方法就能調(diào)用Dispose方法了。據(jù)此,我們能夠得到如下的代碼:
public void Dispose()
{
// 完成清理操作
// 通知GC不要再調(diào)用Finalize方法
GC.SuppressFinalize(this);
}
public override void Finalize()
{
Dispose();
base.Finalize();
}
對(duì)于有些對(duì)象,可能調(diào)用Close方法就更合適(例如,對(duì)于文件對(duì)象調(diào)用Close就比Dispose更合適),可以通過創(chuàng)建一個(gè)private屬性的Dispose方法和public屬性的Close方法,并讓Close調(diào)用Dispose來實(shí)現(xiàn)對(duì)某些對(duì)象調(diào)用Close方法。
由于不能確定一定會(huì)調(diào)用Dispose,而且finalizer的執(zhí)行也是不確定的(我們無法控制GC會(huì)在何時(shí)運(yùn)行),C#提供了一個(gè)Using語(yǔ)句來保證Dispose方法會(huì)在盡可能早的時(shí)間被調(diào)用。一般的方法是定義使用哪個(gè)對(duì)象,然后用括號(hào)為這些對(duì)象指定一個(gè)活動(dòng)的范圍,當(dāng)遇到最內(nèi)層的括號(hào)時(shí),Dispose方法就會(huì)被自動(dòng)調(diào)用,對(duì)該對(duì)象進(jìn)行處理。
using System.Drawing;
class Tester
{
public static void Main()
{
using (Font theFont = new Font("Arial", 10.0f))
{
//使用theFont對(duì)象
} // 編譯器將調(diào)用Dispose處理theFont對(duì)象
Font anotherFont = new Font("Courier",12.0f);
using (anotherFont)
{
// 使用anotherFont對(duì)象
} // 編譯器將調(diào)用Dispose處理anotherFont對(duì)象
}
}
在本例的第一部分中,F(xiàn)ont對(duì)象是在Using語(yǔ)句中創(chuàng)建的。當(dāng)Using語(yǔ)句結(jié)束時(shí),系統(tǒng)就會(huì)調(diào)用Dispose,對(duì)Font對(duì)象進(jìn)行處理。在本例的第二部分,F(xiàn)ont對(duì)象是在Using語(yǔ)句外部創(chuàng)建的,在決定使用它時(shí),再將它放在Using語(yǔ)句內(nèi),當(dāng)Using語(yǔ)句結(jié)束時(shí),系統(tǒng)就會(huì)調(diào)用Dispose。
Using語(yǔ)句還能防止其他意外的發(fā)生,保證系統(tǒng)一定會(huì)調(diào)用Dispose。
錯(cuò)誤3:C#中的值型變量和引用型變量是有區(qū)別的
與C++一樣,C#也是一種強(qiáng)類型編程語(yǔ)言。C#中的數(shù)據(jù)類型被分為了二大類:C#語(yǔ)言本身所固有的數(shù)據(jù)類型和用戶自定義數(shù)據(jù)類型,這一點(diǎn)也與C++相似。
此外,C#語(yǔ)言還把變量分為值類型和引用類型。除非是被包含在一個(gè)引用類型中,值類型變量的值保留在棧中,這一點(diǎn)與C++中的變量非常相似。引用類型的變量也是棧的一種,它的值是堆中對(duì)象的地址,與C++中的指針非常地相似。值類型變量的值被直接傳遞給方法,引用型變量在被作為參數(shù)傳遞給方法時(shí),傳遞的是索引。
類和界面可以創(chuàng)建引用類變量,但需要指出的是,結(jié)構(gòu)數(shù)據(jù)類型是C#的一種內(nèi)置數(shù)據(jù)類型,同時(shí)也是一種值型的數(shù)據(jù)類型。
錯(cuò)誤4:注意隱性的數(shù)據(jù)類型轉(zhuǎn)換
Boxing和unboxing是使值型數(shù)據(jù)類型被當(dāng)作索引型數(shù)據(jù)類型使用的二個(gè)過程。值型變量可以被包裝進(jìn)一個(gè)對(duì)象中,然后再被解包回值型變量。包括內(nèi)置數(shù)據(jù)類型在內(nèi)的所有C#中的數(shù)據(jù)類型都可以被隱性地轉(zhuǎn)化為一個(gè)對(duì)象。包裝一個(gè)值型變量就會(huì)生成一個(gè)對(duì)象的實(shí)例,然后將變量拷貝到實(shí)例中。
Boxing是隱性的,如果在需要索引型數(shù)據(jù)類型的地方使用了值型數(shù)據(jù)類型的變量,值型變量就會(huì)隱性地轉(zhuǎn)化為索引型數(shù)據(jù)類型的變量。Boxing會(huì)影響代碼執(zhí)行的性能,因此應(yīng)當(dāng)盡量避免,尤其是在數(shù)據(jù)量較大的時(shí)候。
如果要將一個(gè)打包的對(duì)象轉(zhuǎn)換回原來的值型變量,必須顯性地對(duì)它進(jìn)行解包。解包需要二個(gè)步驟:首先對(duì)對(duì)象實(shí)例進(jìn)行檢查,確保它們是由值型的變量被包裝成的;第二步將實(shí)例中的值拷貝到值型變量中。為了確保解包成功,被解包的對(duì)象必須是通過打包一個(gè)值型變量的值生成的對(duì)象的索引。
using System;
public class UnboxingTest
{
public static void Main()
{
int i = 123;
//打包
object o = i;
// 解包(必須是顯性的)
int j = (int) o;
Console.WriteLine("j: {0}", j);
}
}
如果被解包的對(duì)象是無效的,或是一個(gè)不同數(shù)據(jù)類型對(duì)象的索引,就會(huì)產(chǎn)生InvalidCastException異外。
錯(cuò)誤5:結(jié)構(gòu)與對(duì)象是有區(qū)別的
C++中的結(jié)構(gòu)與類差不多,唯一的區(qū)別是,在缺省狀態(tài)下,結(jié)構(gòu)的訪問權(quán)限是public,其繼承權(quán)限也是public。一些C++編程人員將結(jié)構(gòu)作為數(shù)據(jù)對(duì)象,但這只是一個(gè)約定而非是必須這樣的。
在C#中,結(jié)構(gòu)只是一個(gè)用戶自定義的數(shù)據(jù)類型,并不能取代類。盡管結(jié)構(gòu)也支持屬性、方法、域和操作符,但不支持繼承和destructor。
更重要的是,類是一種索引型數(shù)據(jù)類型,結(jié)構(gòu)是值型數(shù)據(jù)類型。因此,結(jié)構(gòu)在表達(dá)無需索引操作的對(duì)象方面更有用。結(jié)構(gòu)在數(shù)組操作方面的效率更高,而在集合的操作方面則效率較低。集合需要索引,結(jié)構(gòu)必須打包才適合在集合的操作中使用,類在較大規(guī)模的集合操作中的效率更高。
錯(cuò)誤6:虛方法必須被明確地覆蓋
在C#語(yǔ)言中,編程人員在覆蓋一個(gè)虛方法時(shí)必須顯性地使用override關(guān)健字。假設(shè)一個(gè)Window類是由A公司編寫的,ListBox和RadioButton類是由B公司的和編程人員在購(gòu)買的A公司編寫的Window類的基礎(chǔ)上編寫的,B公司的編程人員對(duì)包括Window類未來的變化情況在內(nèi)的設(shè)計(jì)知之甚少。
如果B公司的一位編程人員要在ListBox上添加一個(gè)Sort方法:
public class ListBox : Window
{
public virtual void Sort() {"}
}
在A公司發(fā)布新版的Window類之前,這不會(huì)有任何問題。如果A公司的編程人員也在Window類中添加了一個(gè)Sort方法。
public class Window
{
// "
public virtual void Sort() {"}
}
在C++中,Windows類中的Sort方法將成為L(zhǎng)istBox類中Sort方法的基礎(chǔ)方法,在希望調(diào)用Windows類中的Sort方法時(shí),ListBox類中的Sort方法就會(huì)被調(diào)用。在C#中,虛擬函數(shù)總是被認(rèn)為是虛擬調(diào)度的根。也就是說,一旦C#發(fā)現(xiàn)一個(gè)虛擬的方法,就不會(huì)再在虛擬鏈中查找其他虛擬方法。如果ListBox再次被編譯,編譯器就會(huì)生成一個(gè)警告信息:
"\class1.cs(54,24): warning CS0114: ‘ListBox.Sort()‘ hides
inherited member ‘Window.Sort()‘.
要使當(dāng)前的成員覆蓋原來的方法,就需要添加override關(guān)健字,或者添加new關(guān)健字。
要消除警告信息,編程人員必須搞清楚他想干什么??梢栽贚istBox類中的Sort方法前添加new,表明它不應(yīng)該覆蓋Window中的虛方法:
public class ListBox : Window
{
public new virtual void Sort() {"}
這樣就可以清除警告信息。如果編程人員確實(shí)希望覆蓋掉Window中的方法,就必須使用override關(guān)健字來顯性地表明其意圖。
錯(cuò)誤7:類成員變量的初始化
C#中的初始化與C++中不同。假設(shè)有一個(gè)帶有private性質(zhì)的成員變量age的Person類,Employee是由繼承Person類而生成的,它有一個(gè)private性質(zhì)的salaryLevel成員變量。在C++中,我們可以在Employee的構(gòu)造器的初始化部分初始化salaryLevel,如下面的代碼所示:
Employee::Employee(int theAge, int theSalaryLevel):
Person(theAge) // 初始化基礎(chǔ)類
salaryLevel(theSalaryLevel) // 初始化成員變量
{
// 構(gòu)造器的代碼
}
這種方法在C#中是非法的。盡管仍然可以初始化基礎(chǔ)類,但象上面的代碼那樣對(duì)成員變量初始化就會(huì)引起編譯錯(cuò)誤。在C#中,我們可以在定義成員變量時(shí)的同時(shí)對(duì)它進(jìn)行初始化:
Class Employee : public Person
{
// 成員變量的定義
private salaryLevel = 3; // 初始化
}
注意:必須明確地定義每個(gè)變量的訪問權(quán)限。
錯(cuò)誤8:布爾型變量與整型變量是兩回事兒
if( someFuncWhichReturnsAValue() )
在C#中,布爾型變量與整型變量并不相同,因此下面的代碼是不正確的:
if( someFuncWhichReturnsAValue() )
if someFuncWhichReturnsAValue返回零表示false,否則表示true的想法已經(jīng)行不通了。這樣的好處是原來存在的將賦值運(yùn)算與相等相混淆的錯(cuò)誤就不會(huì)再犯了。因此下面的代碼:
if ( x = 5 )
在編譯時(shí)就會(huì)出錯(cuò),因?yàn)閤=5只是把5賦給了X,而不是一個(gè)布爾值。
錯(cuò)誤9:switch語(yǔ)句中會(huì)有些語(yǔ)句執(zhí)行不到
在C#中,如果一個(gè)switch語(yǔ)句執(zhí)行了一些操作,則程序就可能不能執(zhí)行到下一個(gè)語(yǔ)句。因此,盡管下面的代碼在C++中是合法的,但在C#中卻不合法:
switch (i)
{
case 4:
CallFuncOne();
case 5: // 錯(cuò)誤,不會(huì)執(zhí)行到這里
CallSomeFunc();
}
要實(shí)現(xiàn)上面代碼的目的,需要使用一個(gè)goto語(yǔ)句:
switch (i)
{
case 4:
CallFuncOne();
goto case 5;
case 5:
CallSomeFunc();
}
如果case語(yǔ)句不執(zhí)行任何代碼,則所有的語(yǔ)句都會(huì)被執(zhí)行。如下面的代碼:
switch (i)
{
case 4: // 能執(zhí)行到
case 5: // 能執(zhí)行到
case 6:
CallSomeFunc();
}
錯(cuò)誤10:C#中的變量要求明確地賦值
在C#中,所有的變量在使用前都必須被賦值。因此,可以在定義變量時(shí)不對(duì)它進(jìn)行初始化,如果在把它傳遞給一個(gè)方法前,必須被賦值。
如果只是通過索引向方法傳遞一個(gè)變量,并且該變量是方法的輸出變量,這是就會(huì)帶來問題。例如,假設(shè)有一個(gè)方法,它返回當(dāng)前時(shí)間的小時(shí)、分、秒,如果象下面這樣編寫代碼:
int theHour;
int theMinute;
int theSecond;
timeObject.GetTime( ref theHour, ref theMinute, ref theSecond)
如果在使用theHour、theMinute和theSecond這三個(gè)變量之前沒有對(duì)它們進(jìn)行初始化,就會(huì)產(chǎn)生一個(gè)編譯錯(cuò)誤:
Use of unassigned local variable ‘theHour‘
Use of unassigned local variable ‘theMinute‘
Use of unassigned local variable ‘theSecond‘
我們可以通過將這些變量初始化為0或其他對(duì)方法的返回值沒有影響的值,以解決編譯器的這個(gè)小問題:
int theHour = 0;
int theMinute = 0;
int theSecond = 0;
timeObject.GetTime( ref theHour, ref theMinute, ref theSecond)
這樣就有些太麻煩了,這些變量傳遞給GetTime方法,然后被改變而已。為了解決這一問題,C#專門針對(duì)這一情況提供了out參數(shù)修飾符,它可以使一個(gè)參數(shù)無需初始化就可以被引用。例如,GetTime中的參數(shù)對(duì)它本身沒有一點(diǎn)意義,它們只是為了表達(dá)該方法的輸出。在方法中返回之前,Out參數(shù)中必須被指定一個(gè)值。下面是經(jīng)過修改后的GetTime方法:
public void GetTime(out int h, out int m, out int s)
{
h = Hour;
m = Minute;
s = Second;
}
下面是新的GetTime方法的調(diào)用方法:
timeObject.GetTime( out theHour, out theMinute, out theSecond);