动态

详情 返回 返回

C# 中面向對象技術概述 - 动态 详情

在 C# 中,類型(class、struct 或 record)的定義就像是一份藍圖,它規定了該類型能夠做什麼。對象基本上就是根據這份藍圖分配和配置的一塊內存。本文概述了這些藍圖及其特性。

封裝(Encapsulation)

封裝有時被稱為面向對象編程的第一大支柱或原則。class 或 struct 可以指定其每個成員對於類或結構體外部的代碼的可訪問性。對於不打算供類或程序集外部的使用者使用的成員,將其隱藏起來,以限制出現編碼錯誤或惡意利用的可能性。

成員(Members)

類型成員包括所有方法、字段、常量、屬性和事件。在 C# 中,不存在像某些其他語言中的全局變量或方法。即使程序的入口點 Main 方法,也必須聲明在類或結構體中(當您使用頂級語句時會隱式聲明)。

以下列表包含了可以在類、結構體或記錄中聲明的各種類型的成員。

  • 字段(Fields)
  • 常量(Constants)
  • 屬性(Property)
  • 方法(Method)
  • 構造函數(Constructors)
  • 事件(Events)
  • 終結器(Finalizers)
  • 索引器(Indexers)
  • 運算符(Operators)
  • 嵌套類型(Nested Types)

可訪問性(Accessibility)

有些方法和屬性是為從 class 或 struct 外部的代碼(即客户端代碼)調用或訪問而設計的。而其他方法和屬性則僅適用於 class 或 struct 本身內部使用。限制代碼的可訪問性非常重要,這樣只有預期的客户端代碼才能訪問到這些代碼。您可以通過使用以下訪問修飾符來指定類型及其成員對客户端代碼的可訪問性:

  • public - 公共的;公開的
  • protected - 受保護的
  • internal - 內部的
  • protected internal - 受保護的內部的
  • private - 私有的
  • private protected - 私有的受保護的

默認的訪問權限是 private。

繼承(Inheritance)

class(但不包括 struct)支持繼承的概念。從另一個 class(稱為基類)派生出來的 class 會自動包含基類的所有 public、protected 和 internal 成員,但不包括其構造函數和終結器。

class 可以被聲明為抽象類(abstract),這意味着該類的一個或多個方法沒有實現。儘管抽象類不能直接被實例化,但它們可以作為其他類的基類,這些派生類會提供缺失的實現。class 也可以被聲明為密封類(sealed),以防止其他類繼承自它們。

接口(Interfaces)

class、struct 和 record 可以實現多個接口。要從一個接口實現,意味着該類型要實現該接口中定義的所有方法。

通用類型(Generic Types)

class、struct 和 record 都可以通過一個或多個類型參數(type parameters)來定義。客户端代碼在創建該類型實例時需提供類型信息。例如,System . Collections . Generic 命名空間中的 List < T > 類就定義了一個類型參數。客户端代碼通過創建 List < string > 或 List < int > 的實例來指定列表所存儲的類型。

靜態類型(Static Types)

class(但不包括 struct 或 record)可以被聲明為 static(靜態)的。static class 只能包含 static 成員,並且不能使用 “new” 關鍵字進行實例化。程序加載時,該類的一個副本會被加載到內存中,其成員通過類名進行訪問。class、struct 和 record 都可以包含 static 成員。

嵌套類型(Nested Types)

一個 class、struct 或 record 可以嵌套在另一個 class、struct 或 record 之中。

分部類型(Partial Types)

您可以在一個代碼文件中定義 class、struct 或 method(方法)的一部分,而在另一個單獨的代碼文件中定義另一部分。

對象初始化器(Object Initializers)

您可以通過為對象的屬性賦值的方式來實例化並初始化 class 或 struct 對象,以及對象集合。

匿名類型(Anonymous Types)

在不方便或沒有必要創建命名類的情況下,您可以使用匿名類型。命名的數據成員可定義匿名類型。

擴展成員(Extension Members)

您可以通過創建一個單獨的類型來 “擴展” 一個類,而無需創建派生類。該類型包含一些方法,這些方法可以像屬於原始類型一樣被調用。

隱式類型局部變量

在 class 或 struct 的方法中,您可以使用隱式類型來指示編譯器在編譯時確定變量的類型。

記錄(record)

