Visual Basic/有效程式設計
在任何計算機語言中有效程式設計時,無論是 VB 還是 C++,你的風格都應該保持一致,應該有條理,並且應該儘可能地提高執行速度和資源利用率(如記憶體或網路流量)。使用成熟的程式設計技術,可以將錯誤降到最低並更容易識別,這將使程式設計師的工作更輕鬆,更愉快。
編寫可靠程式有很多不同的方面。對於一個由作者互動式使用且僅由作者使用的簡短程式,為了快速獲得答案,打破所有規則是合理的。但是,如果該小程式發展成大型程式,你最終會希望自己一開始就走上了正確的道路。每種程式語言都有自己的優缺點,一種在一種語言中幫助編寫良好程式的技術在另一種語言中可能是沒有必要的、不可能的或適得其反的;這裡介紹的內容特別適用於 VB6,但其中大部分是標準內容,適用於或在 Pascal、C、Java 和其他類似的命令式語言中強制執行。
一般指南
[edit | edit source]這些建議將在下面更詳細地描述。這些都不能稱為規則,有些是有爭議的,你必須根據成本和效益自己做出決定。
- 編寫註釋以解釋你為什麼要這樣做。如果程式碼或正在解決的問題特別複雜,你應該也解釋為什麼選擇你選擇的這種方法而不是其他更明顯的方法。
- 縮排你的程式碼。這可以使其他人更容易閱讀程式碼,並可以輕鬆地發現語句是否未正確關閉。這在多重巢狀語句中尤其重要。
- 宣告所有變數,透過在每個程式碼模組的頂部放置Option Explicit來強制執行此操作。
- 使用有意義的變數和子例程名稱。變數 FileHandle 對我們人類來說比 X 更有意義。還要避免縮寫名稱的趨勢,因為這也會使程式碼難以閱讀。不要使用 FilHan,而 FileHandle 更清晰。
- 在函式和子例程的引數列表中,將所有引數宣告為ByRef。這會強制編譯器檢查你傳入的變數的資料型別。
- 在儘可能小的範圍內宣告變數、子例程和函式:優先使用 Private 而不是 Friend,Friend 而不是 Public。
- 在 .bas 模組中,儘可能少地宣告為 Public 的變數;這樣的變數對整個元件或程式是公開的。
- 將相關的函式和子例程分組到一個模組中,為不相關的例程建立一個新模組。
- 如果一組變數和過程密切相關,請考慮建立一個類來封裝它們。
- 在程式碼中包含斷言以確保例程被提供正確的資料並返回正確的資料。
- 編寫和執行測試。
- 先讓程式工作,然後讓程式快速工作。
- 如果一個變數可以儲存有限範圍的離散值,這些值在編譯時是已知的,請使用列舉型別。
- 將大型程式分解成單獨的元件(DLL 或類庫),以便你僅對需要使用它們的程式碼部分降低資料和例程的可見性。
- 使用簡單的字首符號來顯示變數的型別和例程的作用域。
宣告變數
[edit | edit source]在本書的前面,你可能被教導使用簡單的 Dim 語句宣告變數,或者根本不宣告。在不同級別宣告變數是一項關鍵技能。將你的程式視為三個分支:模組(對所有窗體開放)、單個窗體和子程式本身。如果在模組中宣告一個變數,該變數將在所有窗體中保留其值。Dim 語句將起作用,但按照慣例使用“Public”代替。例如
Public X as Integer
在窗體程式碼的頂部宣告某些內容將使其對該窗體私有,因此,如果在一個窗體中 X=10,而在另一個窗體中 X=20,它們不會相互干擾。如果變數被宣告為 public,那麼它們之間會相互影響。要在一個窗體中宣告某些內容,按照慣例使用“Private”。
Private X as Integer
最後是子程式。僅對子程式設定變數的維度非常有效,這樣你就可以在所有子程式中使用預設變數(例如 sum 用於求和),而無需擔心某個值因為程式碼的其他部分而改變。但是,有一個轉折。Dim,你習慣的,在子程式完成後不會保留變數的值。因此,在重新執行子程式後,子程式中的所有區域性變數都將被重置。要解決此問題,可以使用“Static”。
Static X as Integer
你們中的一些人可能想走捷徑,對所有東西都使用 Public。但是,最好在儘可能小的級別上宣告。通常,引數是將變數從一個子程式傳送到另一個子程式的最佳方法。這是因為引數使跟蹤變數在哪裡被更改(以防出現邏輯錯誤)變得容易得多,並且幾乎限制了錯誤程式碼部分可以造成的損害。宣告變數在使用預設變數時同樣有用。常見的預設變數是
I for loops J for loops Sum(self explanatory) X for anything
因此,與其建立變數 I 和 II 或 Sum1、Sum2,你可以看到為什麼將變數保持本地是一個有用的技能。
註釋
[edit | edit source]我認識的每個程式設計師都不喜歡寫註釋。我不是說那些只會寫一些腦殘的 Visual Basic Script 來刪除可憐的祖母電腦中所有檔案的文盲指令碼小子,我說的是他們以及所有那些擁有多個博士學位和榮譽博士學位的人。
所以,如果你難以說服自己註釋是一個好主意,那麼你並不孤單。
不幸的是,這不是你我與他們一樣好的情況,而是他們與我們一樣糟糕的情況。良好的註釋對程式的永續性至關重要;如果維護程式設計師無法理解你的程式碼應該如何工作,他可能不得不重寫它。如果他這樣做,他將不得不編寫更多註釋和更多測試。他幾乎肯定會引入更多錯誤,而這將是你的錯,因為你沒有禮貌地解釋你的程式為什麼按這種方式編寫。維護程式設計師通常不屬於原始團隊,因此他們沒有共同的背景來幫助他們理解,他們只有錯誤報告、程式碼和截止日期。如果你不寫註釋,你可以肯定沒有人會在以後添加註釋。
註釋需要與程式碼本身一樣仔細和認真地編寫。僅僅重複程式碼內容的隨意編寫的註釋是在浪費時間,最好什麼也不說。對於與程式碼相矛盾的註釋也是如此;讀者應該相信什麼,程式碼還是註釋。如果註釋與程式碼相矛盾,有人可能會“修復”程式碼,結果卻發現實際上是註釋出了問題。
以下是一些示例註釋
Dim cbMenuCommandBar As Office.CommandBarButton 'command bar object
這來自我自己的一個程式,我透過對別人提供的模板進行駭客攻擊而建立了它。他為什麼添加註釋我不得而知,它對程式碼沒有任何幫助,程式碼中本來就兩次出現了 CommandBar 這個詞!
這是來自類似程式的另一個例子
Public WithEvents MenuHandler As CommandBarEvents 'command bar event handler
同樣程度的無意義。這兩個例子都來自我每天使用的程式。
來自同一程式的另一個例子,它展示了一個好的註釋和一個毫無意義的註釋
Public Sub Remove(vntIndexKey As Variant)
'used when removing an element from the collection
'vntIndexKey contains either the Index or Key, which is why
'it is declared as a Variant
'Syntax: x.Remove(xyz)
mCol.Remove vntIndexKey
End Sub
第一行註釋只是重複了名稱中包含的資訊,最後一行註釋只告訴了我們從宣告中可以輕鬆推斷出來的事情。中間兩行說了一些有用的東西,它們解釋了Variant資料型別令人費解的使用。我的建議是刪除第一行和最後一行註釋,不是因為它們不正確,而是因為它們毫無意義,並且會使真正重要的註釋難以看到。
總結:好的註釋解釋為什麼而不是什麼。它們告訴讀者程式碼不能告訴他們的東西。
你可以透過閱讀程式碼來了解正在發生的事情,但通常很難或不可能知道為什麼程式碼這樣編寫。
解釋程式碼的註釋確實有其位置。如果演算法很複雜或很精巧,你可能需要用簡單的語言對其進行概述。例如,實現方程求解器的例程需要附帶對所用數學方法的描述,並附帶教科書的參考文獻。每一行程式碼都可能非常清楚,但總體計劃可能仍然不清楚;用簡單的語言進行概述可以使其變得清晰。
如果你使用了你知道很少使用的 VB 功能,你可能需要指出它為什麼有效,以防止善意的維護程式設計師將其清理掉。
如果你的程式碼正在解決一個複雜的問題或為了速度而進行了高度最佳化,它將比其他程式碼需要更多更好的註釋,但即使是簡單的程式碼也需要註釋來解釋它存在的原因並概述它的功能。通常情況下,在檔案開頭放置一個敘述性內容,而不是在單個程式碼行上放置註釋,會更好。這樣,讀者就可以閱讀摘要,而不是程式碼。
摘要
[edit | edit source]- 註釋應該增加清晰度和意義,
- 保持註釋簡短,除非程式碼的複雜性需要敘述性的描述,
- 在每個檔案的開頭添加註釋,以解釋它存在的原因以及如何使用它,
- 對每個函式、子程式和屬性進行註釋,以解釋任何奇怪之處,例如 Object 或 Variant 資料型別的使用。
- 如果一個函式有副作用,解釋它們是什麼,
- 如果例程只適用於一定範圍的輸入,那麼就說明,並指出如果呼叫者提供了一些意外的內容會發生什麼。
練習
[edit | edit source]- 拿一段別人寫的程式碼,嘗試在不閱讀註釋的情況下理解它是如何工作的。
- 嘗試找到一些不需要任何註釋的程式碼。解釋為什麼它不需要註釋。這樣的程式碼存在嗎?
- 在網路、你自己的程式碼或同事的程式碼中搜索好的註釋的示例。
- 把自己放在一個維護程式設計師的位置,他被叫來修復你自己的程式碼中一個難以處理的錯誤。添加註釋或改寫現有的註釋,使工作更容易。
避免防禦性程式設計,而是快速失敗
[edit | edit source]防禦性程式設計指的是編寫程式碼來嘗試彌補資料中的一些錯誤,編寫程式碼來假設呼叫者可能會提供不符合呼叫者和子程式之間合同的資料,並且子程式必須以某種方式處理它。
通常會看到這樣的屬性
Public Property Let MaxSlots(RHS as Long)
mlMaxSlots = RHS
End Property
Public Property Get MaxSlots() as Long
if mlMaxSlots = 0 then
mlMaxSlots = 10
End If
MaxSlots = mlMaxSlots
End Property
這樣編寫的理由可能是,編寫該屬性所在的類的程式設計師擔心使用該屬性的程式碼的作者會忘記正確初始化該物件。因此,他或她提供了一個預設值。
問題是,MaxSlots 屬性的程式設計師無法知道客戶端程式碼需要多少插槽。如果客戶端程式碼沒有設定 MaxSlots,它可能會失敗,或者更糟的是,行為異常。最好像這樣編寫程式碼
Public Property Let MaxSlots(RHS as Long)
mlMaxSlots = RHS
End Property
Public Property Get MaxSlots() as Long
if mlMaxSlots = 0 then
Err.Raise ErrorUninitialized, "MaxSlots.Get", "Property not initialized"
End If
MaxSlots = mlMaxSlots
End Property
現在,當客戶端程式碼在呼叫 MaxSlots Let 之前呼叫 MaxSlots Get 時,會引發一個錯誤。現在,解決問題或傳遞錯誤的責任就落到了客戶端程式碼的肩上。無論如何,錯誤會比我們提供預設值時更早被發現。
另一種看待防禦性程式設計和快速失敗之間區別的方式是將快速失敗視為嚴格的合同實施,而將防禦性程式設計視為寬容。是否寬容始終是一個好主意,這在人類社會中經常會爭論不休,但在計算機程式中,它僅僅意味著你不信任程式的所有部分都遵守程式的規範。在這種情況下,你需要修復規範,而不是寬恕違反規範的行為。
練習
[edit | edit source]- 取一個正在執行的非平凡程式,並搜尋程式碼中的防禦性程式設計,
- 改寫程式碼,使其快速失敗,
- 再次執行程式,看看它是否失敗,
- 修復程式的客戶端部分,以消除錯誤。
參考文獻
[edit | edit source]有關此主題的簡明文章,請參閱 http://www.martinfowler.com/ieeeSoftware/failFast.pdf。
斷言和契約設計
[edit | edit source]斷言是一個斷言某些事情為真的語句。在 VB6 中,你可以像這樣新增斷言
Debug.Assert 0 < x
如果語句為真,程式將繼續執行,就好像什麼也沒發生一樣,但如果不是,程式將在該行停止。不幸的是,VB6 有一個特別弱的斷言形式,它們只在程式碼在偵錯程式中執行時執行。這意味著它們在已編譯的程式中沒有任何效果。不要讓這一點阻止你使用它們,畢竟,這在許多常見的程式語言中根本不存在。
如果你真的需要在已編譯的程式中測試斷言,你可以這樣做
If Not(0 < x) Then
Debug.Assert False
Err.Raise ErrorCodes.AssertionFailed
End If
現在,程式將在 IDE 中執行時在失敗處停止,並在編譯後執行時引發錯誤。如果你計劃在多個地方使用這種技術,宣告一個子程式來執行它將是明智之舉,以減少混亂
Public Sub Assert(IsTrue as Boolean)
If Not IsTrue Then
Debug.Assert False
Err.Raise ErrorCodes.AssertionFailed
End If
End Sub
然後,你就可以寫Debug.Assert,而不是寫
Assert 0 < x
斷言可用於實現一種形式的契約設計。在每個例程的開頭新增斷言,斷言有關例程引數的值以及有關任何相關模組或全域性變數的值的一些內容。例如,一個接受一個必須大於零的單個整數引數的例程將具有與上面所示形式相同的斷言。如果它被呼叫時帶有零引數,程式將在帶有斷言的行上停止。你也可以在例程的退出處新增斷言,指定返回值或任何副作用的允許值。
斷言與顯式驗證的不同之處在於,它們不會引發錯誤,也不會允許程式在斷言失敗時採取措施。這並不一定是斷言概念的弱點,它是斷言和驗證檢查使用方式的不同之處。
斷言用於執行以下幾件事
- 它們指定了呼叫程式碼和被呼叫程式碼必須遵守的契約,
- 它們透過在已知有錯誤的最早時間點停止執行來幫助除錯。正確編寫的斷言會在錯誤導致程式崩潰之前很久就捕獲它們。
斷言通常有助於在程式開發過程中發現邏輯錯誤,而驗證通常用於捕獲來自人類或其他不可靠外部來源的不良輸入。程式通常編寫為,輸入驗證失敗不會導致程式失敗,而是將驗證錯誤報告給更高的許可權,並採取糾正措施。如果斷言失敗,通常意味著程式的兩個內部部分未能就雙方都應該遵守的契約條款達成一致。如果呼叫例程傳送了一個負數,而預期的是一個正數,並且該數不是由使用者提供的,那麼無論進行多少驗證,程式都無法恢復,因此引發錯誤毫無意義。在 C 等語言中,斷言失敗會導致程式停止並輸出一個堆疊跟蹤,但 VB 在 IDE 中執行時只會停止。在 VB 中,斷言在已編譯的程式碼中沒有效果。
與全面的測試相結合,斷言是編寫正確程式的絕佳輔助工具。斷言也可以替代某些型別的註釋。描述允許的引數值範圍的註釋最好寫成斷言,因為它們是程式中實際檢查的顯式語句。在 VB 中,你必須在 IDE 中執行程式才能獲得斷言的好處,這是一個小麻煩。
斷言還有助於確保程式在新增新程式碼和修復錯誤時保持正確。假設一個子程式計算由於對流導致的散熱器的等效電導(如果物理知識不熟悉,不要擔心)
Public Function ConvectionConductance(Byval T1 as Double, Byval T2 as Double) as Double
ConvectionConductance = 100 * Area * (T2 - T1)^0.25
End Sub
現在,如果你知道物理知識,電導率總是負數,無論溫度 T1 和 T2 之間的關係如何。但是,此函式假設 T1 始終大於或等於 T2。這種假設對於正在討論的程式來說可能是完全合理的,但它仍然是一個限制,因此應該將其納入此例程與其呼叫者之間的契約中
Public Function ConvectionConductance(Byval T1 as Double, Byval T2 as Double) as Double
Debug.Assert T2 < T1
ConvectionConductance = 100 * Area * (T2 - T1)^0.25
End Sub
對例程結果進行斷言也是一個好主意。
Public Function ConvectionConductance(Byval T1 as Double, Byval T2 as Double) as Double
Debug.Assert T2 <= T1
ConvectionConductance = 100 * Area * (T2 - T1)^0.25
Debug.Assert 0 <= ConvectionConductance
End Sub
在這種特定情況下,斷言結果似乎毫無價值,因為如果前提條件滿足,那麼透過檢查就可以明顯地發現後置條件也必須滿足。在現實生活中,前提條件和後置條件斷言之間的程式碼通常要複雜得多,並且可能包含對不受建立函式的人員控制的函式的許多呼叫。在這種情況下,即使後置條件看起來像是在浪費時間,也應該對其進行指定,因為它可以防止引入錯誤,並向其他程式設計師提供函式應遵守的約定。
測試
[edit | edit source]測試範圍從編寫程式,然後執行並隨意檢視其行為,到首先編寫全套自動化測試,然後編寫程式以使其符合要求。
我們大多數人都在兩者之間工作,通常更接近第一個選擇而不是第二個選擇。測試通常被認為是額外的成本,但就像對物理產品的質量控制系統一樣,所謂的質量成本通常是負面的,因為產品的質量提高了。
你可以使用測試來定義函式或程式的規範,方法是首先編寫測試,這是極限程式設計方法的實踐之一。然後,逐個編寫程式,直到所有測試都透過。對於大多數人來說,這似乎是完美無瑕的建議,並且完全不切實際,但其中一定程度的內容將透過幫助確保元件在整合之前正常工作而帶來豐厚的回報。
測試通常被構建為一個單獨的程式,該程式使用與可交付程式相同的原始碼。只需編寫另一個可以使用可交付程式元件的程式,通常就足以暴露設計中的弱點。
要測試的元件越小,編寫測試就越容易,但是如果你測試非常小的部分,你可能會浪費大量時間編寫對可以透過肉眼輕鬆檢查的事物進行測試。自動化測試最適合那些足夠小以至於可以從真實程式中提取出來而不會造成干擾,但又足夠大以至於具有一些複雜行為的程式部分。很難做到精確,最好做一些測試而不是不做任何測試,經驗會告訴你哪些地方最需要在你的特定程式中付出努力。
你也可以將測試作為程式本身的一部分。例如,每個類都可以有一個測試方法,該方法在測試透過時返回 true,否則返回 false。這樣做的好處是,每次編譯真實程式時,你也會編譯測試,因此任何會導致測試失敗的程式介面更改都可能會及早被捕獲。因為測試位於程式內部,所以它們還可以測試外部測試例程無法訪問的部分。
範圍、可見性和名稱空間
[edit | edit source]| 本節是一個存根。 你可以透過擴充套件它來幫助 Wikibooks。 |
匈牙利命名法
[edit | edit source]匈牙利命名法是許多程式設計師用來表示範圍和型別的變數名字首的名稱。這樣做的目的是透過消除不斷引用變數宣告以確定變數或函式的型別或範圍的需要來提高程式碼的可讀性。
經驗豐富的 Basic 程式設計師已經熟悉這種記法的某種形式很久了,因為 Microsoft Basic 已經使用字尾來表示型別(# 表示 Double,& 表示 Long,等等)。
在任何給定程式中使用的匈牙利命名法的細微差別並不重要。重點是要保持一致,以便其他閱讀你程式碼的程式設計師能夠快速學習約定並遵守它們。出於這個原因,明智的做法是不過度使用這種記法,如果存在太多不同的字首,人們會忘記很少使用的字首的含義,這會違背目的。最好使用一個通用的字首,這樣人們會記住它,而不是使用一大堆人們記不住的模糊的字首。
在編碼標準章節中,對匈牙利命名法進行了更詳細的說明。
記憶體和資源洩漏
[edit | edit source]你可能認為,由於 Visual Basic 沒有本機記憶體分配函式,因此永遠不會發生記憶體洩漏。不幸的是,情況並非如此;Visual Basic 程式可以透過多種方式洩漏記憶體和資源。對於小型實用程式,記憶體洩漏在 Visual Basic 中不是一個嚴重的問題,因為在程式關閉之前,洩漏沒有機會變得足夠大以至於會威脅到其他資源使用者。
但是,在 Visual Basic 中建立伺服器和守護程式是完全合理的,並且此類程式會執行很長時間,因此即使是一個小的洩漏最終也會使作業系統不堪重負。
在 Visual Basic 程式中,記憶體洩漏最常見的原因是迴圈物件引用。當兩個物件相互引用,但不存在對這兩個物件的任何其他引用時,就會出現此問題。
不幸的是,記憶體洩漏的症狀很難在執行的程式中發現,你可能只會在作業系統開始抱怨記憶體不足時才會注意到。
這是一個展示問題的示例問題。
'Class1
Public oOther as Class1
'module1
Public Sub main()
xProblem
End Sub
Private Sub xProblem
Dim oObject1 As Class1
Dim oObject2 As Class1
set oObject1 = New Class1
set oObject2 = New Class1
set oObject1.oOther = oObject2
set oObject2.oOther = oObject1
End Sub
Class1 是一個簡單的類,沒有方法,只有一個公共屬性。對於真實程式來說這不是好的程式設計實踐,但足以說明問題。xProblem 子例程只是建立了兩個 Class1 例項(物件)並將它們連結在一起。請注意,oObject1 和 oObject2 變數對 xProblem 是區域性的。這意味著當子例程完成時,這兩個變數將被丟棄。當 Visual Basic 這樣做時,它會在每個物件中遞減一個計數器,如果此計數器變為零,它會執行 Class_Terminate 方法(如果有),然後恢復物件佔用的記憶體。不幸的是,在這種情況下,引用計數器永遠不會變為零,因為每個物件都引用了另一個物件,因此即使程式中的任何變數都不引用任何物件,它們也永遠不會被丟棄。任何使用簡單引用計數方案來清理物件記憶體的語言都會遇到這個問題。傳統的 C 和 Pascal 不會遇到這個問題,因為它們根本沒有垃圾收集器。Lisp 及其相關語言通常使用標記和清除垃圾收集的某種變體,這會減輕程式設計師的負擔,但會以資源負載不可預測的變化為代價。
為了證明確實存在問題,請在 Class1 中新增 Initialize 和 Terminate 事件處理程式,這些處理程式只會向 立即視窗列印訊息。
Private Sub Class_Initialize()
Debug.Print "Initialize"
End Sub
Private Sub Class_Terminate()
Debug.Print "Terminate"
End Sub
如果 xProblem 例程在沒有洩漏的情況下工作,你會看到數量相等的 Initialize 和 Terminate 訊息。
練習
[edit | edit source]- 修改 xProblem 以確保在退出時兩個物件都被處置(提示:將變數設定為 Nothing 會降低它指向的物件的引用計數)。
避免和處理迴圈引用
[edit | edit source]有許多技術可以用來避免這個問題,從最明顯的技術開始,即根本不允許迴圈引用。
- 在你的程式設計風格指南中禁止迴圈引用,
- 顯式清理所有引用,
- 透過其他慣用法提供功能。
在實際程式中,禁止迴圈引用通常不切實際,因為它意味著放棄使用諸如雙向連結串列之類的有用資料結構。
迴圈引用的一種經典用法是父子關係。在這種關係中,父級是主物件,並擁有子級或子級。父級及其子級共享一些通用資訊,因為資訊對所有這些資訊都是通用的,所以最自然的是讓父級擁有和管理它。當父級超出範圍時,父級和所有子級都應該被處置。不幸的是,這在 Visual Basic 中不會發生,除非你幫助完成此過程,因為為了訪問共享資訊,子級必須引用父級。這是一個迴圈引用。
---------- ---------- | parent | ---> | child | | | <--- | | ---------- ----------
在這種特定情況下,你通常可以透過引入輔助物件來完全避免子級到父級的引用。如果你將父級的屬性分成兩個集合:一個包含僅父級訪問的屬性,另一個包含父級和子級都使用的屬性,那麼你可以透過將所有這些共享屬性放在輔助物件中來避免迴圈性。現在,父級和子級都引用輔助物件,並且任何子級都不需要引用父級。
---------- ----------
| parent | ----> | child |
| | | |
---------- ----------
| |
| |
| ---------- |
-> | common | <-
----------
注意所有箭頭都指向遠離父節點。這意味著當我們的程式碼釋放對父節點的最後一個引用時,引用計數將變為零,並且父節點將被釋放。這反過來又釋放了對子節點的引用。現在,由於父節點和子節點都已消失,因此不再有對公共物件的引用,因此它也將被釋放。所有引用計數和釋放都會自動執行,作為 Visual Basic 內部行為的一部分,無需編寫程式碼來實現它,您只需正確設定結構即可。
請注意,父節點可以擁有任意數量的子節點,例如儲存在物件引用的集合或陣列中。
這種結構的常見用途是,當子節點需要將有關父節點的一些資訊與其自身的一些資訊組合在一起時。例如,如果您正在模擬一些複雜的機器,並希望每個部件都具有一個屬性來顯示其位置。您不希望將其設定為簡單的讀寫屬性,因為這樣您必須在機器整體移動時顯式地更新每個物件上的該屬性。最好將其設定為基於父節點位置和一些維度屬性的計算屬性,這樣當父節點移動時,所有計算屬性將保持正確,而無需執行任何額外的程式碼。另一個應用是返回從根物件到子節點的完全限定路徑的屬性。
以下是一個程式碼示例
'cParent
Private moChildren as Collection
Private moCommon as cCommon
Private Sub Class_Initialize()
Set moChildren = New Collection
Set moCommon = New cCommon
End Sub
Public Function NewChild as cChild
Set NewChild = New cChild
Set NewChild.oCommon = moCommon
moChildren.Add newChild
End Function
'cCommon
Public sName As String
'cChild
Private moCommon As cCommon
Public Name as String
Public Property Set oCommon(RHS as cCommon)
Set moCommon = RHS
End Property
Public Property Get Path() as String
Path = moCommon.Name & "/" & Name
End Property
就目前而言,它實際上只適用於一層級的父子關係,但通常我們會擁有無限級的層次結構,例如在磁碟目錄結構中。
我們可以透過認識到父節點和子節點實際上可以是同一個類,並且子節點並不關心父節點路徑是如何確定的,只要它來自公共物件即可,來對這種情況進行概括。
'cFolder
Private moChildren as Collection
Private moCommon as cCommon
Private Sub Class_Initialize()
Set moChildren = New Collection
Set moCommon = New cCommon
End Sub
Public Function NewFolder as cFolder
Set NewFolder = New cFolder
Set NewFolder.oCommon = moCommon
moChildren.Add newFolder
End Function
Public Property Set oCommon(RHS as cCommon)
Set moCommon.oCommon = RHS
End Property
Public Property Get Path() as String
Path = moCommon.Path
End Property
Public Property Get Name() as String
Name= moCommon.Name
End Property
Public Property Let Name(RHS As String)
moCommon.Name = RHS
End Property
'cCommon
Private moCommon As cCommon
Public Name As String
Public Property Get Path() as String
Path = "/" & Name
if not moCommon is Nothing then
' has parent
Path = moCommon.Path & Path
End If
End Property
現在,我們可以要求結構中任何級別上的任何物件提供其完整路徑,它將返回該路徑,而無需引用其父節點。
練習
[edit | edit source]- 建立一個使用 cfolder 和 cCommon 類別的簡單程式,並證明它有效;也就是說,它既不會洩漏記憶體,也不會在Path屬性方面給出錯誤的答案。
錯誤和異常
[edit | edit source]在討論各種錯誤之前,我們將展示 Visual Basic 中如何處理錯誤。
Visual Basic 沒有異常類,而是使用較舊的錯誤程式碼系統。雖然這會使某些型別的程式設計變得笨拙,但它在編寫良好的程式中實際上不會造成很大的麻煩。如果您的程式在正常操作期間不依賴於處理異常,那麼您對異常類也沒有什麼用處。
但是,如果您是正在建立由大量元件(COM DLL)組成的龐大程式的團隊中的一員,那麼很難保持錯誤程式碼列表的同步。一種解決方案是維護一個所有專案成員都使用的主列表,另一種解決方案是最終使用異常類。有關在純 VB6 中實現 mscorlib.dll 中許多類的實現,請參見VBCorLib,該實現為 Microsoft 的 .NET 架構建立的程式提供了基礎。
Visual Basic 中有兩種語句實現錯誤處理系統
- On Error Goto
- Err.Raise
處理錯誤的常用方法是在過程的頂部放置一個On Error Goto語句,如下所示
On Error Goto EH
EH是過程末尾的一個標籤。在標籤之後,您放置處理錯誤的程式碼。這是一個典型的錯誤處理程式
Exit Sub
EH:
If Err.Number = 5 Then
FixTheProblem
Resume
End If
Err.Raise Err.Number
End Sub
需要注意以下幾點
- 在錯誤處理程式標籤之前有一個Exit Sub語句,以確保當沒有發生錯誤時,程式不會進入錯誤處理程式。
- 將錯誤程式碼與常量進行比較,並根據結果採取行動。
- 最後一條語句重新引發錯誤,以防沒有顯式處理程式。
- 包含一個resume語句,以便在失敗語句處繼續執行。
這可能是一個對某些過程非常有用的處理程式,但它也有一些弱點
- 使用字面常量。
- 捕獲所有Err.Raise語句沒有提供任何有用的資訊,它只是傳遞了原始錯誤程式碼。
- Resume重新執行失敗的語句。
沒有理由在任何程式中使用字面常量。始終將它們宣告為單個常量或列舉。錯誤程式碼最好像這樣宣告為列舉
Public Enum ErrorCodes
dummy = vbObjectError + 1
MyErrorCode
End Enum
當您需要一個新的錯誤程式碼時,只需將其新增到列表中即可。程式碼將按遞增順序分配。實際上,您真的不需要關心實際數字是多少。
您可以以相同的方式宣告內建的錯誤程式碼,只是您必須顯式設定值
Public Enum vbErrorCodes
InvalidProcedureCall = 5
End Enum
錯誤分類
[edit | edit source]大體上來說,有三種類型的錯誤
- 預期錯誤
- 預期錯誤發生在使用者或其他外部實體提供明顯無效的資料時。在這種情況下,使用者(無論是人類還是其他程式)必須被告知資料中的錯誤,而不是被告知在程式中發現錯誤的位置。
- 未能遵守契約
- 呼叫者未能向子例程提供有效的引數,或者子例程未能提供有效的返回值。
- 意外錯誤
- 發生了一個程式規範中未預測到的錯誤。
預期錯誤實際上不是程式中的錯誤,而是呈現給程式的資料中的錯誤或不一致。程式必須請求使用者修復資料並重試操作。在這種情況下,使用者不需要堆疊跟蹤或其他內部資訊,而是非常希望以外部術語清晰完整地描述問題。錯誤報告應該直接將使用者指向資料中的問題,並提供修復建議。不要只說“無效資料”,說出哪些資料無效,為什麼無效以及可以接受的值範圍。
契約失敗通常表明程式碼中存在邏輯錯誤。假設所有無效資料都已被前端剔除,那麼程式必須工作,除非程式本身存在故障。有關此主題的更多資訊,請參見#斷言和契約式設計。
意外錯誤是大多數程式設計師關注的錯誤。實際上,它們並不十分常見,之所以看起來很常見,是因為程式各個部分之間的契約很少被明確說明。意外錯誤與預期錯誤的不同之處在於,使用者通常無法預期能夠立即從意外錯誤中恢復。然後,程式需要以一種可以輕鬆地傳輸回程序維護人員的形式,向用戶提供程式在發現錯誤時內部狀態的所有詳細資訊。日誌檔案非常適合此目的。不要只是向用戶顯示一個標準的訊息框,因為沒有實際方法可以捕獲該描述,以便將其傳送給維護人員。
錯誤引發和處理
[edit | edit source]預期錯誤
[edit | edit source]這些是輸入資料中的錯誤。從程式的角度來看,它們不是異常。程式應該顯式地檢查有效的輸入資料,並在資料無效時顯式地通知使用者。這種檢查應該在使用者介面或程式的其他部分中進行,這些部分能夠直接與使用者互動。如果使用者介面無法自行執行檢查,那麼底層元件必須提供驗證資料的方法,以便使用者介面可以使用這些方法。用於通知使用者的方法應該取決於錯誤的嚴重程度和緊迫性,以及與程式互動的一般方法。例如,原始碼編輯器可以在不影響使用者創意流程的情況下透過突出顯示有問題的語句來標記語法錯誤,使用者可以隨時修復它們。在其他情況下,模態訊息框可能是正確的通知方法。
這些錯誤透過斷言過程的前置條件和後置條件的真值來檢測,可以使用斷言語句,或者在條件不滿足時引發錯誤。這類錯誤通常表明程式邏輯存在錯誤,出現時,報告必須包含足夠的資訊以便複製問題。報告可以非常明確地說明故障的直接原因。在使用 Visual Basic 時,要記住 Debug.Assert 語句僅在 IDE 中執行,因此除了最關鍵的例程外,在其他例程中引發錯誤可能會有用。
這是一個簡單的契約斷言示例,過於簡單:
Public Sub Reciprocal(n as Double) as Double
Debug.Assert 0 <> n
Reciprocal = 1 / n
End Sub
Visual Basic 將在斷言 n = 0 時停止,因此如果在 IDE 中執行測試,則會直接跳轉到發現錯誤的位置。
這些錯誤既沒有被輸入資料驗證捕獲,也沒有被前置條件或後置條件的真值斷言捕獲。與契約錯誤一樣,它們表明程式中存在邏輯錯誤;當然,邏輯錯誤可能存在於輸入資料的驗證中,甚至可能存在於某個前置條件或後置條件中。
這些錯誤需要最全面的報告,因為它們顯然不常見,而且很難預測,否則它們將在驗證和契約檢查中被預見。與契約錯誤一樣,應將報告記錄到文字檔案中,以便可以輕鬆地將其傳送給維護人員。與契約錯誤不同,很難確定哪些資訊是相關的,因此在 Err.Raise 語句中包含所有子例程引數的描述,以確保安全。
| 上一節:面向物件程式設計 | 目錄 | 下一節:最佳化 Visual Basic |