背景
提到異常,我們會想到:拋出異常、異常恢復、資源清理、吞掉異常、重新拋出異常、替換異常、包裝異常。本文想談談 “包裝異常”,主要針對這個問題:何時應該 “包裝異常”?
“包裝異常” 的技術形式
包裝異常是替換異常的特殊形式,具體的技術形式如下:
1 try 2 { 3 // do something 4 } 5 catch (SomeException ex) 6 { 7 throw new WrapperException("New Message", ex); 8 }
注意:WrapperException 需要將 ex 作為 InnerException,這樣才不至於丟失 StackTrace,WrapperException.StackTrace 和 ex.StackTrace 共同構成了完整的 StackTrace。
讓例子幫助我們得出答案
有這樣一種場景:我希望為各種 ORM 框架提供一種抽象,這可以讓應用開發人員自由的在不同的 ORM 實現之間做出選擇。
第一個版本的實現
實現偽代碼
1 interface IRepository<TEntity> 2 { 3 void Update(TEntity entity); 4 } 5 6 class EntityFrameworkRepository<TEntity> : IRepository<TEntity> 7 { 8 public void Update(TEntity entity) 9 { 10 throw new EntityFrameworkConcurrentException(); // 可能會拋出這樣的異常,這里的代碼不是十分准確。 11 } 12 } 13 14 class NHibernateRepository<TEntity> : IRepository<TEntity> 15 { 16 public void Update(TEntity entity) 17 { 18 throw new NHibernateConcurrentException(); // 可能會拋出這樣的異常,這里的代碼不是十分准確。 19 } 20 }
有什么問題?
處理並發異常是應用層開發人員的一個非常重要的職責,他們或者選擇自動重試、或者選擇讓用戶重試、甚至允許並發帶來的不一致性,如果使用了上面的接口問題就大了,應用中該攔截哪種並發異常呢?EntityFrameworkConcurrentException?NHibernateConcurrentException?這樣的接口和實現無論如何都達不到:OCP 和 LSP。
將異常作為契約的一部分
實現偽代碼
1 interface IRepository<TEntity> 2 { 3 void Update(TEntity entity); 4 } 5 6 class ConcurrentException : Exception 7 { 8 } 9 10 class EntityFrameworkRepository<TEntity> : IRepository<TEntity> 11 { 12 public void Update(TEntity entity) 13 { 14 try 15 { 16 } 17 catch (EntityFrameworkConcurrentException ex) 18 { 19 throw new ConcurrentException(ex); 20 } 21 } 22 } 23 24 class NHibernateRepository<TEntity> : IRepository<TEntity> 25 { 26 public void Update(TEntity entity) 27 { 28 try 29 { 30 } 31 catch (NHibernateConcurrentException ex) 32 { 33 throw new ConcurrentException(ex); 34 } 35 } 36 }
有什么問題?
目前來說還覺得不錯,如果 C# 編譯器或 CLR 能支持異常契約就好了,Java 雖然支持,但是對於調用者來說又不太友好。
這里給出答案
當異常是契約的一部分時,才需要包裝異常。
可能還會有其它答案,等我再思考思考,朋友們也可以給出一些想法。
微軟的一個反例
MethodBae.Invoke
1 // System.Reflection.TargetInvocationException: 2 // 調用的方法或構造函數引發異常。 3 // 4 // System.MethodAccessException: 5 // 調用方沒有調用此構造函數的權限。 6 // 7 // System.InvalidOperationException: 8 // 聲明此方法的類型是開放式泛型類型。 即,System.Type.ContainsGenericParameters 屬性為聲明類型返回 true。 9 public abstract object Invoke(object obj, BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture);
當方法內部拋出異常時,Invoke 會將內部異常給包裝起來,這明顯不是我們期望的行為,后來微軟的 dynamic 調用 和 CreateDelegate 之后使用 Delegate 調用 都修復了這個問題。
備注
最近在讀第四版的 clr via c#,確實是一部好書,關於異常作為契約部分的想法,和作者產生了很大的共鳴,書中對異常處理的講解非常細致,推薦大家讀一讀這本書。