您可以將 record 修飾符添加到 class 或 struct 中。record 是一種具有內置值比較行為的類型。record(無論是 record class 還是 record struct)具有以下特性:

  • 用於創建具有不可變屬性的引用類型的簡潔語法。
  • 值相等性。如果兩個 record 類型的變量具有相同的類型,並且對於每個字段,兩個 record 中的值都相等,則這兩個變量就是相等的。class 使用引用相等性:如果兩個 class 類型的變量引用的是同一個對象,則它們就是相等的。
  • 非破壞性修改的簡潔語法。使用 “with” 表達式,您可以創建一個新 record 實例,該實例是現有實例的副本,但其中指定的屬性值已進行了更改。
  • 內置的顯示格式化。ToString 方法會打印 record 類型名稱以及公共屬性的名稱和值。
  • record class 中的繼承層次結構支持。record class 支持繼承。record struct 不支持繼承。

對象 - 創建各種類型的實例

class 或 struct 的定義就像是一個藍圖,它明確了該類型所能實現的功能。一個對象本質上就是一段已分配並根據藍圖進行配置的內存塊。程序可以創建許多相同類別的對象。對象也被稱為實例,它們可以存儲在命名變量中,或者存儲在數組或集合中。客户端代碼是使用這些變量來調用對象的方法並訪問其公共屬性的代碼。在諸如 C# 這樣的面嚮對象語言中,一個典型的程序由多個相互動態交互的對象組成。

注意:static 類型的行為與這裏所描述的有所不同。

struct 實例與 class 實例

由於 class 是引用類型,class 對象的變量會保存指向託管堆中該對象地址的引用。如果將另一個相同類型的變量賦值給第一個變量,那麼這兩個變量都將指向該地址處的對象。

類的實例是通過使用 “new” 運算符來創建的。在下面的示例中,Ren 是類型名稱,而 r1 和 r2 則是該類型的實例,即對象。

private static void Main(string[] args)
    {
        Lei人 NvHai = new ( "蘇小華" , 16 );
        Console . WriteLine ( $"女孩姓名 = {NvHai . z姓名},年齡 = {NvHai . z年齡}" );

        Lei人 NvHai2 = NvHai;
        NvHai2 . z姓名 = "程曉蕾";
        NvHai2 . z年齡 = 18;
        Console . WriteLine ( $"女孩 2 姓名 = {NvHai2 . z姓名},年齡 = {NvHai2 . z年齡}" );
        Console . WriteLine ( $"女孩姓名 = {NvHai . z姓名},年齡 = {NvHai . z年齡}" );
        }

public class Lei人
    {
        public string z姓名 { get; set; }
        public int z年齡 { get; set; }

        public Lei人 ( string 姓名 , int 年齡)
            {
                z姓名 = 姓名;
                z年齡 = 年齡;
            }
    }

輸出:
女孩姓名 = 蘇小華,年齡 = 16
女孩 2 姓名 = 程曉蕾,年齡 = 18
女孩姓名 = 程曉蕾,年齡 = 18

因為 struct 是值類型,所以 struct 變量會保存整個對象的副本。struct 的實例也可以通過使用 new 操作符來創建,但並非必須這樣做,如下面的示例所示:

private static void Main(string[] args)
    {
        JG人 r1 = new ( "方麗麗" , 21 );
        Console . WriteLine ( $"第一個人:{r1 . 姓名},芳齡:{r1 . 年齡}" );
        JG人 r2 = r1;
        r2 . 姓名 = "張曉琳";
        r2 . 年齡 = 22;
        Console . WriteLine ( $"第二個人:{r2 . 姓名},芳齡:{r2 . 年齡}" );
        Console . WriteLine ( $"第一個人:{r1 . 姓名},芳齡:{r1 . 年齡}" );
    }

public struct JG人
    {
        public string 姓名;
        public int 年齡;
        public JG人 ( string 姓名 , int 年齡 )
            {
                this . 姓名 = 姓名;
                this . 年齡 = 年齡;
            }
    }

輸出:
第一個人:方麗麗,芳齡:21
第二個人:張曉琳,芳齡:22
第一個人:方麗麗,芳齡:21

r1 和 r2 的內存均分配在線程棧中。該內存會與其所聲明的類型或方法一同被回收。這就是為什麼 struct 在賦值時會被複制的原因之一。相比之下,為 class 實例分配的內存會由通用語言運行時(CLR)在對象的所有引用都超出作用域後自動回收(即進行垃圾回收)。與 C++ 不同,無法像在 C++ 中那樣確定性地銷燬 class 對象。

