《NET CLR via C#》---第六章(型別成員,型別的可訪問性,友元程式集,分部型別,CLR呼叫方法指令)

陈侠云發表於2024-08-14

型別成員

型別可以定義0個或者多個以下種類的成員。

成員 描述
常量 常量是指出資料值恆定不變的符號。這種符號使程式碼更易閱讀和維護。常量總與型別關聯,不與型別的例項關聯。常量總與型別關聯,不與型別的例項關聯
欄位 欄位表示只讀或可讀/可寫的資料值。欄位可以是靜態的:這種欄位被認為是型別狀態的一部分。欄位也可以是例項(非靜態);這種欄位被認為是物件狀態的一部分。強烈建議將欄位宣告為私有,防止型別或物件的狀態被型別外部的程式碼破壞。
例項構造器 例項構造器是將新物件的例項欄位初始化為良好初始狀態的特殊方法。
型別構造器 型別構造器是將型別的靜態欄位初始化為良好初始狀態的特殊方法。
方法 方法是用來改變或獲取物件或型別資訊的函式。作用於型別稱為靜態方法,作用於物件稱為例項方法。
操作過載符 運算子過載實際是方法,定義了當運算子作用於物件時,應該如何操作該物件。操作過載符不屬於CLS的一部分(不是所有程式語言都支援)
轉換運算子 轉換運算子是定義如何隱式或顯式將物件從一種型別轉型為另一種型別的方法。轉換運算子不屬於CLS的一部分(不是所有程式語言都支援)
屬性 屬性讓你用簡單的語法像訪問欄位一樣讀取或修改物件的資訊,同時確保資料的完整性。作用於型別稱為靜態函式,作用於物件稱為例項屬性。屬性可以無參,也可以有多個引數。
事件 靜態事件允許型別向一個或多個靜態或例項方法傳送通知。例項物件允許物件向一個或多個靜態或例項方法傳送通知。
型別 型別可以定義其他巢狀型別 。

無論什麼程式語言,編譯器都必須能處理原始碼,為上述每種成員生成後設資料和IL程式碼。所有程式語言生成的後設資料格式完全一致。這正式CLR成為“公共語言執行時”的原因。後設資料是所有語言都生成和使用的公共資訊。CLR還利用公共後設資料格式決定常量、欄位、構造器、方法、屬性和事件在執行時的行為。簡單來說,後設資料是整個Microsoft .NET Framework開放平臺的關鍵,它實現了程式語言、型別和物件的無縫集合。

public class MainClass
{
    private class SubClass { }              // 巢狀類

    private const int constVal = 1;         // 常量
    private readonly int readonlyVal = 2;   // 只讀
    private static int staticVal = 3;       // 靜態欄位

    static MainClass() { }                  // 型別構造器

    public MainClass() { }                  // 例項構造器-無參

    public MainClass(int val) { }           // 例項構造器-帶參

    private void ObjMethod() { }            // 例項方法
    
    private static void StaticMethod() { }  // 靜態方法

    public int prop { get; set; }           // 例項屬性

    public int this[int index] { get => 0;set { } } // 例項有參屬性(索引器)

    public event EventHandler SomeEvent;    // 例項事件
}

編譯這個型別,並用ILDasm.exe檢視下後設資料,看看編譯是如何將型別極其成員轉為後設資料:
image

型別的可訪問性

CLR自己定義了一組可訪問性修飾符,但每種程式語言在向成員應用可訪問性時,都選擇了自己的一組術語以及相應的語法。

CLR術語 C#術語 描述
Private private 成員只能由定義型別或任何巢狀型別中的方法訪問
Family protected 成員只能由定義型別、任何巢狀型別或者不管在什麼程式集中的派生型別中方法訪問
Family and Assembly 不支援 成員只能由定義型別、任何巢狀型別或者同一程式集中的派生型別中方法訪問
Assembly internal 成員只能由定義程式集中的方法訪問
Family or Assembly protected internal 成員可由任何巢狀型別,任何派生型別(同一或不同程式集都可)中的任何方法訪問
Public public 成員可由任何程式集的任何方法訪問

編譯程式碼時,程式語言的編譯器檢查程式碼是不是正確引用了型別和成員。如果程式碼不正確地引用了型別或成員,編譯器會生成一條合適的錯誤資訊。

在C#中,如果沒有顯式宣告成員的可訪問性,編譯器通常(但不總是)預設選擇private。CLR要求所有介面型別的所有成員都具有public可訪問性。

派生類重寫基類定義的成員時,C#編譯器要求原始成員和重寫成員具有相同的可訪問性,即基類成員是protected,子類也要是protected。CLR則允許放寬但不能收緊成員的可訪問性,即基類是protected,子類可以是public。這是因為CLR承諾派生類總能轉為基類,並獲取對基類方法的訪問權。

友元程式集

public型別不僅對定義程式集中的所有程式碼可見,還對其他程式集中的程式碼可見。internal則僅對定義程式集中的所有程式碼可見,對其他程式集中的程式碼不可見。

生成程式集時,可用System.Runtime.CompilerServices名稱空間中的InternalsVisibleTo特性標明它認為是“友元”的其他程式集。該特性獲取標識友元程式集名稱和公鑰的字串引數。注意當程式集認了“友元”之後,友元程式集就能訪問該程式集中的所有internal型別,以及這些型別的internal成員。這在需要共享程式集之間的內部實現細節時特別有用,比如單元測試時需要訪問類的內部實現。

