[轉貼] C++編程人員容易犯的10個C#錯誤

我又看見一個獸從海中上來、有十角七頭、在十角上戴著十個冠冕、七頭上有褻瀆的名號。
我所看見的獸、形狀像豹、腳像熊的腳、口像獅子的口.那龍將自己的能力、座位、和大權柄、都給了他...

~聖經 啓示錄第十三

中心應該有部分同仁和我一樣,是從C++轉到C#的吧?剛好看到這篇文章,也順便把自己這段時間的一些心得加上,和大家討論。
雖說原文的標題是十個"錯誤",但其實是太誇張了,不如說是十個注意事項...

由於對岸的許多用語和我們不同,所以我儘可能將之翻成台灣的用語,或是加上英文名詞。因為常看侯捷大哥的書,所以台灣用語會取自他的譯法。其他可能會混淆的部分,我會在另外再加註囉!我的心得用綠色字呦。

~3rd Jason

--------------------------------------------------------------------

簡體原文網頁

我們知道,C#的語法與C++非常相似,實現從C++向C#的轉變,其困難不在於語言本身,而在於熟悉.NET的可管理環境和對.NET Framework 的理解。

儘管C#與C++在語法上的變化是很小的,幾乎不會對我們有什麼影響,但有些變化卻足以使一些粗心的C++編程人員時刻銘記在心。在本篇文章中我們將討論C++編程人員最容易犯的十個錯誤。

錯誤1:沒有明確的結束函式

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

如果使用非可管理性資源,在不使用這些資源後,必須明確地釋放它。對資源的隱性控制是由Finalize函式(也被稱為finalizer)提供的,當物件被銷毀時,它就會被GC程序呼叫收回物件所佔用的資源。

finalizer應該只釋放被銷毀物件佔用的非可管理性資源,而不應牽涉到其他物件。如果在程序中只使用了可管理性資源,那就無需也不應當執行 Finalize函式,只有在非可管理性資源的處理中才會用到Finalize函式。由於finalizer需要佔用一定的資源,因此應當只在需要它的函式中執行finalizer。

直接呼叫一個物件的Finalize函式是絕對不允許的(除非是在子類別別的Finalize中呼叫父類別別的Finalize。),GC程序會自動地呼叫Finalize。

從語法上看,C#中的destructor與C++非常相似,但其實它們是完全不同的。C#中的destructor只是定義Finalize函式的捷徑。

Jason註:其實C#解構式在編譯後,會變成:

c# 原始碼
  1. protected override void Finalize()
  2. {
  3. try
  4. {
  5. ...
  6. }
  7. finally
  8. {
  9. base.Finalize();
  10. }
  11. }

由於 Finalize() 的被呼叫時機無法預測,所以它只能當做釋放資源的第二道防線。
主要還是使用 Dispose。

錯誤2:Finalize 和 Dispose 使用誰?

從上面的論述中我們已經很清楚,直接呼叫finalizer是不允許的,它只能被GC程序呼叫。如果希望儘快地釋放一些不再使用的數量有限的非可管理性資源(如handle),則應該使用IDisposable界面,這一界面有個Dispose函式,它能夠幫你完成這個任務。Dispose是無需等待 Finalize被呼叫而能夠釋放非可管理性資源的函式。

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

c# 原始碼
  1. publicvoidDispose()

  2. {

  3. //完成清理操作

  4. //通知GC不要再呼叫Finalize函式

  5. GC.SuppressFinalize(this);

  6. }

  7. publicoverridevoidFinalize()

  8. {

  9. Dispose();

  10. base.Finalize();

  11. }

對於有些物件,可能呼叫Close函式就更合適,可以通過建立一個private屬性的 Dispose函式和public屬性的Close函式,並讓Close呼叫Dispose來實現對某些物件呼叫Close函式。

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