注意:在通用語言運行時(CLR)中,對託管堆上的內存分配和釋放進行了高度優化。在大多數情況下,在堆上分配 class 實例與在棧上分配 struct 實例在性能成本方面沒有顯著差異。

Object 的標識與值相等性

當您比較兩個對象的相等性時,您首先必須區分您是想知道這兩個變量在內存中是否代表同一個對象,還是它們的某些字段的值是否相等。如果您打算比較值,您必須考慮對象是值類型(structs)的實例還是引用類型(類(classes)、委託(delegates)、數組(arrays))。

  • 若要確定兩個 class 實例是否指向內存中的同一位置(這意味着它們具有相同的標識),請使用 static 的 Object . ReferenceEquals 方法(System . Object 是所有值類型和引用類型的隱式基類,包括用户自定義的 struct 和 class)。
  • ValueType . Equals 方法默認情況下會判斷兩個 struct 實例中的實例字段是否具有相同的值。由於所有 struct 都隱式地繼承自 System . ValueType 類,因此您可以像下面的示例那樣直接在您的對象上調用該方法:

    JG人 r1 = new ( "方麗麗" , 21 );
    JG人 r2 = new ( "" , 24 );
    r2 . 姓名 = "方麗麗";
    r2 . 年齡 = 21;
    if ( r2 . Equals ( r1 ) )
      Console . WriteLine ( "兩個人具有相同的值。" );

    默認情況下,System . ValueType 類的 Equals 方法在某些情況下會使用 boxing(裝箱)和 reflection(反射)操作。record 是引用類型,它們使用值語義來實現等值。

  • 要確定兩個 class 實例中字段的值是否相等,您可以使用 Equals 方法或 == 運算符。但僅在該類已重寫或重載了這些方法,併為該類型對象提供了自定義的 “相等性” 定義時,才使用它們。該類還可能實現了 IEquatable < T > 接口或 IEqualityComparer < T > 接口。這兩個接口都提供了可用於測試值相等性的方法。在設計自己重寫 Equals 的類時,請務必遵循 “如何為類型定義值相等性” 以及 “Object . Equals ( Object )” 中所述的準則。

繼承——通過派生類型來實現更特定的行為

繼承、封裝和多態性是面向對象編程的三大主要特徵之一。繼承使您能夠創建新的類,這些新類可以複用、擴展和修改其他類中定義的行為。被繼承的成員所在的類稱為基類,而繼承這些成員的類稱為派生類。派生類只能有一個直接的基類。然而,繼承是傳遞性的。如果 LeiC 從 LeiB 派生,而 LeiB 又從 LeiA 派生,那麼 LeiC 就會繼承 LeiB 和 LeiA 中聲明的成員。

注意:struct 不支持繼承,但它們可以實現接口。

從概念上講,派生類是對基類的特定化。例如,如果有一個基類 “動物”,那麼可能會有一個名為 “哺乳動物” 的派生類,還有一個名為 “爬行動物” 的派生類。哺乳動物是動物的一種,而爬行動物也是動物的一種,但每個派生類都代表了基類的不同特化形式。

接口(interface)聲明可以為其成員定義默認實現。這些實現會被派生接口繼承,並且也會被實現這些接口的 class 繼承。

當你定義一個 class 要從另一個 class 派生時,派生類會隱式地獲得基類的所有成員(除了其構造函數和終結器)。派生類可以複用基類中的代碼,而無需重新實現這些代碼。你可以在派生類中添加更多的成員。派生類擴展了基類的功能。

以下示例展示了 “Lei工作項” 這一類,它代表了某個業務流程中的工作項目。與所有類一樣,它從 “System . Object” 派生而來,並繼承了其所有方法。工作項自身還添加了 6 個成員。這些成員包括一個構造函數,因為構造函數不會被繼承。類 “Lei變更請求” 從 “Lei工作項” 派生而來,並代表一種特定類型的工作項。“Lei變更請求” 在它從 “Lei工作項” 和 “Object” 繼承的成員基礎上又添加了另外 2 個成員。它必須添加自己的構造函數,並且還添加了 “原始工作項 ID” 屬性。屬性 “原始工作項 ID” 使 “變更請求” 實例能夠與 “Lei變更請求” 求所適用的原始 “Lei工作項” 相關聯。

