.NET基礎拾遺(2)面向對象的實現和異常的處理基礎


 Index :

(1)類型語法、內存管理和垃圾回收基礎

(2)面向對象的實現和異常的處理基礎

(3)字符串、集合與流

(4)委托、事件、反射與特性

(5)多線程開發基礎

(6)ADO.NET與數據庫開發基礎

(7)WebService的開發與應用基礎

一、面向對象的實現

1.1 C#中的類可以多繼承嗎?

  在C#中申明一個類型時,只支持單繼承(即繼承一個父類),但支持實現多個接口(Java也是如此)。像C++可能會支持同時繼承自多個父類,但.NET的設計小組認為這樣的機制會帶來一些弊端,並且沒有必要。

  首先,看看多繼承有啥好處?多繼承的好處是更加貼近地設計類型。例如,當為一個圖形編輯器設計帶文本框的矩形類型時,最方便的方法可能是這個類型既繼承自文本框類型,又繼承自矩形類型,這樣它就天生地具有輸入文本和繪畫矩形的功能。But,自從C++使用多繼承依賴,就一直存在一些弊端,其中最為嚴重的還是所謂的“磚石繼承”帶來的問題,下圖解釋了磚石繼承問題。

  如上圖所示,磚石繼承問題根源在於最終的子類從不同的父類中繼承到了在它看來完全不同的兩個成員,而事實上,這兩個成員又來自同一個基類。鑒於此,在C#/Java中,多繼承的機制已經被徹底拋棄,取而代之的是單繼承和多接口實現的機制。眾所周知,接口並不做任何實際的工作,但是卻制定了接口和規范,它定義了特定的類型都需要“做什么”,而把“怎么做”留給實現它的具體類型去考慮。也正是因為接口具有很大的靈活性和抽象性,因此它在面向對象的程序設計中更加出色地完成了抽象的工作。

1.2 C#中重寫、重載和隱藏是什么鬼?

  在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 "我隱藏了基類";
        }
    }
View Code

  以上代碼的運行結果如下圖所示:

  我們可以看到:當通過基類的引用去調用對象內的方法時,重寫仍然能夠找到定義在對象真正類型中的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();
        }
    }
View Code

  運行結果如下圖所示:

1.3 為什么不能在構造方法中調用虛方法?

  在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();
        }
    }
View Code

  下面是運行結果,異常信息是空指針異常?

  (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);
        }
    }
View Code

  調試運行,可以看到派生順序是:Base → A → B → C,也驗證了剛剛我們所提到的構造順序。

  上述代碼的整個構造順序如下圖所示:

  (2)了解完產生本問題的根本原因,反觀虛方法的概念,當一個虛方法被調用時,CLR總是根據對象的實際類型來找到應該被調用的方法定義。換句話說,當虛方法在基類的構造方法中被調用時,它的類型讓然保持的是子類,子類的虛方法將被執行,但是這時子類的構造方法卻還沒有完成,任何對子類未構造成員的訪問都將產生異常

  如何避免這類問題呢?其根本方法就在於:永遠不要在非葉子類的構造方法中調用虛方法

1.4 C#如何聲明一個類不能被繼承?

  這是一個被問爛的問題,在C#中可以通過sealed關鍵字來申明一個不可被繼承的類,C#將在編譯階段保證這一機制。但是,繼承式OO思想中最重要的一環,但是否想過繼承也存在一些問題呢?在設計一個會被繼承的類型時,往往需要考慮再三,下面例舉了常見的一些類型被繼承時容易產生的問題:

  (1)為了讓派生類型可以順利地序列化,非葉子類需要實現恰當的序列化方法;

  (2)當非葉子類實現了ICloneable等接口時,意味着所有的子類都被迫需要實現接口中定義的方法;

  (3)非葉子類的構造方法不能調用虛方法,而且更容易產生不能預計的問題;

  鑒於以上問題,在某些時候沒有派生需要的類型都應該被顯式地添加sealed關鍵字,這是避免繼承帶來不可預計問題的最有效辦法。

二、異常的處理

2.1 如何針對不同的異常進行捕捉?

  相信閱讀本文的園友都已經養成了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();
        }
    }
View Code

  (1)如代碼所示,針對特定的異常進行不同的捕捉通常很有意義,真正的系統往往要針對不同異常進行復雜的處理。異常的分別處理是一種好的編碼習慣,這要求程序員在編寫代碼的時候充分估計到所有可能出現異常的情況,當然,無論考慮得如何周到,最后都需要對異常的基類Exception進行捕捉,這樣才能保證所有的異常都不會被隨意地拋出。

  (2)除此之外,除了在必要的時候寫try-catch,很多園友更推薦使用框架層面提供的異常捕捉方案,以.NET為例:

  • WinForm,可以這樣寫:AppDomain.CurrentDomain.UnhandledException +=new UnhandledExceptionEventHandler(UnhandledExceptionFunction);

  • ASP.NET WebForm,可以在Application_Error()方法里捕獲異常
  • ASP.NET MVC,可以寫ExceptionFilter
  • ASP.NET WebAPI,可以寫ExceptionHandler

2.2 如何使用Conditional特性?

  大家都知道,通常在編譯程序時可以選擇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();
        }
    }
View Code

  下圖則展示了上述代碼在Debug版本和Release版本中的輸出結果:

  ①Debug版本:

  

  ②Release版本:

  

  Conditional機制很簡單,在編譯的時候編譯器會查看編譯狀態和Conditional特性的參數,如果兩者匹配,則正常編譯。否則,編譯器將簡單地移除方法內的所有內容。

2.3 如何避免類型轉換時的異常?

  我們經常會面臨一些類型轉換的工作,其中有些是確定可以轉換的(比如將一個子類類型轉為父類類型),而有些則是嘗試性的(比如將基類引用的對象轉換成子類)。當執行常識性轉換時,我們就應該做好捕捉異常的准備。

  當一個不正確的類型轉換發生時,會產生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();
    }
View Code

  (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();
    }
View Code

  兩者的共同之處都在於:不會拋出異常!綜上比較,as 較 is 在執行效率上會好一些,在實際開發中應該量才而用,在只進行類型判斷的應用場景時,應該多使用 is 而不是 as。

參考資料

(1)朱毅,《進入IT企業必讀的200個.NET面試題》

(2)張子陽,《.NET之美:.NET關鍵技術深入解析》

(3)王濤,《你必須知道的.NET》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM