1. 程式人生 > >.NET基礎拾遺(2)面向物件的實現和異常的處理基礎

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

一、面向物件的實現

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》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

.NET基礎拾遺2面向物件實現異常處理基礎

一、面向物件的實現 1.1 C#中的類可以多繼承嗎?   在C#中申明一個型別時,只支援單繼承(即繼承一個父類),但支援實現多個介面(Java也是如此)。像C++可能會支援同時繼承自多個父類,但.NET的設計小組認為這樣的機制會帶來一些弊端,並且沒有必要。   首先,看看多繼承有啥好處?多繼承的

java基礎筆記面向物件

java是一種面向物件的語言 一句話:萬物皆物件 類與物件的區別: 類:是一組相關屬性與行為的集合,抽象概念 物件:是類的具體體現 這是一個person類:裡面 public class Person { private String name; //person類的屬性 in

JavaSE基礎學習——面向物件

1.1 面向物件思想 1.1.1 概述: 我們回想一下,之前我們完成一個需求的步驟:首先是搞清楚我們要做什麼,然後在分析怎麼做,最後我們再程式碼體現。一步一步去實現,而具體的每一步都需要我們去實現和操

Java基礎知識——面向物件

Java基礎知識(四)——面向物件(下) Java8的增強包裝類: 為了解決8種基本資料型別不能當成Object型別變數使用的問題。   JDK1.5提供了自動裝箱(Autoboxing)和自動拆箱·(AotuUnboxing)功能,所謂自動裝箱,就是可以把一個基本型別變數直接賦給對應的包裝

.NET基礎拾遺3字串、集合

一、字串處理 1.1 StringBuilder型別有什麼作用?   眾所周知,在.NET中String是引用型別,具有不可變性,當一個String物件被修改、插入、連線、截斷時,新的String物件就將被分配,這會直接影響到效能。但在實際開發中經常碰到的情況是,一個String物件的最終生成需要經過

.NET基礎拾遺7Web Service的開發與應用基礎

一、SOAP和Web Service的基本概念   Web Service基於SOAP協議,而SOAP本身符合XML語法規範。雖然.NET為Web Service提供了強大的支援,但瞭解其基本機制對於程式設計師來說仍然是必需的。 1.1 神馬是SOAP協議?   SOAP協議的全稱是簡單物件訪問協議

JAVA面向物件程式設計基礎複習面向物件基本概念

從今天開始有計劃的寫一些博文。內容主要涉及JAVA語言、面向物件程式設計、設計模式、android開發(這才是重點嘛)。今天開始寫 JAVA面向物件程式設計基礎複習這個系列的文章。 JAVA面向物件程式設計基礎複習目錄      (二)異常處理與自定義異常      (

TensorFlow實現經典深度學習網路5:TensorFlow實現自然語言處理基礎網路Word2Vec

TensorFlow實現經典深度學習網路(5):TensorFlow實現自然語言處理 基礎網路Word2Vec         迴圈神經網路RNN是在自然語言處理NLP領域最常使用的神經網路結構,和卷積神經網路在影象識別領域的地位相似,影響深遠。而Word2Vec則是將語

30try語句塊異常處理

異常是指存在於執行時的反常行為,這些行為超出了函式正常功能的範圍。 當程式的某部分檢測到一個它無法處理的問題時,需要用到異常處理。異常處理機制為程式中異常檢測和異常處理這兩部分的協作提供支援。在C++語言中,異常處理包括: 一.throw表示式,異常檢測部分使用throw表

熟練使用Lua面向物件:基於table的面向物件實現2

myluaootest.lua –1. 基本原理 local Cal = {} function Cal:New(o) o = o or {} setmetatable(o, self) self.__index = self return o end functio

JAVA基礎42---面向物件程式設計

面向物件 概述            類(class)和物件(object)是面向物件方法的核心概念。 類是對一類事物描述,是抽象的、概念上的定義;物件是實際存在的該類事物的每個個體,

java語言基礎----面向物件的三大特徵

1.面向物件的三大特徵 (1)封裝:隱藏物件的屬性和實現細節,僅對外提供公共訪問方式。 (2)繼承:它可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴充套件。           通過繼承建立的新類稱為“子類”或“派生類”。        

Java面試系列總結 :JavaSE基礎1 面向物件/語法/異常

1. 面向物件都有哪些特性以及你對這些特性的理解 繼承:繼承是從已有類得到繼承資訊建立新類的過程。提供繼承資訊的類被稱為父類(超類、基類);得到繼承資訊的類被稱為子類(派生類)。繼承讓變化中的軟體系統有了一定的延續性,同時繼承也是封裝程式中可變因素的 重要

.NET基礎拾遺5多執行緒開發基礎

一、多執行緒程式設計的基本概念   下面的一些基本概念可能和.NET的聯絡並不大,但對於掌握.NET中的多執行緒開發來說卻十分重要。我們在開始嘗試多執行緒開發前,應該對這些基礎知識有所掌握,並且能夠在作業系統層面理解多執行緒的執行方式。 1.1 作業系統層面的程序和執行緒   (1)程序   程序

.NET基礎拾遺1型別語法基礎記憶體管理基礎

一、基礎型別和語法 1.1 .NET中所有型別的基類是什麼?   在.NET中所有的內建型別都繼承自System.Object型別。在C#中,不需要顯示地定義型別繼承自System.Object,編譯器將自動地自動地為型別新增上這個繼承申明,以下兩行程式碼的作用完全一致: public

.NET基礎拾遺6ADO.NET與資料庫開發基礎

一、ADO.NET和資料庫程式基礎 1.1 安身立命之基本:SQL   SQL語句時操作關係型資料庫的基礎,在開發資料訪問層、除錯系統等工作中十分常用,掌握SQL對於每一個程式設計師(無論是.NET、Java還是C++等)都非常重要。這裡挑選了一個常見的面試題目,來熱熱身。   常見場景:通過SQL

.NET基礎拾遺4委託、事件、反射與特性

一、委託基礎 1.1 簡述委託的基本原理   委託這個概念對C++程式設計師來說並不陌生,因為它和C++中的函式指標非常類似,很多碼農也喜歡稱委託為安全的函式指標。無論這一說法是否正確,委託的的確確實現了和函式指標類似的功能,那就是提供了程式回撥指定方法的機制。   在委託內部,包含了一個指向某個方

.NET Core 小程式開發零基礎系列2——小程式服務通知模板訊息

基於上一篇檔案“.NET Core 小程式開發零基礎系列(1)——開發者啟用並校驗牽手成功”的反映,個人覺得效果很不錯,大家對公眾號開發還是有很大需求的,同時也收到了很多同學的問題,後面我也會通過實戰性文章慢慢的表現出來 ,讓大家更容易吃得透一些。在這裡特別感謝

Asp.net Security框架2

默認 隨機 async 技術分享 希望 win 認證 用戶認證 uget Asp.net 的Security框架除了提供Cookies,OAuth,ActiveDirectory等多個用戶認證實現,基本上已經滿足業務項目的開發需要了。 當需要實現OAuth2.0服務器端實現

ArcGIS基礎2——如何將模型導成py文件?

src 代碼 使用 images 友好 編程 基礎篇 {} left Python腳本使用很方便,熟悉一點編程的,了解一點Python的,都可以在ArcGIS中嘗試用Python進行數據處理。把模型導出成py需要註意三個問題: 一是格式,Python對縮進很敏感,不使用{}