以下示例展示了前一圖示中所展示的類關係在 C# 中是如何表達的。該示例還展示了 “Lei工作項” 如何重寫虛擬方法 Object . ToString,以及 “Lei變更請求” 類如何繼承 “Lei工作項” 對該方法的實現。第一塊代碼定義了這些類:

public class Lei工作項
    {
        /// <summary>
        /// static 字段 ID當前 用於存儲最後創建的 Lei工作項 的作業 ID
        /// </summary>
        private static int ID當前;

        /// <summary>
        /// “ID當前”是一個 static 字段。每次創建一個新的“工作項”實例時,該字段的值都會自動遞增
        /// </summary>
        /// <returns>表示下一個 ID 的整數</returns>
        protected static int ID下一個 ( ) => ++ ID當前;

        // 屬性:

        /// <summary>
        /// 當前工作項的 ID
        /// </summary>
        protected int ID { get; set; }

        /// <summary>
        /// 當前工作項的標題
        /// </summary>
        protected string 標題 { get; set; }

        /// <summary>
        /// 當前工作項的説明
        /// </summary>
        protected string 説明 { get; set; }

        /// <summary>
        /// 當前工作項的工作時長
        /// </summary>
        protected TimeSpan 工作時長 { get; set; }

        /// <summary>
        /// 默認構造函數。如果派生類未顯式調用基類的構造函數,則系統會自動調用默認構造函數
        /// </summary>
        public Lei工作項 ( )
            {
                ID = 0;
                標題 = "默認標題";
                説明 = "默認説明";
                工作時長 = new ( );
            }

        /// <summary>
        /// 提供三個參數的構造函數
        /// </summary>
        /// <param name="標題">工作項的標題</param>
        /// <param name="説明">工作項的説明</param>
        /// <param name="工作時長">工作項的工作時長</param>
        public Lei工作項 ( string 標題 , string 説明 , TimeSpan 工作時長 )
            {
                this . ID = ID下一個 ( );
                this . 標題 = 標題;
                this . 説明 = 説明;
                this . 工作時長 = 工作時長;
            }

        /// <summary>
        /// static 構造函數用於初始化靜態成員“ID當前”。此構造函數會在創建任何“工作項”或“變更請求”的實例之前,或者在引用“ID當前”之前自動執行一次
        /// </summary>
        static Lei工作項 ( ) => ID當前 = 0;

        /// <summary>
        /// “FF更新”方法允許您對現有的“工作項”對象的標題和工作時長進行修改
        /// </summary>
        /// <param name="標題">新的工作標題</param>
        /// <param name="工作時長">新的工作時長</param>
        public void FF更新 ( string 標題 , TimeSpan 工作時長 )
            {
                this . 標題 = 標題;
                this . 工作時長 = 工作時長;
            }

        /// <summary>
        /// 重寫從“System . Object”繼承而來 virtual 的“ToString”方法
        /// </summary>
        /// <returns>工作項的 ID 和標題</returns>
        public override string ToString ( )
            {
                return $"{this . ID} - {this . 標題}";
            }
    }

/// <summary>
/// “Lei變更請求”類繼承自“Lei工作項”類,並新增了一個屬性(“ID當前項目”)以及兩個構造函數
/// </summary>
public class Lei變更請求 : Lei工作項
    {
        protected int ID當前項目 { get; set; }

        // 構造函數。由於這兩個構造函數都不會顯式調用基類的構造函數,因此基類中的默認構造函數會被隱式調用。基類必須包含一個默認構造函數

        /// <summary>
        /// 派生類的默認構造函數
        /// </summary>
        public Lei變更請求 ( ) { }

        /// <summary>
        /// 派生類的具有四個參數的構造函數
        /// </summary>
        /// <param name="標題">工作項的標題</param>
        /// <param name="説明">工作項的説明</param>
        /// <param name="工作時長">工作項的工作時長</param>
        /// <param name="當前ID">工作項的當前 ID</param>
        public Lei變更請求 ( string 標題 , string 説明 , TimeSpan 工作時長 , int 當前ID )
            {
                // 以下屬性以及 ID下一個 方法是從 Lei工作項 類中繼承而來的
                this . ID = ID下一個 ( );
                this . 標題 = 標題;
                this . 説明 = 説明;
                this . 工作時長 = 工作時長;

                // 屬性“ID當前項目”屬於“Lei變更請求”類,但不屬於“Lei工作項”類
                this . ID當前項目 = 當前ID;
            }
    }

