Index :html
(1)類型語法、內存管理和垃圾回收基礎程序員
(2)面向對象的實現和異常的處理基礎面試
(3)字符串、集合與流數據庫
(4)委託、事件、反射與特性多線程
(5)多線程開發基礎框架
(6)ADO.NET與數據庫開發基礎less
(7)WebService的開發與應用基礎編輯器
在C#中申明一個類型時,只支持單繼承(即繼承一個父類),但支持實現多個接口(Java也是如此)。像C++可能會支持同時繼承自多個父類,但.NET的設計小組認爲這樣的機制會帶來一些弊端,而且沒有必要。ide
首先,看看多繼承有啥好處?多繼承的好處是更加貼近地設計類型。例如,當爲一個圖形編輯器設計帶文本框的矩形類型時,最方便的方法多是這個類型既繼承自文本框類型,又繼承自矩形類型,這樣它就天生地具備輸入文本和繪畫矩形的功能。But,自從C++使用多繼承依賴,就一直存在一些弊端,其中最爲嚴重的仍是所謂的「磚石繼承」帶來的問題,下圖解釋了磚石繼承問題。性能
如上圖所示,磚石繼承問題根源在於最終的子類從不一樣的父類中繼承到了在它看來徹底不一樣的兩個成員,而事實上,這兩個成員又來自同一個基類。鑑於此,在C#/Java中,多繼承的機制已經被完全拋棄,取而代之的是單繼承和多接口實現的機制。衆所周知,接口並不作任何實際的工做,可是卻制定了接口和規範,它定義了特定的類型都須要「作什麼」,而把「怎麼作」留給實現它的具體類型去考慮。也正是由於接口具備很大的靈活性和抽象性,所以它在面向對象的程序設計中更加出色地完成了抽象的工做。
在C#或其餘面嚮對象語言中,重寫、重載和隱藏的機制,是設計高可擴展性的面向對象程序的基礎。
(1)重寫和隱藏
重寫(Override)是指子類用Override關鍵字從新實現定義在基類中的虛方法,而且在實際運行時根據對象類型來調用相應的方法。
隱藏則是指子類用new關鍵字從新實現定義在基類中的方法,但在實際運行時只能根據引用來調用相應的方法。
如下的代碼說明了重寫和隱藏的機制以及它們的區別:
public class Program { public static void Main(string[] args) { // 測試兩者的功能 OverrideBase ob = new OverrideBase(); NewBase nb = new NewBase(); Console.WriteLine(ob.ToString() + ":" + ob.GetString()); Console.WriteLine(nb.ToString() + ":" + nb.GetString()); Console.WriteLine(); // 測試兩者的區別 BaseClass obc = ob as BaseClass; BaseClass nbc = nb as BaseClass; Console.WriteLine(obc.ToString() + ":" + obc.GetString()); Console.WriteLine(nbc.ToString() + ":" + nbc.GetString()); Console.ReadKey(); } } // Base class public class BaseClass { public virtual string GetString() { return "我是基類"; } } // Override public class OverrideBase : BaseClass { public override string GetString() { return "我重寫了基類"; } } // Hide public class NewBase : BaseClass { public new virtual string GetString() { return "我隱藏了基類"; } }
以上代碼的運行結果以下圖所示:
咱們能夠看到:當經過基類的引用去調用對象內的方法時,重寫仍然可以找到定義在對象真正類型中的GetString方法,而隱藏則只調用了基類中的GetString方法。
(2)重載
重載(Overload)是擁有相同名字和返回值的方法卻擁有不一樣的參數列表,它是實現多態的立項方案,在實際開發中也是應用得最爲普遍的。常見的重載應用包括:構造方法、ToString()方法等等;
如下代碼是一個簡單的重載示例:
public class OverLoad { private string text = "我是一個字符串"; // 無參數版本 public string PrintText() { return this.text; } // 兩個int參數的重載版本 public string PrintText(int start, int end) { return this.text.Substring(start, end - start); } // 一個char參數的重載版本 public string PrintText(char fill) { StringBuilder sb = new StringBuilder(); foreach (var c in text) { sb.Append(c); sb.Append(fill); } sb.Remove(sb.Length - 1, 1); return sb.ToString(); } } public class Program { public static void Main(string[] args) { OverLoad ol = new OverLoad(); // 傳入不一樣參數,PrintText的不一樣重載版本被調用 Console.WriteLine(ol.PrintText()); Console.WriteLine(ol.PrintText(2,4)); Console.WriteLine(ol.PrintText('/')); Console.ReadKey(); } }
運行結果以下圖所示:
在C#程序中,構造方法調用虛方法是一個須要避免的禁忌,這樣作到底會致使什麼異常?咱們不妨經過下面一段代碼來看看:
// 基類 public class A { protected Ref my; public A() { my = new Ref(); // 構造方法 Console.WriteLine(ToString()); } // 虛方法 public override string ToString() { // 這裏使用了內部成員my.str return my.str; } } // 子類 public class B : A { private Ref my2; public B() : base() { my2 = new Ref(); } // 重寫虛方法 public override string ToString() { // 這裏使用了內部成員my2.str return my2.str; } } // 一個簡單的引用類型 public class Ref { public string str = "我是一個對象"; } public class Program { public static void Main(string[] args) { try { B b = new B(); } catch (Exception ex) { // 輸出異常信息 Console.WriteLine(ex.GetType().ToString()); } Console.ReadKey(); } }
下面是運行結果,異常信息是空指針異常?
(1)要解釋這個問題產生的緣由,咱們須要詳細地瞭解一個帶有基類的類型(事實上是System.Object,全部的內建類型都有基類)被構造時,全部構造方法被調用的順序。
在C#中,當一個類型被構造時,它的構造順序是這樣的:
執行變量的初始化表達式 → 執行父類的構造方法(須要的話)→ 調用類型本身的構造方法
咱們能夠經過如下代碼示例來看看上面的構造順序是如何體現的:
public class Program { public static void Main(string[] args) { // 構造了一個最底層的子類類型實例 C newObj = new C(); Console.ReadKey(); } } // 基類類型 public class Base { public Ref baseString = new Ref("Base 初始化表達式"); public Base() { Console.WriteLine("Base 構造方法"); } } // 繼承基類 public class A : Base { public Ref aString = new Ref("A 初始化表達式"); public A() : base() { Console.WriteLine("A 構造方法"); } } // 繼承A public class B : A { public Ref bString = new Ref("B 初始化表達式"); public B() : base() { Console.WriteLine("B 構造方法"); } } // 繼承B public class C : B { public Ref cString = new Ref("C 初始化表達式"); public C() : base() { Console.WriteLine("C 構造方法"); } } // 一個簡單的引用類型 public class Ref { public Ref(string str) { Console.WriteLine(str); } }
調試運行,能夠看到派生順序是:Base → A → B → C,也驗證了剛剛咱們所提到的構造順序。
上述代碼的整個構造順序以下圖所示:
(2)瞭解完產生本問題的根本緣由,反觀虛方法的概念,當一個虛方法被調用時,CLR老是根據對象的實際類型來找到應該被調用的方法定義。換句話說,當虛方法在基類的構造方法中被調用時,它的類型讓然保持的是子類,子類的虛方法將被執行,可是這時子類的構造方法卻尚未完成,任何對子類未構形成員的訪問都將產生異常。
如何避免這類問題呢?其根本方法就在於:永遠不要在非葉子類的構造方法中調用虛方法。
這是一個被問爛的問題,在C#中能夠經過sealed關鍵字來申明一個不可被繼承的類,C#將在編譯階段保證這一機制。可是,繼承式OO思想中最重要的一環,可是否想過繼承也存在一些問題呢?在設計一個會被繼承的類型時,每每須要考慮再三,下面例舉了常見的一些類型被繼承時容易產生的問題:
(1)爲了讓派生類型能夠順利地序列化,非葉子類須要實現恰當的序列化方法;
(2)當非葉子類實現了ICloneable等接口時,意味着全部的子類都被迫須要實現接口中定義的方法;
(3)非葉子類的構造方法不能調用虛方法,並且更容易產生不能預計的問題;
鑑於以上問題,在某些時候沒有派生須要的類型都應該被顯式地添加sealed關鍵字,這是避免繼承帶來不可預計問題的最有效辦法。
相信閱讀本文的園友都已經養成了try-catch的習慣,但對於異常的捕捉和處理可能並不在乎。確實,直接捕捉全部異常的基類:Exception 使得程序方便易懂,但有時這樣的捕捉對於業務處理沒有任何幫助,對於特殊異常應該採用特殊處理可以更好地引導規劃程序流程。
下面的代碼演示了一個對於不一樣異常進行處理的示例:
public class Program { public static void Main(string[] args) { Program p = new Program(); p.RiskWork(); Console.ReadKey(); } public void RiskWork() { try { // 一些可能會出現異常的代碼 } catch (NullReferenceException ex) { HandleExpectedException(ex); } catch (ArgumentException ex) { HandleExpectedException(ex); } catch (FileNotFoundException ex) { HandlerError(ex); } catch (Exception ex) { HandleCrash(ex); } } // 這裏處理預計可能會發生的,不屬於錯誤範疇的異常 private void HandleExpectedException(Exception ex) { // 這裏能夠藉助log4net寫入日誌 Console.WriteLine(ex.Message); } // 這裏處理在系統出錯時可能會發生的,比較嚴重的異常 private void HandlerError(Exception ex) { // 這裏能夠藉助log4net寫入日誌 Console.WriteLine(ex.Message); // 嚴重的異常須要拋到上層處理 throw ex; } // 這裏處理可能會致使系統崩潰時的異常 private void HandleCrash(Exception ex) { // 這裏能夠藉助log4net寫入日誌 Console.WriteLine(ex.Message); // 關閉當前程序 System.Threading.Thread.CurrentThread.Abort(); } }
(1)如代碼所示,針對特定的異常進行不一樣的捕捉一般頗有意義,真正的系統每每要針對不一樣異常進行復雜的處理。異常的分別處理是一種好的編碼習慣,這要求程序員在編寫代碼的時候充分估計到全部可能出現異常的狀況,固然,不管考慮得如何周到,最後都須要對異常的基類Exception進行捕捉,這樣才能保證全部的異常都不會被隨意地拋出。
(2)除此以外,除了在必要的時候寫try-catch,不少園友更推薦使用框架層面提供的異常捕捉方案,以.NET爲例:
WinForm,能夠這樣寫:AppDomain.CurrentDomain.UnhandledException +=new UnhandledExceptionEventHandler(UnhandledExceptionFunction);
你們都知道,一般在編譯程序時能夠選擇Bebug版本仍是Release版本,編譯器將會根據」調試「和」發佈「兩個不一樣的出發點去編譯程序。在Debug版本中,全部Debug類的斷言(Assert)語句都會獲得保留,相反在Release版本中,則會被統統刪除。這樣的機制有助於咱們編寫出方便調試同時又不影響正式發佈的程序代碼。
But,單純的診斷和斷言可能並不能徹底知足測試的需求,有時可能會須要大批的代碼和方法去支持調試和測試,這個時候就須要用到Conditional特性。Conditional特性用於編寫在某個特定版本中運行的方法,一般它編寫一些在Debug版本中支持測試的方法。當版本不匹配時,編譯器會把Conditional特性的方法內容置爲空。
下面的一段代碼演示了Conditional特性的使用:
//含有兩個成員,生日和身份證 //身份證的第6位到第14位必須是生日 //身份證必須是18位 public class People { private DateTime _birthday; private String _id; public DateTime Birthday { set { _birthday = value; if (!Check()) throw new ArgumentException(); } get { Debug(); return _birthday; } } public String ID { set { _id = value; if (!Check()) throw new ArgumentException(); } get { Debug(); return _id; } } public People(String id, DateTime birthday) { _id = id; _birthday = birthday; Check(); Debug(); Console.WriteLine("People實例被構造了..."); } // 只但願在DEBUG版本中出現 [Conditional("DEBUG")] protected void Debug() { Console.WriteLine(_birthday.ToString("yyyy-MM-dd")); Console.WriteLine(_id); } //檢查是否符合業務邏輯 //在全部版本中都須要 protected bool Check() { if (_id.Length != 18 || _id.Substring(6, 8) != _birthday.ToString("yyyyMMdd")) return false; return true; } } public class Program { public static void Main(string[] args) { try { People p = new People("513001198811290215", new DateTime(1988, 11, 29)); p.ID = "513001198811290215"; } catch (ArgumentException ex) { Console.WriteLine(ex.GetType().ToString()); } Console.ReadKey(); } }
下圖則展現了上述代碼在Debug版本和Release版本中的輸出結果:
①Debug版本:
②Release版本:
Conditional機制很簡單,在編譯的時候編譯器會查看編譯狀態和Conditional特性的參數,若是二者匹配,則正常編譯。不然,編譯器將簡單地移除方法內的全部內容。
咱們常常會面臨一些類型轉換的工做,其中有些是肯定能夠轉換的(好比將一個子類類型轉爲父類類型),而有些則是嘗試性的(好比將基類引用的對象轉換成子類)。當執行常識性轉換時,咱們就應該作好捕捉異常的準備。
當一個不正確的類型轉換髮生時,會產生InvalidCastException異常,有時咱們會用try-catch塊作一些嘗試性的類型轉換,這樣的代碼沒有任何錯誤,可是性能卻至關糟糕,爲何呢?異常是一種耗費資源的機制,每當異常被拋出時,異常堆棧將會被創建,異常信息將被加載,而一般這些工做的成本相對較高,而且在嘗試性類型轉換時,這些信息都沒有意義。
So,在.NET中提供了另一種語法來進行嘗試性的類型轉換,那就是關鍵字 is 和 as 所作的工做。
(1)is 只負責檢查類型的兼容性,並返回結果:true 和 false。→ 進行類型判斷
public static void Main(string[] args) { object o = new object(); // 執行類型兼容性檢查 if(o is ISample) { // 執行類型轉換 ISample sample = (ISample)o; sample.SampleShow(); } Console.ReadKey(); }
(2)as 不只負責檢查兼容性還會進行類型轉換,並返回結果,若是不兼容則返回 null 。→ 用於類型轉型
public static void Main(string[] args) { object o = new object(); // 執行類型兼容性檢查 ISample sample = o as ISample; if(sample != null) { sample.SampleShow(); } Console.ReadKey(); }
二者的共同之處都在於:不會拋出異常!綜上比較,as 較 is 在執行效率上會好一些,在實際開發中應該量才而用,在只進行類型判斷的應用場景時,應該多使用 is 而不是 as。
(1)朱毅,《進入IT企業必讀的200個.NET面試題》
(2)張子陽,《.NET之美:.NET關鍵技術深刻解析》
(3)王濤,《你必須知道的.NET》