文章目錄
- 捕獲異常私了還是將異常簡單處理後繼續拋出
- 如何快速高效的定義自己的異常類型
- 異常和效能
這幾天看C#基礎書籍中關於異常處理部分的內容,總結一下我的收穫,呵呵!
總共有以下幾個收穫:
- 如何能有一個機制在APP最頂層捕獲到所有拋出的異常(包括被捕獲的和未被捕獲的),而又不影響App內部處理每個異常的方式?
- 捕獲異常私了還是將異常簡單處理後繼續拋出?
- 如何快速高效的定義自己的異常類型?
- 異常和效能
下面詳細談一談:
捕獲應用程式定義域中拋出的所有異常
在開發過程中,有時候我們可能需要有一個統一的介面來處理所有當前Managed 程式碼中任何位置拋出的異常。實際上,由於很多的異常在代碼的不同位置被我們自己寫的代碼已經捕獲了,而有些代碼又沒有被捕獲,還有些異常是被捕獲之後又重新被拋出了,等等。其實,AppDomain這個對象上有一個很重要的事件,那就是FirstChanceException,這個事件可以在一個異常觸發後,而且是在未被其他代碼捕獲前首先觸發該事件,並把異常對象包含在FirstChanceExceptionEventArgs中,換句話說,該事件能夠在第一時間向我們報告任何一個被拋出的異常,包括Re-Throw,下面是它的聲明:
// // Summary: // Occurs when an exception first occurs in managed code, before the runtime // has made its first pass searching for suitable exception handlers. public event EventHandler<FirstChanceExceptionEventArgs> FirstChanceException;
從注釋上已經可以看到,該事件可以讓我們嘗到最新鮮、最及時的異常。
下面是我寫的一個Demo例子,來觀察一下它的行為。首先是在main函數中對currentDomain註冊FirstChanceException事件的處理方法。
static void Main(string[] args) { AppDomain.CurrentDomain.FirstChanceException += new EventHandler<System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs>
(CurrentDomain_FirstChanceException); SimpleExceptionTest.Test(); Console.ReadLine(); } static void CurrentDomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) { Console.WriteLine("CurrentDomain_FirstChanceException:"); Console.WriteLine(e.Exception); Console.WriteLine(); Console.WriteLine(); }
然後我編寫了一個可以拋出異常的並能自己捕獲的SimpleExceptionTest類:
class SimpleExceptionTest { static void TestInner() { try { Console.WriteLine("Exec in Try block!"); object o = null; Console.WriteLine(o.GetType()); } catch (NullReferenceException e) { Console.WriteLine("Exec in catch block! "); Console.WriteLine(e.Message); Console.WriteLine("Exec end catch block! "); throw; } finally { Console.WriteLine("================Exec in finally block!"); int i = 0; //finnally 塊中產生異常 int j = 5 / i; Console.WriteLine("================Exec end in finally block!"); } } public static void Test() { try { TestInner(); } catch (Exception e) { Console.WriteLine(e.Message); } } }
程式執行的結果如下:
Exec in Try block!
CurrentDomain_FirstChanceException:Object reference not set to an instance of an
object.
Exec in catch block!
Object reference not set to an instance of an object.
Exec end catch block!
CurrentDomain_FirstChanceException:Object reference not set to an instance of an
object.
================Exec in finally block!
CurrentDomain_FirstChanceException:Attempted to divide by zero.
Attempted to divide by zero.
從上面的執行結果可以看出如下幾個問題:
- CurrentDomain_FirstChanceException總是在第一時間捕獲到了異常,包括在Catch塊中ReThrow的異常。
- Finnally塊中拋出異常時,中斷了Finally塊的執行。我們知道,在Finally塊中,通常是寫一些清理性的代碼,比如釋放Try中開啟的資源,關閉Try中啟動的等待窗等等。所以如果你的finally中又會拋出異常的話,清理工作就會中斷了。
那麼如何才能保證Finnally塊中的清理工作一定能夠正確完成哪,我們難道要在Finally塊中嵌套一個TryCatchFinnally語句嗎,這將會形成死迴圈的,最後一個Finnally總是沒有保證的。唯一可行的辦法就是在finnally中不要放置多餘的代碼,並且認真的寫出高品質的清理代碼,以祈求它不要再拋出異常。
捕獲異常私了還是將異常簡單處理後繼續拋出
這個問題也是很多初學者的疑惑。我認為有以下幾個原則:
1、只私了自己可以明確知道如何處理的異常,比如 從業務和項目組的約定來看,當除以0發生時,我們都將該運算式的值返回0。 那麼這就是一個處理異常的法規,依據它就可以將這個異常私了,使異常止步於此,不再向呼叫堆疊上層拋出。
2、你是一個獨立的模組,而且有需要要求你必須做到決定的健壯,那麼你可以捕獲所有的異常,能處理的依照法規私了,不能處理的就記錄或者將異常以友好的方式報告給使用者或者伺服器,但是程式並不會因此而崩潰。
3、不知道如何處理的異常,自己捕獲到後,進行簡單處理(比如將異常封裝一下,加上更加明確的異常出現位置啊,異常觸發時的業務參數資訊等)再拋出,交給呼叫堆疊的上層去捕獲後處理。
如何快速高效的定義自己的異常類型
自訂一個異常,其實挺簡單的,只要定義一個類並繼承自System.Exception或其子類即可。但是那會造成異常類型繁多冗雜,我們自己的團隊成員都會記不清楚該有多少異常需要捕獲了。而且定義一個符合業務需要的異常又談何容易。
下面推薦一個CLR via C#一書中定義的異常泛型類Exception<TExceptionArgs> ,只要定義一個新的TExceptionArgs,就能可以讓我們獲得一個實用的自訂異常類型。
[Serializable] public sealed class Exception<TExceptionArgs> : Exception, ISerializable where TExceptionArgs : ExceptionArgs { private const String c_args = "Args"; // For (de)serialization private readonly TExceptionArgs m_args; public TExceptionArgs Args { get { return m_args; } } public Exception(String message = null, Exception innerException = null) : this(null, message, innerException) { } public Exception(TExceptionArgs args, String message = null, Exception innerException = null) : base(message, innerException) { m_args = args; } // The constructor is for deserialization; since the class is sealed, the constructor is // private. If this class were not sealed, this constructor should be protected [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] private Exception(SerializationInfo info, StreamingContext context) : base(info, context) { m_args = (TExceptionArgs)info.GetValue(c_args, typeof(TExceptionArgs)); } // The method for serialization; it’s public because of the ISerializable interface [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(c_args, m_args); base.GetObjectData(info, context); } public override String Message { get { String baseMsg = base.Message; return (m_args == null) ? baseMsg : baseMsg + " (" + m_args.Message + ")"; } } public override Boolean Equals(Object obj) { Exception<TExceptionArgs> other = obj as Exception<TExceptionArgs>; if (obj == null) return false; return Object.Equals(m_args, other.m_args) && base.Equals(obj); } public override int GetHashCode() { return base.GetHashCode(); } } [Serializable] public abstract class ExceptionArgs { public virtual String Message { get { return String.Empty; } } }}
下面我使用這個泛型異常類來構造自己的異常類型:
首先定義一個計算兩個城市距離的異常參數類,繼承自ExceptionArgs。
[Serializable] public sealed class CalcDistanceErrorArgs : ExceptionArgs { string m_cityId1 = string.Empty; string m_cityId2 = string.Empty; public CalcDistanceErrorArgs(string cityId1, string cityId2) { this.m_cityId1 = cityId1; this.m_cityId2 = cityId2; } public override string Message { get { return string.Format("city1'id = '{0}', city2 'id = '{1}' ", m_cityId1, m_cityId2); } } }
然後編寫一個函數,拋出一個這樣的異常:
public static void ThrowMyOwnExceptionType() { throw new Exception<CalcDistanceErrorArgs>(new CalcDistanceErrorArgs("NanJing", "Beijing"), "擷取不到這兩個城市之間的距離"); }
使用下面的代碼捕獲異常:
try { ThrowMyOwnExceptionType(); } catch (Exception<CalcDistanceErrorArgs> e) { Console.WriteLine(e); }
運行時,我們查看異常如所示:
運行輸出的異常資訊如下:
ConsoleApplication1.Exception`1[ConsoleApplication1.CalcDistanceErrorArgs]: 擷取不到這兩個城市之間的距離 (city1'id = 'NanJing', city2 'id = 'Beijing' )
at ConsoleApplication1.SimpleExceptionTest.ThrowMyOwnExceptionType() in D:\Study Prj\ConsoleApplication1\ConsoleApplication1\SimpleExceptionTest.cs:line 57
at ConsoleApplication1.SimpleExceptionTest.Test() in D:\Study Prj\ConsoleApplication1\ConsoleApplication1\SimpleExceptionTest.cs:line 47
好了,希望這個泛型異常類對您自訂異常有所協助吧。
異常和效能
下面的回複中有網友提出throw對效能會產生一定的影響,影響肯定是有的,我就不詳細闡述了,請參考MSDN的兩篇文章:
http://blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx (Design Guidelines Update: Exception Throwing)(1.2 Exceptions and Performance)
http://msdn.microsoft.com/zh-cn/library/ms229009.aspx (異常和效能)