接下來這一部分展示瞭如何使用基類和派生類:

// 通過使用基類中的帶有三個參數的構造函數來創建一個 Lei工作項 的實例
Lei工作項 GZ = new ( "找錯" , "在我的代碼分支中找出並修復所有錯誤" , new TimeSpan ( 3 , 4 , 0 , 0 ) );

// 通過使用派生類中的帶有四個參數的構造函數來創建一個 Lei變更請求 的實例
Lei變更請求 BG = new ( "修改基類設計" , "向類中添加成員" , new TimeSpan ( 4 , 0 , 0 ) , 1 );

// 使用 Lei工作項 中聲明的 ToString 方法
Console . WriteLine ( GZ . ToString ( ) );

// 使用繼承自 Lei工作項 的“FF更新”方法來更改“變更請求”對象的標題
BG . FF更新 ( "重新設計基類" , new TimeSpan ( 4 , 0 , 0 ) );

// Lei變更請求 繼承了 Lei工作項 的重寫的 ToString
Console . WriteLine ( BG . ToString ( ) );

abstract(抽象)方法和 virtual(虛擬)方法

當基類將一個方法聲明為 virtual 時,派生類可以使用自身的實現來 override 該方法。如果基類將一個成員聲明為 abstract 的,那麼在任何直接繼承自該類的非抽象類中,該方法都必須被 override。如果派生類本身是 abstract 的,它會繼承 abstract 成員但不會實現它們。abstract 和 virtual 成員是多態性的基礎,多態性是面向對象編程的第二個主要特徵。

abstract(抽象)基類

如果您不希望通過使用 new 操作符來直接實例化某個類,可以將該類聲明為 abstract 類。只有當新類從該抽象類派生出來時,才能使用該抽象類。抽象類可以包含一個或多個方法簽名,而這些簽名本身被聲明為 abstract 的。這些簽名指定了參數和返回值,但沒有實現(方法體)。抽象類不一定包含 abstract 成員;然而,如果一個類確實包含一個抽象成員,那麼該類本身必須被聲明為 abstract class。非抽象的派生類必須為抽象基類中的任何抽象方法提供實現。

接口

接口是一種引用類型,它定義了一組成員。所有實現該接口的類和結構體都必須實現這組成員。接口可以為這些成員中的任何一個或全部定義默認實現。一個類可以實現多個接口,儘管它只能從一個直接基類派生。

接口用於為那些並非必然存在 “屬於” 關係的類定義特定的功能。例如,System . IEquatable < T > 這個接口可以被任何類或結構體實現,以確定該類型兩個對象是否相等(但具體等價關係的定義由該類型決定)。IEquatable < T > 並不意味着存在與基類和派生類之間相同的 “屬於” 關係(例如,哺乳動物是動物的一種)。

防止進一步派生

一個類可以通過將自身或其成員聲明為 “密封(sealed)” 來阻止其他類對其進行繼承,或者阻止其他類對其成員進行繼承。

派生類對基類成員的隱藏

派生類可以通過聲明具有相同名稱和簽名的成員來隱藏基類成員。可以使用 “new” 修飾符來明確表示該成員並非是對基類成員的重寫。使用 “new” 並非強制要求,但如果未使用 “new”,編譯器將會發出警告。

多態性

多態性通常被稱為面向對象編程的第三個支柱,排在封裝和繼承之後。多態性是一個源自希臘語的詞,意思是 “多種形狀”,它有兩個不同的方面:

  • 在運行時,派生類的對象在諸如方法參數、集合或數組等地方可以被視為基類的對象。當這種多態性發生時,對象的聲明類型不再與運行時類型完全相同。
  • 基類可以定義並實現 virtual 方法,而派生類可以重寫這些方法,這意味着它們會提供自己的定義和實現。在運行時,當客户端代碼調用該方法時,CLR 會查找對象的運行時類型,並調用該 virtual 方法的 override 版本。在您的源代碼中,您可以調用基類的方法,並導致調用派生類的方法版本來執行。

virtual 方法使您能夠以統一的方式處理一組相關對象。例如,假設您有一個繪圖應用程序,它允許用户在繪圖表面上創建各種形狀。您在編譯時無法預知用户將創建哪些具體類型的形狀。然而,該應用程序必須跟蹤所有創建的各類形狀,並且必須根據用户鼠標操作對其進行更新。您可以使用多態性來解決這個問題,具體步驟如下:

  1. 創建一個類層次結構,其中每個具體的形狀類都從一個共同的基類派生而來。
  2. 使用 virtual 方法通過對基類方法的一次調用來在任何派生類中調用適當的方法。

