1.System.Configuration.ConfigurationSettings.AppSettings”已淘汰【.net2.0】:
“System.Configuration.ConfigurationSettings.AppSettings”已淘汰:“This method is obsolete, it has been replaced by System.Configuration!System.Configuration.ConfigurationManager.AppSettings”
報出出錯:
在VS.NET2005中使用System.Configuration.ConfigurationManager.AppSettings["xxx"]時出錯提示
錯誤 1 命名空間“System.Configuration”中不存在類型或命名空間名稱“ConfigurationManager”(是缺少程式集引用嗎?)
解決方案:
在工程中的“引用”中添加“.NET-->System.Configuration”
將System.Configuration.ConfigurationSettings.AppSettings修改為System.Configuration.ConfigurationManager.AppSettings就可以解決問題^_^2.理解.net中的異常及處理 或許從第一次使用異常開始,我們就要經常考慮諸如何時捕獲異常,何時拋出異常,異常的效能如何之類的問題,有時還想瞭解究竟什麼是異常,它的機制又是什麼。本文試著對這些問題進行討論。
主要內容包括:
為什麼使用異常 主要討論異常與錯誤碼之間的選擇
異常的本質 異常的概念的理解
異常的機制 try,catch,finally三種語句塊的討論
System.Exception及其它FCL中的異常類 討論.NET Framework中預定義的異常類型
自訂異常類 如何建立自訂的異常類型
正確地使用異常 關於異常使用的一些規範和約定
效能問題的考慮 瞭解異常對效能的影響,並給出一些建議
應用程式中的未處理異常; 如何處理常式中的那些未處理的異常
1、為什麼要使用異常
通常在處理常式中的錯誤時,可以使用異常,也可以使用傳回值的方式。與用傳回值來報告錯誤相比,異常處理有諸多優勢:
異常已經很好地與物件導向語言整合在一起。
在某些情況下,如建構函式、操作符重載及屬性,開發人員對傳回值沒有什麼選擇的餘地。想在物件導向架構中統一使用傳回值來報告錯誤是不可能的。在上述情況下,唯一的選擇是使用傳回值之外的方法,如異常。既然如此,最好的辦法就是在所有地方都使用異常來報告錯誤。這是必須用異常來報告錯誤的最重要原因(Jeffery Richter語)。
異常增強了API的一致性。
異常的唯一目的就是為了報告錯誤,而傳回值則有多種用途,報告錯誤只是其中之一。因此如果使用異常,那麼報告錯誤的方式是固定的,這保證了API的一致性。
更容易使錯誤處理的代碼局部化,錯誤處理的代碼可以放在一個更集中的位置。
有很多原因可以導致代碼失敗,如引用null對象、索引越界、訪問已關閉的檔案、記憶體耗盡等等。不使用異常處理,要使我們的代碼容易地檢測這些原因、並從中恢複過來,即使有可能,也是很困難的。同時要進行這種檢測的代碼需要散布在程式主邏輯中,使得編碼過程變得非常困難,代碼也難以理解和維護。
如果使用異常處理,我們就無需再自己編寫代碼來檢測這些潛在的故障。我們只管放心地編碼,並“自信地”認為代碼沒有問題,這個過程自然會變得簡單,代碼也容易理解和維護。最後將異常恢複代碼放在一個集中的位置(try代碼塊的後面,或者調用棧的更高層),當程式出現故障時,才會執行這些恢複代碼。
使用異常,我們可以將資源清理代碼放在一個固定的位置,並確保得到執行。
將資源清理代碼從應用程式的主邏輯移到一個固定的位置後,應用程式將會變得更容易編寫、理解和維護。
容易定位和修複代碼中的Bug
一旦程式出現了問題,CLR會遍曆線程的呼叫堆疊,以尋找能夠處理該異常的代碼。如果找不到這樣的代碼,我們將收到一個開發人員很不願看到的“未處理異常”通知。根據這個通知,可以很容易地定位問題發生地點,判定原因,並修複Bug。
錯誤碼容易被忽略,而且通常會被忽略。而異常則不然。
異常允許使用者定義未處理異常的處理常式,那錯誤碼呢?
另外,異常有助於與一些工具的互動。
各種工具(如調試器、效能分析器、效能計數器等)會時刻注意異常的發生。返回錯誤碼的方法就沒有這些好處。
看了這些比較之後,你還會選擇錯誤碼嗎?
2、異常的本質
異常本質上是對程式界面隱含式假設的一種違反。
在設計一個類型時,我們首先應設想該類型被應用的各種情境。類型的名稱通常是一個名詞,如FileStream,StringBuilder等。然後再為類型定義屬性、方法及事件等成員。這些成員的定義方式(屬性的資料類型,方法的參數、傳回值等)就是類型的程式介面。同時,這些成員描述了類型(或其執行個體)所能執行的操作,它們的名稱通常為動詞,如Read,Write,Flush,Append,Insert,Remove等。
對於類型的一個成員來說,如果不能完成它的任務,就應當拋出異常。異常意味著類型的成員未能完成其名稱所描述的功能。
我們定義的程式介面通常會有一些隱含假設,如System.IO.File.OpenText(string path)方法,它的功能是開啟現有的UTF-8編碼文字檔以進行讀取,要完成該功能,就要求path參數是一個合法的路徑、指定的檔案存在、有足夠的許可權等。所有這些要求在MSDN文檔中都有所記錄,我們在調用該方法時可以詳細地查看文檔,以最高效的方式來調用它。但我們都瞭解,這不太現實,我們不太可能去瞭解所有的這些隱含的假設,從而也就不可避免地違反它們了。
那麼,對於OpenText方法的開發人員來說,如何通知調用它的程式,隱含假設被違反了呢?答案是拋出一個異常(你也許會想到傳回值,關於傳回值和異常的取捨請參看第1節)。
所以在設計一個類型時,我們應該首先假設類型最常見的使用方式,然後設計其介面使之能夠很好地處理這種情況。最後再考慮介面帶來的隱含假設,並且當任何假設被違反時便拋出異常。
這樣理解下來,有些觀點正確與否就很清楚了。
我們把注意力放在前面提及的OpenText方法,來看看下面兩種關於異常的誤解。
異常與事情發生的頻率有關
OpenText方法的開發人員決定何時拋出異常,但真正引發異常的卻是調用代碼,那OpenText方法的開發人員又怎麼知道調用代碼引發異常的頻率?
異常就是錯誤
錯誤意味著程式員犯錯了,當調用代碼在應用程式中錯誤地調用了OpenText方法時,設計該方法的開發人員何從知道呢?只有調用程式才能判斷調用的結果是否出現了錯誤。換言之,異常是OpenText方法對調用程式的一種反饋,而調用的結果是否為錯誤則是由調用程式判斷的,是與調用程式的上下文環境相關的。
3、異常的機制
下面的C#代碼展示了異常處理的標準用法,從總體上描述了異常處理的樣子和用途。在代碼之後詳細討論了try、catch、finally三種語句塊。
try
{
// 在此處編寫那些需要恢複或清理操作的代碼
}
catch (NullReferenceException)
{
// 在此處編寫能夠從NullReferenceException(或其衍生類別型異常)中恢複的代碼
}
catch (Exception)
{
// 我們在這個塊中編寫能夠從任何與CLS相容的異常中恢複的代碼
// 另外,此時通常應將其重新拋出
throw;
}
catch
{
// 我們在這個塊中編寫能夠從任何與CLS相容或者不相容的異常中恢複的代碼
// 此時通常應將其重新拋出
throw;
}
finally
{
// 在finally塊中我們放入那些對try塊中啟動的操作進行清理的代碼。
// 不管是否有異常拋出,此處代碼總是執行。
}
3.1 try塊
try塊中包含的通常是需要進行清理或/和異常恢複的操作。所有的資源清理代碼都應該放在一個finally塊中(以確保總是得到執行)。try塊還可以包含可能拋出異常的代碼。進行異常恢複操作的代碼則應該放在一個或多個catch塊中。一個try塊必須有至少一個與之相關聯的catch塊或finally塊,單獨一個try塊是沒有意義的(C#編譯器也不允許你這麼做)。
3.2 catch塊
catch塊中包含的是出現異常時需要執行的相應代碼。一個try塊可以有0個或多個catch塊與之相關聯。
如果try塊中的代碼沒有拋出異常,CLR就不會執行與該try塊關聯的catch塊的代碼。而是會跳過所有的catch塊,直接執行finally塊(如果存在的話)中的代碼,然後再執行finally塊之後的語句。
catch關鍵字後面的運算式稱作異常篩選器(exception filter)。它表示開發人員預料到的、並可以從中恢複的一種異常情況。代碼執行時是自上而下搜尋catch塊的,因此要將更具體的(是指該類型在繼承體系中的層次)異常放在上面。事實上,C#編譯器不允許更具體的catch塊出現在離代碼底部更近的位置。
如果try塊(或者被try塊調用的方法)中的代碼拋出了一個異常,CLR將搜尋那些篩選器中能夠識別該異常的catch塊。如果如該try塊相關聯的篩選器中沒有一個能夠接受該異常,CLR將沿著呼叫堆疊向更高層搜尋能夠接受該異常的篩選器,如果直到堆棧頂部依然沒有找到能夠處理該異常的catch塊,就會出現所謂的未處理異常。
一旦CLR找到了一個能夠處理所拋出異常的篩選器,它將執行從拋出異常的try塊開始,到匹配異常的catch塊為止的範圍內所有的finally塊(注意要在呼叫堆疊中理解,此處執行的代碼不包括匹配異常的catch塊相關聯的finally塊),然後調用匹配catch塊中的代碼,最後才是匹配catch塊相關聯的finally塊的代碼。
在C#中,異常篩選器可以指定一個異常變數。當捕獲到一個異常時,該變數指向那個被拋出的、類型繼承自System.Exception的對象,此時可以通過該變數擷取異常的相關資訊(如其堆棧蹤跡)。儘管可以改變該對象,但不應這麼做,應把它當作唯讀變數。
catch塊中的代碼一般執行一些從異常中恢複的操作。在catch塊的末尾(比如進行恢複操作後),我們有三種選擇:
<!--[if !supportLists]--><!--[endif]-->重新拋出所捕獲的異常,向更高一層的呼叫堆疊中的代碼通知該異常的發生。
<!--[if !supportLists]--><!--[endif]-->拋出一個不同的異常,向更高一層的呼叫堆疊中的代碼提供更多資訊。
<!--[if !supportLists]-->讓線程從catch塊的底部退出。
如果我們選擇前兩種方法,將拋出一個異常,CLR的行為將和處理前面的異常時一樣,遍曆呼叫堆疊搜尋合適的篩選器。如果選用第三種方法,會將異常吞掉,更高層次的呼叫堆疊將不會知曉異常的發生,在執行完catch塊中的代碼後,立即執行與之關聯的finally塊(如果存在的話),然後執行當前try/catch/finally語句塊之後的代碼。
關於這三種方法的選擇,將在後面討論。
3.3 finally塊
finally塊中包含的是確保要執行的代碼。一般地,finally塊中的代碼執行的是一些資源清理操作,這些清理操作通常是try塊中的行為所需要的。例如,我們在try塊中開啟一個檔案,那麼我們就應該將關閉檔案的代碼放在與其對應的finally塊中。
FileStream fs = null;
try
{
fs = new FileStream(fileName, FileMode.Open);
}
catch (OverflowException)
{
// 恢複操作的代碼
}
finally
{
if (fs != null)
{
fs.Close();
}
}
清理資源的代碼不能放在finally塊之後,如果出現了異常,但catch塊未能捕獲,那麼這些代碼就不會執行了。
try塊也並非總需要finally塊,有時候try塊的操作並不需要任何清理工作。
這裡將討論FCL中預定義的異常類,自訂異常類,正確的使用異常(拋出、捕獲、封裝),最後給出效能方面的建議。
4、System. Exception及其它FCL中的異常類
4.1 System.Exception 類型
CLR允許我們將任何類型——Int32、String等——的一個執行個體作為異常拋出。但實際上,微軟定義了System.Exception類型,並規定所有和CLS相容的程式設計語言都必須能夠拋出並捕獲那些繼承自System.Exception的異常類型。繼承自System.Exception的異常類型被認為是與CLS相容的。C#和其它許多語言都只允許代碼拋出與CLR相容的異常。
System.Exception類型是一個很簡單的類型,下表列出了它所包含的一些屬性。
屬性
存取權限
類型
描述
Message
唯讀
String
包含一段輔助性的文本,描述異常發生的原因。在出現未處理異常時,這些資訊通常會寫入log。這些資訊使用者通常是看不見的,所以應盡量使用技術性的詞彙以協助其它開發人員修正代碼。
Data
唯讀
IDictionary
一個指向key-value對集合的引用。通常應在拋出異常前,向該集合添加資訊,而捕獲異常的代碼則使用這些資訊進行異常恢複操作。
Source
讀寫
String
產生異常的程式集的名稱
StackTrace
唯讀
String
包含了呼叫堆疊中拋出異常的方法的名稱和簽名。該屬性對於調試極具價值。
TargetSite
唯讀
MethodBase
拋出異常的方法。
HelpLink
讀寫
String
擷取或設定異常的關聯協助檔案的連結。
InnerExceptoin
唯讀
Exception
如果當前異常是在處理另一個異常時產生的,那麼該屬性工作表示前一個屬性。該屬性通常為null。Exception類型還提供了一個公有方法GetBaseException,用以遍曆所以內部異常組成的鏈表,返回最開始那個異常。
4.2 FCL中的異常類結構
.NET架構類庫(FCL)中定義了很多異常類型(而它們最終又都繼承自Exception類)。
微軟的最初想法是這樣的:System.Exception應是所有異常的基底類型,另兩個類型System.SystemException和System.ApplicationException則是僅有的直接繼承自System.Exception的異常類型。此外,CLR拋出的異常都繼承自SystemException,應用程式拋出的異常應當繼承自ApplicationException。這樣一來,開發人員就能夠編寫catch塊來捕獲所有CLR拋出的異常或所有應用程式拋出的異常。
但遺憾的是,FCL也沒能很好地遵循這個原則。有些異常類直接繼承自Exception(IsolatedStorageException),有些CLR拋出的異常卻繼承自ApplicationException(如TargetInvocationException),還有些應用程式拋出的異常則繼承自SystemException(如FormatException)。這顯得有些混亂,而其結果便是SystemException和ApplicationException類型的存在沒有多大價值了。
5、自訂異常類
如果FCL沒有為我們定義合適的異常類型,我們就要考慮定義建立自己的異常類了。
一般情況下,自訂異常類應繼承自Exception類型或其它與Exception相近的基底類型,如果我們定義的類型不打算作為其它類型的基底類型,應該將其標識為sealed。
Exception基底類型定義了4個建構函式:
<!--[if !supportLists]--><!--[endif]-->公有的無參(預設)建構函式,建立一個異常類型的執行個體,並將所有欄位和屬性的值設定為預設值。
<!--[if !supportLists]-->公有的帶一個String參數的建構函式,建立一個異常類型的執行個體,異常的訊息將設定為參數指定的文本。
<!--[if !supportLists]-->公有的帶String和Exception參數的建構函式,建立一個異常類型的執行個體,並設定其訊息文本和內部異常。在對異常進行封裝時,該建構函式會顯示出其重要性。
<!--[if !supportLists]--><!--[endif]-->受保護的帶SerializationInfo和StreamingContext參數的建構函式,還原序列化Exception對象的執行個體。記住,如果異常類型是密封的(sealed),該方法應聲明為private的,確保該建構函式能夠調用基底類型的相同的建構函式,這樣基類的欄位能夠得到正確的序列化。
在定義自己的異常類型時,我們應當實現這四個建構函式,而且它們都要調用基底類型中相應的建構函式。
當然,我們定義的異常類型會繼承Exception類型的所有欄位和屬性。此外,我們還可以為其添加自己的欄位和屬性。例如,System.ArgumentException中添加了一個虛擬(virtual)String屬性ParamName(其它的一切則繼承自Exception類型)。ArgumentException也定義了兩個新的建構函式(除了上述四個),來初始化ParamName屬性。這裡我們也應瞭解,如果為異常類型添加欄位,要確保添加必要的建構函式來初始化這些欄位,同時也要定義相應的屬性或其它成員(如方法)返回這些欄位的值。
所有的異常類型(繼承自Exception類型)都應是可序列化的,只有這樣異常對象才能在跨越應用程式定義域時得到封送處理(marshaled),同時還能持久化至日誌或資料庫。要使自訂的異常類型可序列化,首先為該類型應用Serializable特性(attribute);如果類型定義了新的欄位,我們還必須讓其實現ISerilizable介面的GetObjectData方法和上面提及的受保護的建構函式。
下面是的樣本示範了如何正確地建立自訂異常類型:
要實現上面四個建構函式,建議使用VS 2005中的Code Snippet:
按兩下Tab後,VS就會為你產生類的基本結構:),我們在這個基礎上編寫代碼。
下面是類的完整代碼:
// 為類應用Serializable特性
[Serializable]
public sealed class CustomException : Exception
{
// 添加的自訂欄位
private string stringInfo;
private bool booleanInfo;
// 實現三個公有的建構函式,這裡只是簡單地調用基類的建構函式
public CustomException() { }
public CustomException(string message) : base(message) { }
public CustomException(string message, Exception inner)
: base(message, inner) { }
// 實現ISerialization介面所需要的還原序列化建構函式。
// 因為本類為sealed,該建構函式為private。
private CustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
stringInfo = info.GetString("StringInfo");
booleanInfo = info.GetBoolean("BooleanInfo");
}
// 添加建構函式以確保自訂欄位能得到正確的初始化
public CustomException(string message, string stringInfo, bool booleanInfo)
: base(message)
{
this.stringInfo = stringInfo;
this.booleanInfo = booleanInfo;
}
public CustomException(string message, Exception inner, string stringInfo, bool booleanInfo)
: base(message, inner)
{
this.stringInfo = stringInfo;
this.booleanInfo = booleanInfo;
}
// 通過屬性或其它成員提供對自訂欄位的訪問
public string StringInfo
{
get { return stringInfo; }
}
public bool BooleanInfo
{
get { return booleanInfo; }
}
// 重寫GetObjectData方法。
// 如果添加了自訂欄位,一定要重寫基類GetObjectData方法的實現。
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
// 序列化自訂資料成員
info.AddValue("StringInfo", stringInfo);
info.AddValue("BooleanInfo", booleanInfo);
// 調用基類方法,序列化它的成員
base.GetObjectData(info, context);
}
public override string Message
{
get
{
string message = base.Message;
if (stringInfo != null)
{
message += Environment.NewLine +
stringInfo + " = " + booleanInfo;
}
return message;
}
}
}
然後使用下面這樣的代碼測試異常的序列化:
// 建立一個CustomException對象,將其序列化
CustomException e = new CustomException("New Custom Exception", "My String Info", true);
FileStream fs = new FileStream(@"Test", FileMode.Create);
IFormatter f = new SoapFormatter();
f.Serialize(fs, e);
fs.Close();
// 還原序列化CustomException對象,查看它的欄位
fs = new FileStream(@"Test", FileMode.Open);
e = (f.Deserialize(fs)) as CustomException;
fs.Close();
Console.WriteLine(e.Message);
此處需要引用System.Runtime.Serialization.Formatters.Soap.dll。
6、正確地使用異常
根據第2節的討論,我們已經瞭解何種情況可以稱之為異常。下面再給出一些異常使用相關的規範或建議。
借鑒《.NET設計規範》一書的做法,本節的這些規範和建議通常由要、考慮、避免、不要這些詞進行組織,每一條都描述了一種好的或是不好的做法。對於好的做法,標以√,相應的,對那些不好的做法則標以×。而不同的措辭也能夠揭示該條規範的重要性。
“要……”描述的是總要遵循的規範(但特殊情況下,可能需要違反)。
“考慮……”描述的是一般情況下應該遵循的規範,但如果完全理解規範背後的道理,並有很好的理由不遵循它時,也不要畏懼打破常規。
“不要……”描述的是一些幾乎絕對不該違反的規範。
“避免……”則沒有那麼絕對,它描述的是那些通常並不好,但卻存在一些已知的可以違反的情況。
6.1 拋出異常
在設計我們自己的方法時,應考慮如何正確地拋出異常。
× 不要返回錯誤碼。
前面第1節已經討論了異常的種種好處,所以還是把異常作為報告錯誤的主要方法。記住每個異常都有兩種資訊:其一是異常資訊(Message屬性),其二是異常的類型,例外處理常式根據它來決定應該執行什麼操作。
√ 要通過拋出異常的方式來報告操作失敗。
如果一個方法未能完成它應該完成的任務,那麼應該認為這是方法層面的操作失敗,並拋出異常。
√ 考慮通過調用System.Environment.FailFast(New in .NET 2.0)來終止進程,而不要拋出異常,如果代碼遇到了嚴重問題,已經無法繼續安全地執行。
× 不要在正常的控制流程中使用異常,如果能夠避免的話。
√ 考慮拋出異常可能會對效能造成的影響,詳見第7節。
√ 要為所有的異常撰寫文檔,異常本質上是對程式界面隱含式假設的一種違反。我們顯然需要對這些假設作詳細的文檔,以減少使用者代碼引發異常的機會。
× 不要讓公有成員根據某個選項來決定是否拋出異常。
例如:
// 不好的設計
public Type GetType(string path, bool throwOnError)
調用者要比方法設計者更難以決定是否拋出異常。
× 不要把異常用作公有成員的傳回值或輸出參數。
這樣會喪失用異常來報告操作失敗的諸多好處。
× 避免顯式地從finally代碼塊中拋出異常。
√ 考慮優先使用System命名空間中已有的異常,而不是自己建立新的異常。
√ 要使用自訂的異常類型,如果對錯誤的處理方式與其它已有異常類型有所不同。
關於建立自訂異常類的的細節見第5節。
× 不要僅僅為了擁有自己的異常而建立並使用新的異常。
√ 要使用最合理、最具針對性的異常。
拋出System.Exception總是錯的,如果這麼做了,那麼就想一想自己是否真地瞭解拋出異常的原因。
√ 要在拋出異常時提供豐富而有意義的錯誤訊息。
要注意的是這些資訊是提供給誰的,可能是其它開發人員,也可能是終端使用者,所以這些資訊應根據面向的對象設計。
√ 要確保異常訊息的文法正確無誤(指自然語言,如漢語、英語等)。
√ 要確保異常訊息中的每個句子都有句號。
這個看起來似乎過於追究細節了,那麼想想這種情況:使用FCL預定義異常的Message資訊時,我們有沒有加過句號。如果我們提供的資訊沒有句號,其它開發人員使用時到底加不加句號呢?
× 避免在異常訊息中使用問號和驚嘆號。
或許我們習慣於使用驚嘆號來”警示”某些操作有問題,捫心自問,我們使用的代碼返回一個驚嘆號,自己會有什麼感覺。
× 不要在沒有得到許可的情況下在異常訊息中泄漏安全資訊。
√ 考慮把組件拋出的異常資訊本地化,如果希望組件為使用不用(自然)語言的開發人員使用。
6.2 處理異常
根據6.1節的討論,我們可以決定何時拋出異常,然後為之選擇合適的類型,設計合理的資訊,下一步就是如何處理異常了。
如果用catch語句塊捕獲了某個特定類型的異常,並完全理解在catch塊之後繼續執行對應用程式意味著什麼,那麼我們說這種情況是對異常進行了處理。
如果捕獲的異常具體類型不確定(通常都是如此),並在不完全理解操作失敗的原因或沒有對操作失敗作出反應的情況下讓應用程式繼續執行,那麼我們說這種情況是把異常吞了。
× 不要在架構(是指供開發人員使用的程式)的代碼中,在捕獲具體類型不確定的異常(如System.Exception、System.SystemException)時,把異常吞了。
× 避免在應用程式的代碼中,在捕獲具體類型不確定的異常(如System.Exception、System.SystemException)時,把錯誤吞了。
有時在應用程式中把異常吞了是可以接受的,但必須意識到其風險。發生異常通常會導致狀態的不一致,如果貿然將異常吞掉,讓程式繼續執行下去,後果不堪設想。
× 不要在為了轉移異常而編寫的catch代碼塊中把任何特殊的異常排除在外。
√ 考慮捕獲特定類型的異常,如果理解異常產生的原因,並能對錯誤做適當的反應。
此時一定要能夠確信,程式能夠從異常中完全恢複。
× 不要捕獲不應該捕獲的異常。通常應允許異常沿調用棧向上傳遞。
這一點極為重要。如果捕獲了不該捕獲的異常,會讓bug更難以發現。在開發、測試階段應當把所有bug暴露出來。
√ 要在進行清理工作時使用try-finally,避免使用try-catch。
對於精心編寫的代碼來說,try-finally的使用頻率要比try-catch要高的多。這一點可能有違於直覺,因為有時可能會覺得:try不就是為了catch嗎?要知道一方面我們要考慮程式狀態的一致,另一方面我們還需要考慮資源的清理工作。
√ 要在捕獲並重新拋出異常時使用空的throw語句。這是保持調用棧的最好方法。
如果捕獲異常後拋出新的異常,那麼所報告的異常已不再是實際引發的異常,顯然這會不利於程式的調試,因此應重新拋出原來的異常。
× 不要用無參數的catch塊來處理不與CLS相容的異常(不是繼承自System.Exception的異常)。
有時候讓底層代碼拋出的異常傳遞到高層並沒有什麼意義,此時,可以考慮對底層的異常進行封裝使之對高層的使用者也有意義。還有一種情況,更重要的是要知道代碼拋出了異常,而異常的類型則顯得無關緊要,此時可以封裝異常。
√ 考慮對較低層次拋出的異常進行適當的封裝,如果較低層次的異常在較高層次的運行環境中沒有什麼意義。
× 避免捕獲並封裝具體類型不確定的異常。
√ 要在對異常進行封裝時為其指定內部異常(inner exception)。
這一點極為重要,對於代碼的調試會很有協助。
6.3 標準異常類型的使用
× 不要拋出Exception或SystemException類型的異常。
× 不要在架構(供其它開發人員使用)代碼中捕獲Exception或SystemException類型的異常,除非打算重新拋出。
× 避免捕獲Exception或SystemException類型的異常,除非是在頂層的異常處理器程式中。
× 不要拋出ApplicationException類型的異常或者從它派生新類(參看4.2描述)。
√ 要拋出InvalidOperationException類型的異常,如果對象處於不正確的狀態。
一個例子是向唯讀FileStream寫入資料。
√ 要拋出ArgumentException或其子類,如果傳入的是無效參數。要注意盡量使用位於繼承層次末尾的類型。
√ 要在拋出ArgumentException或其子類時設定ParamName屬性。
該屬性工作表明了哪個參數引發了異常。
public static FileAttributes GetAttributes(string path)
{
if (path == null)
{
throw new ArgumentNullException("path", );
}
}
√ 要在屬性的設定方法中,以value作為隱式值參數的名字。
public FileAttributes Attributes
{
set
{
if (value == null)
{
throw new ArgumentNullException("value", );
}
}
}
× 不要讓公用的API拋出這些異常。
拋出這些異常會暴露實現細節,而細節可能會隨時間變化。
另外,不要顯式地拋出StackOverflowException、OutOfMemeryException、ComException、SEHException異常,應該只有CLR才能拋出這些異常。
7、效能方面的考慮
我們在使用異常時常常會產生效能方面的顧慮,在調試的時候感覺尤其明顯。這樣的顧慮合情合理。當成員拋出異常時,對效能的影響將是指數級的。當遵循前面的規範,我們仍有可能獲得良好的效能。本節推薦兩種模式。
7.1 Tester-Doer 模式
有時候,我們可以把拋出異常的成員分解為兩個成員,這樣就能提高該成員的效能。下面看看ICollection<T>介面的Add方法。
ICollection<int> numbers = …
numbers.Add(1);
如果集合是唯讀,那麼Add方法會拋出異常。在Add方法經常會失敗的情境中,這可能會引起效能問題。緩解問題的方法之一是在調用Add方法前,檢查集合是否可寫。
ICollection<int> numbers = …
…
if(!numbers.IsReadOnly)
{
numbers.Add(1);
}
用來對條件進行測試的成員成為tester,這裡就是IsReadOnly屬性;用來執行實際操作並可能拋出異常的成員成為doer,這裡就是Add方法。
√ 考慮在方法中使用Test-Doer模式來避免因異常而引發的效能問題,如果該方法在普通的情境中都可能會拋出異常(引發異常的頻率較高)。
前提是”test”操作要遠比”do”操作快。另外要注意,在多線程訪問一個對象時會有危險性。
7.2 Try-Parse 模式
與Tester-Doer 模式相比,Try-Parse 模式甚至更快,應在那些對效能要求極高的API中使用。該模式對成員的名字進行調整,使成員的語義包含一個預先定義號的測試。例如,DateTime定義了一個Parse方法,如果解析字串失敗,那麼它會拋出異常,同時還提供了一個與之對應的TryParse方法,在解析失敗時會返回false,成功時則通過一個輸出參數來返回結果。
使用這個模式時注意,如果因為try操作之外的原因導致(方法)操作失敗,仍應拋出異常。
√ 考慮在方法中使用Try-Parse模式來避免因異常而引發的效能問題,如果該方法在普通的情境中都可能會拋出異常。