要定義友元程式集,你需要在被訪問的程式集的 AssemblyInfo.cs 檔案中使用 InternalsVisibleTo 屬性指定友元程式集的名稱。假設我們有一個名為 MainAssembly 的主程式集,其中包含一個類 MyClass,該類具有一個 internal 方法。

// MainAssembly - AssemblyInfo.cs
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("FriendAssembly")]

namespace MainAssembly
{
    public class MyClass
    {
        internal void InternalMethod()
        {
            Console.WriteLine("Internal method in MainAssembly.");
        }
    }
}

接下來,我們建立一個名為 FriendAssembly 的友元程式集,它可以訪問 MainAssembly 的內部成員。

// FriendAssembly - Program.cs
using MainAssembly;

namespace FriendAssembly
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            myClass.InternalMethod(); // 訪問 internal 方法
        }
    }
}

需要注意的是,如果 MainAssembly 使用了強名稱簽名,那麼在使用 InternalsVisibleTo 屬性時,你需要指定友元程式集的公共金鑰(PublicKey)。示例如下:

[assembly: InternalsVisibleTo("FriendAssembly, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d537e8bda...")]

靜態類

有一些永遠不需要例項化的類,例如Console,Math等。這些類只有static成員。事實上,這種類唯一的作用就是組合一組相關的成員。例如,Math類就定義了一組執行數學運算的方法。在C#中,要用static關鍵字定義不可例項化的類。該關鍵字只能用於類,不能應用於結構(值型別)。因為CLR總是允許值型別例項化,這無法避免。

C#編譯器對靜態類做了如下限制:

  • 靜態類必須直接從System.Object派生,從其他任何基類派生都沒有意義。繼承只適用於物件,而你不能建立靜態類的例項。
  • 靜態類不能實現任何介面,這是因為只有使用類的例項時,才可呼叫類的介面方法。
  • 靜態類只能定義靜態成員(欄位、方法、屬性和事件),任何例項成員都會導致編譯器報錯。
  • 靜態類不能作為欄位、方法引數或區域性變數使用,因為它們都代表引用了例項的變數。

編譯如下程式碼:

public static class MainClass
{
    public static int val;
}

使用ILDasm.exe檢視程式集,能夠發現使用關鍵字static定義類,將導致C#編譯器將該類標記為abstract和sealed。另外,編譯器不在型別中生成例項構造器方法(.ctor)
image

分部型別

partial關鍵字告訴C#編譯器:類、結構或介面的定義原始碼可能要分散到一個或多個原始碼檔案中。有如下幾點好處:

  • 原始碼控制:沒法多個程式設計師同時對一個型別進行修改,使用partial關鍵字可以將型別的程式碼分散到多個原始碼檔案中,每個檔案都可以單獨簽出,多個程式設計師能同時編輯型別。
  • 在同一個檔案中將類或結構分解成不同的邏輯單元:分部的每個部分都能實現一個功能,並配以它的全部欄位、方法、屬性、事件等。這樣就可以方便地看到組合以提供一個功能的全體成員,從而簡化編碼。
  • 程式碼拆分:新建一個型別時,可以自動生成一部分程式碼。這些程式碼可以我們自己的程式碼拆分到不同的原始碼檔案中。避免自動生成和我們自己的程式碼互相干擾。

“分部型別”功能完全由C#編譯器實現,CLR對該功能一無所知。這也解釋了一個型別的所有原始碼檔案為什麼必須使用相同程式語言,而且必須作為一個編譯單元編譯到一起。

CLR呼叫方法指令

以下Employee類定義了3種不同的方法:

internal class MainClass
{
    public void CustomMethod() { }
    public void VirtualMethod() { }
    public void StaticMethod() { }
}

編譯上述程式碼,編譯器會在程式集的方法定義表中寫入3個記錄項,每個記錄項都用一組flag指明方法是例項方法、虛方法還是靜態方法。
image
寫程式碼呼叫這些方法,生成呼叫程式碼的編譯器會檢查方法定義的flag,判斷如何生成IL程式碼來正確呼叫方法。

  • call
    在IL指令中,call指令用於呼叫方法,可以是靜態方法、例項方法或虛方法。
    呼叫靜態方法:必須明確指出在哪個型別中定義了該方法。
    呼叫例項方法或虛方法:需要指定一個引用了物件的變數,並且假設這個變數不是null(即,變數必須有一個有效的物件)。變數的型別會確定方法屬於哪個類。如果變數的型別中沒有定義這個方法,就會在其基類中查詢合適的方法。
    此外,call指令通常用於以非虛擬方式呼叫虛方法。
  • callvirt
    callvirt是IL指令的一種,用於呼叫例項方法和虛方法,不能用於靜態方法。
    呼叫例項方法或虛方法:需要指定一個引用物件的變數。對於非虛例項方法,變數的型別決定了呼叫哪個類中的方法。
    呼叫虛方法:CLR(公共語言執行時)會檢查物件的實際型別,並以多型方式呼叫正確的方法。
    為確保物件存在,變數不能是null。編譯時,JIT編譯器會生成程式碼來檢查變數是否為null。如果是null,callvirt指令會引發NullReferenceException異常。
    由於這種額外的null檢查,callvirt指令的執行速度比call指令稍慢。即使callvirt指令呼叫的是非虛方法,也會進行null檢查。

相關文章