未將對象引用到執行個體,即NullReferenceException異常,我相信這是c#編程中最常見的錯誤之一,至少我在做項目的過程中,有很多時候都會拋出這個異常。每當這個異常出現的時候,我都會頭皮一緊,因為以我的經驗總結,一般情況下不會出現這個錯誤,但是一旦出現這個錯誤往往是比較難排查的,特別是代碼量較大的時候,而等找到bug時,往往又會出現糾結狀態,因為NullReferenceException異常經常和程式的邏輯緊密相連,這就意味著不是你的程式寫錯了,而很有可能是你的編程邏輯設計的不夠合理。
當然,NullReferenceException異常的解決方案也非常的簡單,只要在異常處加上類似下面的代碼就可以了
1 if(obj != null)2 {3 //some actions 4 }
意思就是在使用這個引用之前檢查引用是否為空白。但事實並非這麼簡單,正如上面說的,NullReferenceException異常往往和代碼設計的邏輯是相關的,特別是代碼量大的時候,一旦發現這個錯誤,往往會導致這一段代碼的重構!
而且更重要的是,有時候在一段代碼裡,需要根據不同的情況來決定一個引用是否為null,或者是確實指向一個真實的對象。這種情況是最糾結的,因為這就意味著每個需要使用到這個引用的地方都要加上上述的代碼!
作為一個程式員來說,這是不能容忍的!
因為一個簡單的 if(obj != null) 的代碼居然重複出現無數次,一篇代碼裡出現重複模式的代碼,我認識這可以看做是程式員的失敗!如何消除 if(obj != null) 這個模式呢?一些大牛曾經講過:
任何時候都不應該使用null,你應該專門設計一個空類來代替null。
至於null指標的“罪與罰”,我想每個程式員都有自己的見解吧,在我看來,null指標可以列入編程史上最大的創新,同時也可列入最大的罪惡之源!
什麼叫使用空類來代替null,這裡就以日誌Logger類作為樣本來說明一下(當然,實際項目中你可以使用 log4net,或者其他IOC架構來實現日誌記錄)
1 public class Logger 2 { 3 private class EmptyLog : Logger 4 { 5 public override void Log(string msg){ } 6 public override void LogErr(string errMsg) { } 7 public override void LogErr(Exception ex) { } 8 } 9 static Logger()10 {11 Empty = new EmptyLog();12 }13 public static Logger Empty { get; private set; }14 15 readonly TextWriter writer;16 private Logger() { }17 public Logger(TextWriter writer)18 {19 this.writer = writer;20 }21 public virtual void Log(string msg)22 {23 writer.WriteLine(DateTime.Now);24 writer.WriteLine(msg);25 writer.WriteLine();26 }27 public virtual void LogErr(string errMsg)28 {29 writer.WriteLine("error");30 Log(errMsg);31 }32 public virtual void LogErr(Exception ex)33 {34 writer.WriteLine(string.Format("error<{0}>",ex.GetType().FullName));35 Log(ex.Message);36 }37 }
下面是測試代碼及測試結果:
1 [STAThread] 2 static void Main() 3 { 4 var log = Logger.Empty; 5 log.Log("hellow world"); 6 log.LogErr("error"); 7 log.LogErr(new NullReferenceException()); 8 log = new Logger(Console.Out); 9 log.Log("hellow world");10 log.LogErr("this is a error message");11 log.LogErr(new NullReferenceException());12 }
可見,任何時候都不應該使用null,你應該專門設計一個空類來代替null,這句話的意思就是不要給一個引用賦值null,而是在初始化時要引用指向一個空類樣本!
這種方法確實不錯,至少是避免了 if(obj != null) 這種單調乏味的模式,但是我覺得還是有點不爽,因為那個空類純粹就是一個資源浪費,既然不需要記錄日誌的地方,那就應該讓引用指向null才對,表示什麼都沒有,而這種方法意味著即使你什麼都不做,但是也要消耗一部分資源來供給這個空類使用!這種不必要的資源浪費和重複模式一樣,都是我不能容忍的。於是我設計了一個方法:
利用擴充方法來避免 if(obj != null) 模式!
擴充方法和Linq是我最喜歡的兩個C#的語言特性,為什麼Common Lisp那麼強大,就是因為程式員可以自行對Lisp文法進行擴充,定製自己想要的文法,同樣C#重點的擴充方法也可以實現擴充文法的功能,下面就是我用擴充方法重新設計的Logger類:
1 public class Logger 2 { 3 readonly TextWriter writer; 4 public Logger(TextWriter writer) 5 { 6 this.writer = writer; 7 } 8 internal void Log(string msg) 9 {10 writer.WriteLine(DateTime.Now);11 writer.WriteLine(msg);12 writer.WriteLine();13 }14 internal void LogErr(string errMsg)15 {16 writer.WriteLine("error");17 Log(errMsg);18 }19 internal void LogErr(Exception ex)20 {21 writer.WriteLine(string.Format("error<{0}>",ex.GetType().FullName));22 Log(ex.Message);23 }24 }25 public static class LoggerHelper26 {27 public static void Log(this Logger logger, string msg)28 {29 if (logger != null)30 logger.Log(msg);31 }32 public static void LogErr(this Logger logger, string errMsg)33 {34 if (logger != null)35 logger.LogErr(errMsg);36 }37 public static void LogErr(this Logger logger, Exception ex)38 {39 if (logger != null)40 logger.LogErr(ex);41 }42 }
下面是測試代碼及測試結果:
1 [STAThread] 2 static void Main() 3 { 4 Logger log = null; 5 log.Log("hellow world"); 6 log.LogErr("error"); 7 log.LogErr(new NullReferenceException()); 8 log = new Logger(Console.Out); 9 log.Log("hellow world");10 log.LogErr("this is a error message");11 log.LogErr(new NullReferenceException());12 }
從測試代碼可以開出,即使變數log為null時,代碼依然可以正常運行,因為我把 if(obj != null) 模式 利用擴充方法進行了封裝,這樣就可以放心的使用log了,不用擔心log是否為null嗎,而且在不需要記錄日誌的時候,直接將log賦值為null即可,不浪費任何資源。
這就是我關於解決“未將對象引用到執行個體”的方法,如果大家有其它好的辦法,歡迎在這裡進行交流!