首先,創建一個名為 “Shape” 的基類,以及諸如 “Rectangle”(矩形)、“Circle”(圓形)和 “Triangle”(三角形)這樣的派生類。為 “Shape” 類提供一個名為 “Draw” 的 virtual 方法,並在每個派生類中對其進行 override(重載),以繪製該類所表示的特定形狀。創建一個 “List < Shape >” 對象,並向其中添加一個 “Circle”(圓形)、“Triangle”(三角形)和 “Rectangle”(矩形)。

public class Lei形狀
    {
        // 幾個示例成員
        public double X { get; set; }
        public double Y { get; set; }
        public double Chang { get; set; }
        public double Kuan { get; set; }

        public virtual void GH ( )
            {
                Console . WriteLine ( "執行基類繪圖任務" );
            }
    }

public class Lei圓 : Lei形狀
    {
        public override void GH ( )
            {
                Console . WriteLine ( "畫一個圓" );
                base . GH ( );
            }
    }

public class Lei矩形 : Lei形狀
    {
        public override void GH ( )
            {
                Console . WriteLine ( "畫一個矩形" );
                base . GH ( );
            }
    }

public class Lei三角形 : Lei形狀
    {
        public override void GH ( )
            {
                Console . WriteLine ( "畫一個三角形" );
                base . GH ( );
            }
    }

若要更新繪圖表面,請使用 foreach 循環遍歷列表,並對列表中的每個 Lei形狀 對象調用 GH 方法。儘管列表中的每個對象都已聲明為 Lei形狀 類型,但實際調用的將是運行時類型(即每個派生類中重寫的方法版本)。

// 多態性應用示例 #1:矩形、三角形和圓形都可以在需要 “Lei形狀” 對象的地方使用。無需進行類型轉換,因為從派生類到基類存在隱式轉換
var XingZhuangs = new List < Lei形狀 >
    {
        new Lei矩形 ( ),
        new Lei圓 ( ),
        new Lei三角形 ( ),
    };

foreach ( var xz in XingZhuangs )
    {
        xz . GH ( );
    }

在 C# 中,每種類型都是多態的,因為包括用户自定義類型在內的所有類型都繼承自 Object 類。

多態概述

virtual(虛擬)成員

當派生類繼承自基類時,它會包含基類的所有成員。基類中聲明的所有行為都是派生類的一部分。這使得派生類的對象可以被視為基類的對象。訪問修飾符(public、protected、private 等)決定了這些成員是否可以從派生類的實現中訪問到。virtual 方法為設計者提供了對派生類行為的不同選擇:

  • 派生類可以 override(重載)基類中的 virtual 成員,從而定義新的行為。
  • 派生類可以繼承最接近的基類方法而不進行重載,保留現有行為,同時允許後續的派生類重寫該方法。
  • 派生類可以為那些成員定義新的非虛實現,從而隱藏基類的實現。

派生類只有在基類成員被聲明為 virtual 或 abstract 時才能重寫基類成員。派生成員必須使用 “override” 關鍵字明確表示該方法旨在參與 virtual 調用。以下代碼提供了一個示例:

public class LeiJi
    {
        public virtual void Do ( ) { }
        public virtual int SHX虛
            {
                get {  return 0; }
            }
    }

public class LeiPS : LeiJi
    {
        public override void Do ( )
            {
                Console . WriteLine ( "做點什麼吧……" );
            }

        public override int SHX虛
            {  get { return 10; } }
    }

字段不能是 virtual;只有方法、屬性、事件和索引器可以是 virtual 的。當派生類重寫一個 virtual 成員時,即使在訪問該類的實例時將其視為基類的實例,該成員也會被調用。以下代碼提供了一個示例:

LeiPS P = new ( );
P . Do ( ); // 做點什麼吧(派生類的 Do)

LeiJi J = P;
J . Do ( ); // 做點什麼吧(派生類的 Do)

virtual 方法和 virtual 屬性使得派生類能夠擴展基類,而無需使用基類中方法的實現。接口還提供了另一種定義方法或一組方法的方式,這些方法的實現則由派生類來完成。

用新成員隱藏基類成員