c# 原始碼
  1. using System.Drawing;

  2. class Tester

  3. {

  4. public static void Main()

  5. {

  6. using(FonttheFont = new Font("Arial",10.0f))

  7. {

  8. //使用theFont物件

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

  10. Font anotherFont = newFont("Courier",12.0f);

  11. using( anotherFont)

  12. {

  13. //使用anotherFont物件

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

  15. }

  16. }

在本例的第一部分中,Font物件是在Using陳述中產生的。當Using語句結束時,系統就會呼叫Dispose,對Font物件進行處理。在本例的第二部分,Font物件是在Using語句外部產生的,在決定使用它時,再將它放在Using語句內,當Using語句結束時,系統就會呼叫 Dispose。

Using語句還能防止其他意外的發生,保證系統一定會呼叫Dispose。

Jason註:和錯誤1相同,編譯器會在編譯時把using區間用 try / finally 包起來。
有提供Close函式的物件,如資料庫連線、檔案...等,可以從外部直接呼叫 Close 來釋放資源。


錯誤3:C#中的值型別變數(Value Type Variable)和參考型別變數(Reference Type Variable)是有區別的

與C++一樣,C#也是一種 strong-type (嚴格型別)語言。C#中的資料型別被分為了二大類:C#語言本身所固有的資料型別和使用者定義資料型別,這一點也與C++相似。

此外,C#語言還把變數分為值型別和參考型別。除非是被包含在一個參考型別中,否則值類別型變數的值會保留在堆疊(stack)中,這一點與C++中的"變數"非常相似。參考型別的變數也是堆疊的一種,它的值是堆積(heap)中物件的地址,與C++中的"指標"非常地相似。值型別變數變數的值被直接傳遞給函式,參考型別變數在被作為參數傳遞給函式時,傳遞的是索引。

類別和界面可以建立參考型別,但需要指出的是,結構 (struct) 是C#的一種內建資料型別,同時也是一種值型別變數。

Jason: 也就是C#中 class 和 struct 的區別,比起C++來說,又多了這一樣


錯誤4:注意隱式(implicit)的資料型別轉換

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

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

如果要將一個boxing的物件轉換回原來的值型變數,必須"明確"地對它進行unboxing。Unboxing需要二個步驟:首先對物件實體進行檢查,確保它們是由值型別變數被包裝成的;第二步將實體中的值拷貝到值型別變數中。為了確保unboxing成功,被unboxing的物件必須是通過boxing一個值型變數的值生成的物件的參考。

c# 原始碼
  1. using System;

  2. public class UnboxingTest

  3. {

  4. public static void Main()

  5. {

  6. int i=123;

  7. //boxing

  8. object o = i;

  9. //unboxing(必須明確指示)

  10. int j = (int)o;

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

  12. }

  13. }

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

Jason註:其實以C#這樣簡便的寫法,boxing和unboxing對程式員根本沒有什麼負擔。這邊的重點在於隱式轉換,除了boxing下的效能影響外,是程式員常會忽略掉的地方...(呃..常用C++的人應該知道我在說什麼...orz)

錯誤5:結構(struct)與類別(class)是有區別的

C++中的結構與類別差不多,唯一的區別是,在預設狀態下,結構的存取權限是public,其繼承權限也是public。一些C++編程人員將結構作為資料物件,但這只是一個約定而非是必須這樣的。

在C#中,結構只是一個使用者定義的資料型別,並不能取代類別。儘管結構也支援屬性、函式、域和運算子,但不支援繼承和destructor。

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

錯誤6:虛擬函式必須被明確地覆寫 (override)

在C#語言中,編程人員在覆寫一個虛擬函式時必須明確地使用 override 關鍵字。假設一個Window類別是由A公司編寫的,ListBox和 RadioButton類別是由B公司的和編程人員在購買的A公司編寫的Window類別的基礎上編寫的,B公司的編程人員對包括Window類別未來的變化情況在內的設計知之甚少。

如果B公司的一位編程人員要在ListBox上添加一個Sort函式:

c# 原始碼
  1. public class ListBox:Window

  2. {

  3. public virtual void Sort()
  4. {
  5. ...
  6. }

  7. }

在A公司發佈新版的Window類別之前,這不會有任何問題。如果A公司的編程人員也在Window類別中添加了一個Sort函式。

c# 原始碼
  1. public class Window

  2. {

  3. public virtual void Sort()
  4. {
  5. ...
  6. }
  7. }

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

"\class1.cs(54,24):warningCS0114:『ListBox.Sort()『hides

inherited member『Window.Sort()『.

要使當前的成員覆蓋原來的函式,就需要添加 override 關健字,或者添加 new 關健字。

要消除警告信息,編程人員必須搞清楚他想幹什麼。可以在ListBox類別中的Sort函式前添加 new,表明它不應該覆蓋Window中的虛擬函式:

c# 原始碼
  1. public class ListBox:Window
  2. {
  3. public new virtual void Sort()
  4. { ... }
  5. }

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

Jason註:其實簡單來講,這兩個關鍵字就是讓程設人員更明確地表明企圖。override 表明要覆寫該函式,new 表明要重新定義(也就是C++裏面所說的hide...)。new 關鍵字是可以不加的,不加的話編譯器只是會給予警告,仍是可以通過編譯。用意也只是在提醒開發者該函式不會被呼叫到。

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

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

cpp 原始碼
  1. Employee::Employee(inttheAge,inttheSalaryLevel): Person(theAge)//初始化父類別成員
  2. , salaryLevel(theSalaryLevel)//初始化成員變數
  3. {
  4. //建構子的程式碼
  5. }

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

c# 原始碼
  1. ClassEmployee: publicPerson

  2. {

  3. //成員變數的定義

  4. privatesalaryLevel=3;//初始化

  5. }

注意:必須明確地定義每個變數的存取權限。

Jason註:用C++時不少人會使用這樣的方式來初始化,稱為member initialization list。

優點是節省了一次的函式呼叫(只要呼叫 copy constructor 即可)。

錯誤8:布林(boolean)變數與整數是兩回事

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

if(someFuncWhichReturnsAValue())

傳回零表示 false,否則表示 true 的想法已經行不通了。

這樣的好處是原來存在的將 "賦值運算" 與 "相等" 相混淆的錯誤就不會再犯了。因此下面的程式碼:

if(x=5)

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

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

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

c# 原始碼
  1. switch(i)

  2. {

  3. case4:

  4. CallFuncOne();

  5. case5://錯誤,不會執行到這裡

  6. CallSomeFunc();

  7. }

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

c# 原始碼
  1. switch(i)

  2. {

  3. case4:

  4. CallFuncOne();

  5. goto case5;

  6. case5:

  7. CallSomeFunc();

  8. }

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

c# 原始碼

  1. switch(i)

  2. {

  3. case4://能執行到

  4. case5://能執行到

  5. case6:

  6. CallSomeFunc();

  7. }

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

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

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

c# 原始碼
  1. int theHour;

  2. int theMinute;

  3. int theSecond;

  4. 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或其他對函式的返回值沒有影響的值,以解決編譯器的這個小問題:

c# 原始碼
  1. int theHour=0;

  2. int theMinute=0;

  3. int theSecond=0;

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

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

c# 原始碼
  1. public void GetTime(out int h,out int m,out int s)

  2. {

  3. h=Hour;

  4. m=Minute;

  5. s=Second;

  6. }

  7. // 下面是新的GetTime函式的呼叫函式:

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

Jason註:ref 關鍵字是表示傳址(傳參考值),和out的差異基本上只在本節所說的輸出,以及out必須在傳回前被賦值。C#的函式共有四種參數,傳值、ref、out和params。

Share on Google Plus

About 賴大鐵


    Blogger Comment
    Facebook Comment

1 意見:

  1. 這篇是貼在中心的文章,在這邊是測試一下 XD
    可惜blogger這邊的編輯功能比較不足,沒有貼程式碼的功能,字型也不太夠...再研究看看囉

    回覆刪除

Related Posts Plugin for WordPress, Blogger...