C++編程人員容易犯的10個C#錯誤
2024-07-21 02:18:38
供稿:網(wǎng)友
我們知道, c#的語法與c++非常相似,實現(xiàn)從c++向c#的轉(zhuǎn)變,其困難不在于語言本身,而在于熟悉.net的可管理環(huán)境和對.net框架的理解。
盡管c#與c++在語法上的變化是很小的,幾乎不會對我們有什么影響,但有些變化卻足以使一些粗心的c++編程人員時刻銘記在心。在本篇文章中我們將討論c++編程人員最容易犯的十個錯誤。
陷阱1: 沒有明確的結(jié)束方法
幾乎可以完全肯定地說,對于大多數(shù)c++編程人員而言,c#與c++最大的不同之處就在于碎片收集。這也意味著編程人員再也無需擔(dān)心內(nèi)存泄露和確保刪除所有沒有用的指針。但我們再也無法精確地控制殺死無用的對象這個過程。事實上,在c#中沒有明確的destructor。
如果使用非可管理性資源,在不使用這些資源后,必須明確地釋放它。對資源的隱性控制是由finalize方法(也被稱為finalizer)提供的,當(dāng)對象被銷毀時,它就會被碎片收集程序調(diào)用收回對象所占用的資源。
finalizer應(yīng)該只釋放被銷毀對象占用的非可管理性資源,而不應(yīng)牽涉到其他對象。如果在程序中只使用了可管理性資源,那就無需也不應(yīng)當(dāng)執(zhí)行finalize方法,只有在非可管理性資源的處理中才會用到finalize方法。由于finalizer需要占用一定的資源,因此應(yīng)當(dāng)只在需要它的方法中執(zhí)行finalizer。
直接調(diào)用一個對象的finalize方法是絕對不允許的(除非是在子類的finalize中調(diào)用基礎(chǔ)類的finalize。),碎片收集程序會自動地調(diào)用finalize。
從語法上看,c#中的destructor與c++非常相似,但其實它們是完全不同的。c#中的destructor只是定義finalize方法的捷徑。因此,下面的二段代碼是有區(qū)別的:
~myclass()
{
// 需要完成的任務(wù)
}
myclass.finalize()
{
// 需要完成的任務(wù)
base.finalize();
}
錯誤2:finalize和dispose使用誰?
從上面的論述中我們已經(jīng)很清楚,顯性地調(diào)用finalizer是不允許的,它只能被碎片收集程序調(diào)用。如果希望盡快地釋放一些不再使用的數(shù)量有限的非可管理性資源(如文件句柄),則應(yīng)該使用idisposable界面,這一界面有個dispose方法,它能夠幫你完成這個任務(wù)。dispose是無需等待finalize被調(diào)用而能夠釋放非可管理性資源的方法。
如果已經(jīng)使用了dispose方法,則應(yīng)當(dāng)阻止碎片收集程序再對相應(yīng)的對象執(zhí)行finalize方法。為此,需要調(diào)用靜態(tài)方法gc.suppressfinalize,并將相應(yīng)對象的指針傳遞給它作為參數(shù),finalize方法就能調(diào)用dispose方法了。據(jù)此,我們能夠得到如下的代碼:
public void dispose()
{
// 完成清理操作
// 通知gc不要再調(diào)用finalize方法
gc.suppressfinalize(this);
}
public override void finalize()
{
dispose();
base.finalize();
}
對于有些對象,可能調(diào)用close方法就更合適(例如,對于文件對象調(diào)用close就比dispose更合適),可以通過創(chuàng)建一個private屬性的dispose方法和public屬性的close方法,并讓close調(diào)用dispose來實現(xiàn)對某些對象調(diào)用close方法。
由于不能確定一定會調(diào)用dispose,而且finalizer的執(zhí)行也是不確定的(我們無法控制gc會在何時運行),c#提供了一個using語句來保證dispose方法會在盡可能早的時間被調(diào)用。一般的方法是定義使用哪個對象,然后用括號為這些對象指定一個活動的范圍,當(dāng)遇到最內(nèi)層的括號時,dispose方法就會被自動調(diào)用,對該對象進行處理。
using system.drawing;
class tester
{
public static void main()
{
using (font thefont = new font("arial", 10.0f))
{
//使用thefont對象
} // 編譯器將調(diào)用dispose處理thefont對象
font anotherfont = new font("courier",12.0f);
using (anotherfont)
{
// 使用anotherfont對象
} // 編譯器將調(diào)用dispose處理anotherfont對象
}
}
在本例的第一部分中,font對象是在using語句中創(chuàng)建的。當(dāng)using語句結(jié)束時,系統(tǒng)就會調(diào)用dispose,對font對象進行處理。在本例的第二部分,font對象是在using語句外部創(chuàng)建的,在決定使用它時,再將它放在using語句內(nèi),當(dāng)using語句結(jié)束時,系統(tǒng)就會調(diào)用dispose。
using語句還能防止其他意外的發(fā)生,保證系統(tǒng)一定會調(diào)用dispose。
錯誤3:c#中的值型變量和引用型變量是有區(qū)別的
與c++一樣,c#也是一種強類型編程語言。c#中的數(shù)據(jù)類型被分為了二大類:c#語言本身所固有的數(shù)據(jù)類型和用戶自定義數(shù)據(jù)類型,這一點也與c++相似。
此外,c#語言還把變量分為值類型和引用類型。除非是被包含在一個引用類型中,值類型變量的值保留在棧中,這一點與c++中的變量非常相似。引用類型的變量也是棧的一種,它的值是堆中對象的地址,與c++中的指針非常地相似。值類型變量的值被直接傳遞給方法,引用型變量在被作為參數(shù)傳遞給方法時,傳遞的是索引。
類和界面可以創(chuàng)建引用類變量,但需要指出的是,結(jié)構(gòu)數(shù)據(jù)類型是c#的一種內(nèi)置數(shù)據(jù)類型,同時也是一種值型的數(shù)據(jù)類型。
錯誤4:注意隱性的數(shù)據(jù)類型轉(zhuǎn)換
boxing和unboxing是使值型數(shù)據(jù)類型被當(dāng)作索引型數(shù)據(jù)類型使用的二個過程。值型變量可以被包裝進一個對象中,然后再被解包回值型變量。包括內(nèi)置數(shù)據(jù)類型在內(nèi)的所有c#中的數(shù)據(jù)類型都可以被隱性地轉(zhuǎn)化為一個對象。包裝一個值型變量就會生成一個對象的實例,然后將變量拷貝到實例中。
boxing是隱性的,如果在需要索引型數(shù)據(jù)類型的地方使用了值型數(shù)據(jù)類型的變量,值型變量就會隱性地轉(zhuǎn)化為索引型數(shù)據(jù)類型的變量。boxing會影響代碼執(zhí)行的性能,因此應(yīng)當(dāng)盡量避免,尤其是在數(shù)據(jù)量較大的時候。
如果要將一個打包的對象轉(zhuǎn)換回原來的值型變量,必須顯性地對它進行解包。解包需要二個步驟:首先對對象實例進行檢查,確保它們是由值型的變量被包裝成的;第二步將實例中的值拷貝到值型變量中。為了確保解包成功,被解包的對象必須是通過打包一個值型變量的值生成的對象的索引。
using system;
public class unboxingtest
{
public static void main()
{
int i = 123;
//打包
object o = i;
// 解包(必須是顯性的)
int j = (int) o;
console.writeline("j: {0}", j);
}
}
如果被解包的對象是無效的,或是一個不同數(shù)據(jù)類型對象的索引,就會產(chǎn)生invalidcastexception異外。
錯誤5:結(jié)構(gòu)與對象是有區(qū)別的
c++中的結(jié)構(gòu)與類差不多,唯一的區(qū)別是,在缺省狀態(tài)下,結(jié)構(gòu)的訪問權(quán)限是public,其繼承權(quán)限也是public。一些c++編程人員將結(jié)構(gòu)作為數(shù)據(jù)對象,但這只是一個約定而非是必須這樣的。
在c#中,結(jié)構(gòu)只是一個用戶自定義的數(shù)據(jù)類型,并不能取代類。盡管結(jié)構(gòu)也支持屬性、方法、域和操作符,但不支持繼承和destructor。
更重要的是,類是一種索引型數(shù)據(jù)類型,結(jié)構(gòu)是值型數(shù)據(jù)類型。因此,結(jié)構(gòu)在表達無需索引操作的對象方面更有用。結(jié)構(gòu)在數(shù)組操作方面的效率更高,而在集合的操作方面則效率較低。集合需要索引,結(jié)構(gòu)必須打包才適合在集合的操作中使用,類在較大規(guī)模的集合操作中的效率更高。
錯誤6:虛方法必須被明確地覆蓋
在c#語言中,編程人員在覆蓋一個虛方法時必須顯性地使用override關(guān)健字。假設(shè)一個window類是由a公司編寫的,listbox和radiobutton類是由b公司的和編程人員在購買的a公司編寫的window類的基礎(chǔ)上編寫的,b公司的編程人員對包括window類未來的變化情況在內(nèi)的設(shè)計知之甚少。
如果b公司的一位編程人員要在listbox上添加一個sort方法:
public class listbox : window
{
public virtual void sort() {"}
}
在a公司發(fā)布新版的window類之前,這不會有任何問題。如果a公司的編程人員也在window類中添加了一個sort方法。
public class window
{
// "
public virtual void sort() {"}
}
在c++中,windows類中的sort方法將成為listbox類中sort方法的基礎(chǔ)方法,在希望調(diào)用windows類中的sort方法時,listbox類中的sort方法就會被調(diào)用。在c#中,虛擬函數(shù)總是被認(rèn)為是虛擬調(diào)度的根。也就是說,一旦c#發(fā)現(xiàn)一個虛擬的方法,就不會再在虛擬鏈中查找其他虛擬方法。如果listbox再次被編譯,編譯器就會生成一個警告信息:
"/class1.cs(54,24): warning cs0114: 'listbox.sort()' hides
inherited member 'window.sort()'.
要使當(dāng)前的成員覆蓋原來的方法,就需要添加override關(guān)健字,或者添加new關(guān)健字。
要消除警告信息,編程人員必須搞清楚他想干什么。可以在listbox類中的sort方法前添加new,表明它不應(yīng)該覆蓋window中的虛方法:
public class listbox : window
{
public new virtual void sort() {"}
這樣就可以清除警告信息。如果編程人員確實希望覆蓋掉window中的方法,就必須使用override關(guān)健字來顯性地表明其意圖。
錯誤7:類成員變量的初始化
c#中的初始化與c++中不同。假設(shè)有一個帶有private性質(zhì)的成員變量age的person類,employee是由繼承person類而生成的,它有一個private性質(zhì)的salarylevel成員變量。在c++中,我們可以在employee的構(gòu)造器的初始化部分初始化salarylevel,如下面的代碼所示:
employee::employee(int theage, int thesalarylevel):
person(theage) // 初始化基礎(chǔ)類
salarylevel(thesalarylevel) // 初始化成員變量
{
// 構(gòu)造器的代碼
}
這種方法在c#中是非法的。盡管仍然可以初始化基礎(chǔ)類,但象上面的代碼那樣對成員變量初始化就會引起編譯錯誤。在c#中,我們可以在定義成員變量時的同時對它進行初始化:
class employee : public person
{
// 成員變量的定義
private salarylevel = 3; // 初始化
}
注意:必須明確地定義每個變量的訪問權(quán)限。
錯誤8:布爾型變量與整型變量是兩回事兒
if( somefuncwhichreturnsavalue() )
在c#中,布爾型變量與整型變量并不相同,因此下面的代碼是不正確的:
if( somefuncwhichreturnsavalue() )
if somefuncwhichreturnsavalue返回零表示false,否則表示true的想法已經(jīng)行不通了。這樣的好處是原來存在的將賦值運算與相等相混淆的錯誤就不會再犯了。因此下面的代碼:
if ( x = 5 )
在編譯時就會出錯,因為x=5只是把5賦給了x,而不是一個布爾值。
錯誤9:switch語句中會有些語句執(zhí)行不到
在c#中,如果一個switch語句執(zhí)行了一些操作,則程序就可能不能執(zhí)行到下一個語句。因此,盡管下面的代碼在c++中是合法的,但在c#中卻不合法:
switch (i)
{
case 4:
callfuncone();
case 5: // 錯誤,不會執(zhí)行到這里
callsomefunc();
}
要實現(xiàn)上面代碼的目的,需要使用一個goto語句:
switch (i)
{
case 4:
callfuncone();
goto case 5;
case 5:
callsomefunc();
}
如果case語句不執(zhí)行任何代碼,則所有的語句都會被執(zhí)行。如下面的代碼:
switch (i)
{
case 4: // 能執(zhí)行到
case 5: // 能執(zhí)行到
case 6:
callsomefunc();
}
錯誤10:c#中的變量要求明確地賦值
在c#中,所有的變量在使用前都必須被賦值。因此,可以在定義變量時不對它進行初始化,如果在把它傳遞給一個方法前,必須被賦值。
如果只是通過索引向方法傳遞一個變量,并且該變量是方法的輸出變量,這是就會帶來問題。例如,假設(shè)有一個方法,它返回當(dāng)前時間的小時、分、秒,如果象下面這樣編寫代碼:
int thehour;
int theminute;
int thesecond;
timeobject.gettime( ref thehour, ref theminute, ref thesecond)
如果在使用thehour、theminute和thesecond這三個變量之前沒有對它們進行初始化,就會產(chǎn)生一個編譯錯誤:
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參數(shù)修飾符,它可以使一個參數(shù)無需初始化就可以被引用。例如,gettime中的參數(shù)對它本身沒有一點意義,它們只是為了表達該方法的輸出。在方法中返回之前,out參數(shù)中必須被指定一個值。下面是經(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);