如果您希望派生類擁有與基類中某個成員同名的成員,可以使用 “new” 關鍵字來隱藏基類成員。將 “new” 關鍵字置於被替換的類成員的返回類型之前。以下代碼提供了一個示例:

public class LeiJi
    {
        public int z;
        public virtual void Do ( ) { z++; }
        public virtual int SHX虛
            {
                get {  return 0; }
            }
    }

public class LeiPS : LeiJi
    {
        public new int z = 2;
        public new static void Do ( )
            {
                Console . WriteLine ( "做點什麼吧……" );
            }

        public new static int SHX虛
            {  get { return 10; } }
    }

當您使用 “new” 關鍵字時,您創建的其實是一個隱藏基類方法的機制,而非對其進行重寫。這與 virtual 方法有所不同。在方法隱藏的情況下,被調用的方法取決於變量的編譯時類型,而非對象的運行時類型。

隱藏的基類成員可以通過將派生類的實例轉換為基類的實例來在客户端代碼中進行訪問。例如:

LeiPS P = new ( );
P . Do ( ); // 做點什麼吧……

LeiJi J = ( LeiJi ) P;
J . Do ( );
Console . WriteLine ( J . z ); // 1

在這個例子中,兩個變量都指向同一個對象實例,但調用的方法取決於變量所聲明的類型:通過 LeiPS 變量訪問時調用的是 LeiPS . Do ( ),而通過 LeiJi 變量訪問時則調用的是 LeiJi . Do ( )。

禁止派生類重寫 virtual 成員

virtual 成員的特性不會因在 virtual 成員與最初聲明該成員的類之間所包含的類的數量而改變。如果類 A 宣告了一個 virtual 成員,而類 B 派生自 A,類 C 派生自 B,那麼類 C 將繼承該 virtual 成員,並且可以對其進行重寫,無論類 B 是否為該成員聲明瞭重寫版本。以下代碼提供了一個示例:

public class LeiJi
    {
        public virtual void Do ( ) { }
    }

public class LeiPS : LeiJi
    {
        public override void Do ( ) { }
    }

派生類可以通過將 virtual 繼承標記設為 sealed(密封)來終止虛擬繼承。要終止繼承,需要在類成員聲明中將 sealed 關鍵字置於 override 關鍵字之前。以下代碼提供了一個示例:

public class LeiPS2 : LeiPS
    {
        public sealed override void Do ( ) { }
    }

在上述示例中,方法 “Do” 對於任何從 LeiPS2 類派生的類都不再是 virtual 函數了。但對於 LeiPS2 類的實例(即使它們被轉換為類型 LeiPS 或類型 LeiJi)來説,它仍然是 virtual 函數。sealed 方法可以通過使用 “new” 關鍵字由派生類替換,如下例所示:

public class LeiPS3 : LeiPS2
    {
        public new void Do ( ) { }
    }

在這種情況下,如果使用 LeiPS3 類的變量來調用 Do 方法,那麼就會調用新的 Do 方法。如果使用 LeiPS2 類、LeiPS 類或 LeiJi 類的變量來訪問 LeiPS3 類的實例,那麼調用 Do 時將遵循 virtual 繼承的規則,將這些調用路由到 LeiPS2 類中 Do 方法的實現上。

從派生類中訪問基類的 virtual 成員

如果派生類替換或重寫了某個方法或屬性,那麼仍可以通過使用 “base” 關鍵字來訪問基類中的該方法或屬性。以下代碼提供了一個示例:

public class LeiJi
    {
        public virtual void Do ( ) { Console . WriteLine ( "0" ); }
    }

public class LeiPS : LeiJi
    {
        public override void Do ( ) { Console . WriteLine ( "1" ); base . Do ( ); }
    }

public class LeiPS2 : LeiPS
    {
        public sealed override void Do ( ) { Console . WriteLine ( "2" ); base . Do ( ); }
    }

public class LeiPS3 : LeiPS2
    {
        public new void Do ( ) { Console . WriteLine ( "3" ); base . Do ( ); }
    }

注意:建議 virtual 成員使用 “base” 關鍵字來調用其所屬基類的實現部分,以便在各自的實現中使用。讓基類的行為得以執行,這樣派生類就可以專注於實現特定於自身的行為。如果未調用基類的實現,那麼派生類就需要確保其行為與基類的行為相兼容。

Add a new 评论

Some HTML is okay.