16 結構體

16.1 一般

結構與類別類似,因為它們代表可包含數據成員和函式成員的數據結構。 不過,不同於類別,結構是實值型別,不需要堆積配置。 型別為struct的變數直接包含struct的數據,而類別型別的變數則包含對數據的參考,後者稱為物件。

注意:結構特別適用於具有值語意的小型數據結構。 複數、座標系統中的點或字典中的索引鍵/值組都是結構的良好範例。 這些資料結構的關鍵在於,它們通常只有很少的數據成員,不需要使用繼承或引用語意,而是可以方便地使用值語意,讓指派是複製值而不是引用。 結束註解

§8.3.5中所述,C#提供的簡單類型,例如intdoublebool,實際上都是結構類型。

16.2 結構宣告

16.2.1 一般

struct_declaration 是一種宣告新結構的 type_declaration§14.8):

struct_declaration
    : non_record_struct_declaration
    | record_struct_declaration
    ;

non_record_struct_declaration
    : attributes? struct_modifier* 'ref'? 'partial'? 'struct'
      identifier type_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* struct_body ';'?
    ;

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct'
      identifier type_parameter_list? delimited_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* record_struct_body
    ;

record_struct_body
    : struct_body ';'?
    | ';'
    ;

struct_declaration 是用於非記錄結構記錄結構

一個 non_record_struct_declaration 由一組可選 屬性§23)、一組可選的 struct_modifiers(§16.2.2)、一個可選 ref 修飾符(§16.2.3)、一個可選部分修飾符(§15.2.7)、關鍵字 struct 及一個命名結構體的 識別碼 ,最後是可選的 type_parameter_list 規範(§15.2.3), 接著是可選的 struct_interfaces 規範(§16.2.5),接著是可選的 type_parameter_constraints子句 規範(§15.2.5),接著是 struct_body§16.2.6),最後可選擇接分號。

一個 record_struct_declaration 由一組可選 屬性§23)、一組可選的 struct_modifiers(§16.2.2)、一個可選的部分修飾符(§15.2.7)、關鍵字 record、關鍵字 struct 及命名結構的 識別碼 、可選的 type_parameter_list 規範(§15.2.3)以及可選的 delimited_parameter_list組成 規範(§15.2.1),接著是可選的 struct_interfaces 規範(§16.2.5),再接著是可選的 type_parameter_constraints條款 規範(§15.2.5),最後是 record_struct_body

struct_declaration除非同時提供type_parameter_list,否則不得供應type_parameter_constraints_clause

提供type_parameter_liststruct_declaration是一個通用的結構宣告。 此外,泛型類別宣告或泛型結構宣告內巢狀的任何結構本身都是泛型結構宣告,因為應該提供包含型別的型別自變數來建立建構型別 ({8.4)。

包含ref修飾符的non_record_struct_declaration不應有struct_interfaces部分。

擁有 delimited_parameter_listrecord_struct_declaration宣告一個位置記錄結構

最多只有一個 record_struct_declarationpartial 提供 一個delimited_parameter_list

delimited_parameter_list 中的參數不得有 refoutthis修飾符;然而,inparams允許修飾符。 對於一個record_struct_declaration,record_struct_body s{}{};、、 ; 等價。 它們都表示唯一的成員是編譯者綜合的(§16.4)。

16.2.2 結構修飾詞

struct_declaration可以選擇性地包含一連串struct_modifier

struct_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'readonly'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier§24.2) 僅適用於不安全代碼 (§24)。

同一個修飾詞在結構宣告中出現多次是編譯時期錯誤。

readonly除了 之外,結構宣告的修飾詞與類別宣告的修飾詞意義相同({15.2.2)。

readonly修飾詞表示struct_declaration宣告實例不可變的類型。

唯讀結構具有下列條件約束:

  • 每個實體欄位也應該宣告為readonly
  • 它不得宣告任何類似欄位的事件(§15.8.2)。

當只讀結構實例傳遞至方法時,其 this 會被視為輸入參數,即不允許寫入任何實例欄位(除了建構函式除外)。

16.2.3 Ref 修飾詞

ref修飾符表示non_record_struct_declaration宣告一個實例被分配到執行堆疊上的型態。 這些類型稱為 ref 結構 類型。 ref修飾符宣告實例可包含類似參考的欄位,且不得從其安全上下文中複製(§16.5.15)。 判定參考結構安全上下文的規則詳見 第16.5.15節。

如果在下列任一內容中使用 ref 結構類型,則為編譯時期錯誤:

  • 作為陣列的元素類型。
  • 做為類別或結構中沒有 ref 修飾詞之欄位的宣告型別。
  • 作為類型參數。
  • 作為 Tuple 元素的型別。
  • 採用非同步方法。
  • 在迭代器中。
  • 作為方法群組從實例方法轉換為代理型別的接收器類型。
  • 作為 lambda 表達式中的捕捉變數或局部函數。

此外,以下限制適用於類型 ref struct

  • 類型 ref struct 不應被框定為 System.ValueTypeSystem.Object
  • ref struct類型不得宣告為實作任何介面。
  • objectSystem.ValueType 中宣告但未在 ref struct 型別中覆寫的實例方法,不得以該 ref struct 型別的接收者呼叫。

注意:不應該 ref struct 宣告 async 實例方法,也不會在 yield return 實例方法中使用 或 yield break 語句,因為隱含 this 參數不能用於這些內容中。 結束註解

這些條件約束可確保 類型的 ref struct 變數不會參考不再有效的堆疊記憶體,或參考不再有效的變數。

16.2.4 部分修飾詞

修飾詞 partial 表示這個 struct_declaration 是部分類型宣告。 多個在封入命名空間或類型宣告內具有相同名稱的部分結構宣告會結合成一個結構宣告,並遵循在 \15.2.7 中指定的規則。

16.2.5 結構介面

結構宣告可能包含 struct_interfaces 規格,在此情況下,結構據說會直接實作指定的介面類型。 針對建構的結構類型,包括在泛型型別宣告中宣告的巢狀類型(§15.3.9.7),每個實作的介面類型都是通過在所指定的介面中用建構型別的對應type_argument 替代每個type_parameter 來取得。

struct_interfaces
    : ':' interface_type_list
    ;

在多個部分的部分結構宣告(§15.2.7)中介面的處理方式,會在§15.2.4.3中進一步討論。

19.6 節進一步討論介面實作。

16.2.6 結構主體

結構 struct_body 會定義結構的成員。

struct_body
    : '{' struct_member_declaration* '}'
    ;

16.3 結構成員

16.3.1 一般

結構的成員包含其 struct_member_declaration所引進的成員,以及繼承自 類型 System.ValueType的成員。 對於記錄結構,成員集合也包含由編譯器產生的合成成員(§synth-members)。

struct_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | static_constructor_declaration
    | type_declaration
    | fixed_size_buffer_declaration   // unsafe code support
    ;

fixed_size_buffer_declaration§24.8.2) 僅適用於不安全代碼 (§24)。

注意:各種 class_member_declaration,除了 finalizer_declaration 以外,也都是 struct_member_declaration結束註解

除了第 16.5條所述的差異外, 第15.315.12 條中對類別成員的描述同樣適用於結構體成員。

記錄結構體的實例欄位出現不安全型態是錯誤。

16.3.2 只讀會員

實例成員定義或包含該 readonly 修飾符的實例屬性、索引器或事件的存取器,有以下限制:

  • 參數 this 是一個 ref readonly 參考。
  • 成員不得重新指派接收者的值 this 或實例欄位。
  • 成員不得重新指派接收端類似實例欄位事件(§15.8.2)的值。
  • 若一個唯讀成員呼叫非唯讀成員,必須複製該 this 結構以使用可寫的參數參考 this

註: 實例欄位包含用於自動實作屬性的隱藏後盾欄位(§15.7.4)。 結束註解

範例:唯讀成員可以修改實例欄位所指物件的狀態,儘管該只讀成員無法重新指派該實例成員。 以下程式碼示範重新指派與修改實例欄位:

public struct S
{
    private List<string> messages;

    public S(IEnumerable<string> messages) =>
        this.messages = new List<string>(messages);

    public void InitializeMessages() =>
        messages = new List<string>();

    public readonly void AddMessage(string message)
    {
        if (messages == null)
        {
            throw new InvalidOperationException("Messages collection is not initialized.");
        }
        messages.Add(message);
    }
}

readonly 方法 AddMessage 可以改變訊息清單的狀態。 InitializeMessages成員可以清除並重新初始化訊息清單。 在 的情況下 AddMessage,修 readonly 飾符是有效的。 在 的情況下 InitializeMessages,加入 readonly 修飾符是無效的。 結束 範例

16.4 合成記錄結構成員

16.4.1 一般

在記錄結構的情況下,除非 record_struct_body 中宣告具有「匹配」簽章的成員,或繼承一個具有「匹配」簽章的可存取具體非虛擬成員,否則成員會被合成。 若兩個成員有相同的簽名,則視為匹配,或在繼承情況下被視為「隱藏」。 (參見簽名與重載 §7.6。)

綜合成員在以下子句中描述。

16.4.2 平等成員

合成的等號成員類似於記錄類別(§15.16.2),但缺少 EqualityContract、 空檢查或繼承。

記錄結構 RSystem.IEquatable<R> 作並包含一個合成的強型別超載,該超載 Equals(R other)為公開,具體如下:

public readonly bool Equals(R other);

這個方法可以明確宣告。 然而,若明確宣告與預期的簽名或可存取性不符,則為錯誤。

Equals(R other) 是使用者定義(即未合成)但 GetHashCode 未 ,則會產生警告。

合成後的 synthesed Equals(R) 必須回傳 true 當且僅當對於記錄中每個實例欄位 fieldN ,結構體中 的 System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)值為 ,其中 TN 是欄位類型,為 true

記錄結構包含 == 合成與 != 運算子,等價於以下宣告的運算子:

public static bool operator==(R r1, R r2) => r1.Equals(r2);
public static bool operator!=(R r1, R r2) => !(r1 == r2);

Equals運算子==所呼叫的方法即上述Equals(R other)所述方法。 !=操作員將工作權委託給操作員==。 若運算子被明確宣告,則為錯誤。

記錄結構包含一個綜合覆寫,等同於宣告如下方法:

public override readonly bool Equals(object? obj);

若覆蓋被明確宣告,則為錯誤。 合成覆寫將回傳 other is R temp && Equals(temp) ,其中 R 為記錄結構。

記錄結構包含一個綜合覆寫,等同於宣告如下方法:

public override readonly int GetHashCode();

此方法可明確宣告。

若明確宣告其中 Equals(R) 一種 和 GetHashCode() 但另一方未宣告,則應報告警告。

對 的GetHashCode()綜合覆寫將回傳將每個實例欄位fieldNTN的值System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)合併後,與 int 為 的fieldN類型 。

範例:考慮以下記錄結構:

record struct R1(T1 P1, T2 P2);

為此,合成的等式成員大致如下:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }
    public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
    public bool Equals(R1 other)
    {
        return
            EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R1 r1, R1 r2) => r1.Equals(r2);
    public static bool operator!=(R1 r1, R1 r2) => !(r1 == r2);    
    public override int GetHashCode()
    {
        return HashCode.Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));

結束 範例

16.4.3 印刷成員

記錄結構包含一種綜合方法,等價於以下:

private bool PrintMembers(System.Text.StringBuilder builder);

此方法執行以下任務:

  1. 對於記錄結構中每個可列印的成員(非靜態的公共欄位及可讀屬性成員),會在該成員名稱後加上「=」,再以「, “分隔」分隔的成員值,
  2. 如果 record struct 有可列印的成員,則會回傳 true。

對於具有值型別的成員,其值應轉換為字串表示法。

若記錄的可列印成員未包含帶有非readonlyget accessor 的可讀屬性,則合成PrintMembersreadonly為 。 不要求記錄欄位必須為 readonly ,方法 PrintMembersreadonly為 。

PrintMembers此方法可明確宣告。 然而,若明確宣告與預期的簽名或可存取性不符,則為錯誤。

記錄結構包含一種綜合方法,等效於以下:

public override string ToString();

若記錄結構 PrintMembers 的方法為 readonly,則合成 ToString() 方法為 readonly

這個方法可以明確宣告。 若明確宣告與預期的簽章或可存取性不符,則為錯誤。

此方法執行以下任務:

  1. 建立一個 StringBuilder 實例,
  2. 將 record struct 名稱附加到建構者後面,接著是「{」,
  3. 呼叫記錄結構 PrintMembers 的方法,給出建構器,若返回為真,接著加上「 」,
  4. 附錄「}」,
  5. 返回建商的物品。builder.ToString()

範例:考慮以下記錄結構:

record struct R1(T1 P1, T2 P2);

對於這個記錄結構,合成的列印成員會是:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString());
                                 // if P1 has a value type
        builder.Append(", ");

        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2.ToString());
                                 // if P2 has a value type

        return true;
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

結束 範例

16.4.4 位置記錄結構成員

16.4.4.1 一般

除了提供前述子句中描述的成員外,位置記錄結構(§16.2.1)還會以與其他子句相同的條件合成額外的成員,如以下子條款所述。

16.4.4.2 主要建構器

記錄結構體有一個公開建構子,其簽章對應於類型宣告的值參數。 這稱為該類型的主要建構子。 在結構中存在主建構子與帶有相同簽章的建構子是錯誤。 若型別宣告未包含 delimited_parameter_list,則不會產生主要建構子。

record struct R1
{
    public R1() { } // OK
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines
                    // a constructor with the same parameter types
}

記錄結構的實例欄位宣告允許包含變數初始化器。 若無主要建構子,實例初始化器將作為無參數建構子的一部分執行。 否則,執行時主要建構子會執行出現在 record-struct-body 中的實例初始化器。

若記錄結構有主要建構子,任何使用者定義建構器都必須有明確 this 的建構器初始化器,呼叫主要建構子或明確宣告的建構子。

主建構子的參數以及記錄結構的成員都在實例欄位或屬性的初始化器中。 實例成員在這些位置會是錯誤,但主建構器的參數會在作用範圍內且可用,並且會遮蔽成員。 靜態成員也可以使用。

若未讀取主建構子的參數,則會產生警告。

struct instance constructor 的確定指派規則適用於記錄結構體的主要建構子。 例如,以下是一個錯誤:

record struct Pos(int X) // def assignment error in primary constructor
{
    private int x;
    public int X {
        get { return x; } set { x = value; } 
    } = X;
}

16.4.4.3 屬性

對於每個與明確宣告的實例欄位名稱與型別相同的 delimited_parameter_list 參數,該子句的其餘部分不適用。

對於 delimited_parameter_list 的每個記錄結構參數,都有一個對應的公共屬性成員,其名稱與型別取自值參數宣告。

對於記錄結構:

  • 若 record 結構帶有readonly修飾符,則會建立 public getinit auto 屬性, getset否則則不然。 這兩種集合存取器(setinit)都被視為「匹配」的。 因此,使用者可以宣告僅初始化的屬性,取代合成的可變屬性。

  • 繼承 abstract 屬性與類型匹配的屬性會被覆寫。

  • 如果記錄結構體有一個包含預期名稱和類型的實例欄位,則不會產生自動屬性。

  • 若繼承的屬性沒有 publicgetset/init 存取者,則為錯誤。

  • 若繼承的屬性或欄位被隱藏,則為錯誤。

  • 自動屬性初始化為對應的主要建構參數值。

  • 屬性可透過使用property:field:或 目標套用於對應的記錄結構參數,套用於合成的自動屬性及其後盾欄位。

16.4.4.4 解構

一個至少有一個參數的位置記錄結構會合成一個 void公開的 -returning 實例方法,該 Deconstruct 方法對主要建構子宣告的每個參數都有一個 out 參數宣告。 每個參數 Deconstruct 的型別與主建構子宣告的對應參數相同。 方法主體會將 Deconstruct 方法的每個參數指派給同名成員的實例成員存取值。 若在實體中存取的實例成員中沒有包含非readonlyget accessor 的屬性,則合成 Deconstruct 方法為 readonly。 此方法可明確宣告。 若明確宣告與預期的簽章或可存取性不符,或是靜態,則為錯誤。

16.5 班級與結構差異

16.5.1 一般

結構與類別在幾個重要方面上有所不同:

  • 結構體是值型別(§16.5.2)。
  • 所有結構型態都隱含地繼承自類別 System.ValueType§16.5.3)。
  • 對結構型別的變數指派會產生該值的 副本§16.5.4)。
  • 結構體的預設值是將所有欄位設為預設值所產生的值(§16.5.5)。
  • 盒裝與開箱操作用於結構型態與特定參考型別之間的轉換(§16.5.6)。
  • 在結構體成員中,其 this 意義不同(§16.5.7)。
  • 結構體不得宣告終結器。
  • 事件宣告、屬性宣告、屬性存取子、索引器宣告和方法宣告可以有修飾詞 readonly,而這些相同成員類型在類別中通常不允許這樣做。

16.5.2 值語意學

結構是實值型別(~8.3),據說具有值語意。 另一方面,類別是參考型別(~8.2),據說具有參考語意。

結構類型的變數直接包含結構的數據,而類別類型的變數則包含包含數據的 對象參考。 當結構B包含類型A的實例字段,而A是結構類型時,如果A依賴於B或來自B建構的類型,則會產生編譯時期錯誤。 如果 X 包含類型為 的實例欄位,則結構 YX。 鑒於此定義,結構所依賴的完整結構集是「直接依賴於」關係的傳遞閉包。

範例:

struct Node
{
    int data;
    Node next; // error, Node directly depends on itself
}

是錯誤,因為 Node 包含其本身類型的實例欄位。 另一個範例

struct A { B b; }
struct B { C c; }
struct C { A a; }

是錯誤,因為每個類型 ABC 相依於彼此。

結束 範例

使用類別時,兩個變數可以參考相同的物件,因此一個變數上的作業可能會影響另一個變數所參考的物件。 使用結構體時,變數各自擁有自己的數據副本(除了以引用參數的情況外),因此在一個變數上的操作不會影響到另一個變數。 此外,除非明確可為 Null (~8.3.12),否則結構類型的值不可能是 null

注意:如果結構包含參考類型的字段,則其他作業可以改變參考之對象的內容。 不過,欄位本身的值,也就是它所參考的物件,無法透過不同結構值的突變來變更。 結束註解

範例:給定下列

struct Point
{
    public int x, y;

    public Point(int x, int y) 
    {
        this.x = x;
        this.y = y;
    }
}

class A
{
    static void Main()
    {
        Point a = new Point(10, 10);
        Point b = a;
        a.x = 100;
        Console.WriteLine(b.x);
    }
}

輸出為 10。 指派 ab 時會建立值的複本,因此 b 不會受到指派給 a.x 的影響。 若 Point 被宣告為類別,輸出將為 100,因為 ab 将參考相同的物件。

結束 範例

16.5.3 繼承

所有結構型別會隱含地繼承自類別 System.ValueType,而類別 System.ValueType 則會繼承自類別 。 結構宣告可以指定實作介面的清單,但結構宣告不可能指定基類。

結構體永遠不是抽象的,並且總是隱含封閉的。 因此,abstractsealed 修飾詞在結構宣告中不被允許。

由於結構體不支援繼承,結構成員宣告的可及性不能是 protected、 或 private protectedprotected internal

結構中的函式成員不可以是抽象或虛擬的,而且 override 只允許修飾詞覆寫繼承自 System.ValueType的方法。

16.5.4 任務分配

結構類型的變數指派會 建立所指派值的複本 。 這與類別類型的變數指派不同,該變數會複製參考,但不會複製參考所識別的物件。

類似於指派,當結構當做值參數傳遞或當做函式成員的結果傳回時,就會建立結構的複本。 結構可以透過傳址參數,以參考方式傳遞至函式成員。

當結構的屬性或索引器是指派的目標時,與屬性或索引器存取相關聯的實例表達式應分類為變數。 如果實例表達式分類為值,就會發生編譯時期錯誤。 此點在 第12.24.2節有更詳細說明。

16.5.5 預設值

如 \9.3 中所述,在建立變數時,會自動初始化數種變數的預設值。 對於類別型別和其他參考型別的變數,這個預設值為 null。 不過,由於結構是不能 null的實值型別,所以結構的預設值是將所有實值型別欄位設定為預設值,並將所有參考型別字段設定為 null所產生的值。

範例:參考 Point 上述宣告的結構,此範例

Point[] a = new Point[100];

將陣列中的每個Point初始化為將xy欄位設定為零所產生的值。

結束 範例

結構的預設值會對應至結構的預設建構函式所傳回的值(~8.3.3)。 當結構體未宣告明確的無參數實例建構子時,會合成預設建構子,並總是回傳將所有欄位設為預設值所產生的值。 即使結構體宣告明確的無參數實例建構子(§16.4.9),該 default 表達式也總是產生零初始化的預設值。

注意:結構的設計應考慮預設初始化狀態為有效的狀態。 在範例中

struct KeyValuePair
{
    string key;
    string value;

    public KeyValuePair(string key, string value)
    {
        if (key == null || value == null)
        {
            throw new ArgumentException();
        }

        this.key = key;
        this.value = value;
    }
}

用戶定義的實例建構函式只有在被明確呼叫時,才會防止 null 的值造成問題。 在變數受限於預設值初始化的情況下 KeyValuePairkeyvalue 欄位將會是 null,而結構應該準備好處理此狀態。

結束註解

16.5.6 拳擊與開箱

類別類型的值可以轉換成類型 object ,或轉換成類別所實作的介面類型,只要在編譯階段將參考視為另一種類型即可。 同樣地,類型 object 的值或介面類型的值可以轉換回類別類型,而不需變更參考(但在此案例中,需要執行時類型檢查)。

由於結構不是參考型別,因此這些作業會針對結構類型以不同的方式實作。 當結構類型的值轉換成特定參考型別時(如 §10.2.9 中所定義),就會進行裝箱作業。 同樣地,當特定參考型別的值(如第10.3.7節中所定義)轉換為結構類型時,就會進行拆箱操作。 與類別類型相同的作業相比,主要差異在於boxing和unboxing會將結構值複製至Boxed實例或從Boxed實例取出。

注意:因此,在進行 boxing 或 unboxing 操作後,對未封箱的 struct 所做的更改不會反映在已封箱的 struct 中。 結束註解

如需了解封箱和拆箱的更多詳情,請參閱 §10.2.9§10.3.7

16.5.7 此意義

結構中 的意義this與 類別中 的意義this不同,如 •12.8.14 中所述。 當結構類型覆寫繼承自 System.ValueType 的虛擬方法(例如 EqualsGetHashCodeToString),透過結構類型的實例叫用虛擬方法時,不會造成 Boxing 發生。 即使結構當做類型參數使用,而且會透過類型參數類型的實例進行調用,也是如此。

範例:

struct Counter
{
    int value;
    public override string ToString() 
    {
        value++;
        return value.ToString();
    }
}

class Program
{
    static void Test<T>() where T : new()
    {
        T x = new T();
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
    }

    static void Main() => Test<Counter>();
}

程式輸出為:

1
2
3

雖然ToString有副作用是不良的風格,但此範例顯示呼叫x.ToString()的三次操作中沒有發生封箱。

結束 範例

同樣地,當成員實作於實作實值型別時,在限制型別參數上存取成員時,永遠不會隱含地發生Boxing。 例如,假設介面 ICounter 包含方法 Increment,可用來修改值。 如果使用 ICounter 作為限制條件,那麼會以Increment所呼叫的變數作為參考來呼叫Increment方法的實作,絕不會呼叫封裝的複本。

範例:

interface ICounter
{
    void Increment();
}

struct Counter : ICounter
{
    int value;

    public override string ToString() => value.ToString();

    void ICounter.Increment() => value++;
}

class Program
{
    static void Test<T>() where T : ICounter, new()
    {
        T x = new T();
        Console.WriteLine(x);
        x.Increment();              // Modify x
        Console.WriteLine(x);
        ((ICounter)x).Increment();  // Modify boxed copy of x
        Console.WriteLine(x);
    }

    static void Main() => Test<Counter>();
}

第一次呼叫 ,修改 Increment 變數 x中的值。 這與第二次呼叫 Increment 不相等,因為它修改了 x 的封裝複本中的值。 因此,程序的輸出為:

0
1
1

結束 範例

16.5.8 現場初始化器

§16.5.5 所述,結構體的預設值由將所有值型別欄位設為預設值,並將所有參考型別欄位設 null為 後產生的值。 結構體的靜態與實例欄位允許包含變數初始化器;然而,若是實例欄位初始化器,至少還應宣告一個實例建構子,或對於記錄結構體,必須有 delimited_parameter_list

範例:

Console.WriteLine($"Point is {new Point()}");

struct Point
{
    public int x = 1;
    public int y = 1;

    public Point() { }

    public override string ToString()
    {
        return "(" + x + ", " + y + ")";
    }
}
Point is (1, 1)

結束 範例

當結構體實例建構器沒有建構器初始化器時,該建構器會隱含執行由其結構中宣告的實例欄位 variable_initializers 所指定的初始化。 這對應於一連串在進入建構子時立即執行的指派序列。

當結構體實例建構子有一個 this() 代表預設無參數建構器的建構器初始化器時,該宣告建構子會隱含地清除所有實例欄位,並執行其結構中宣告的實例欄位 variable_initializers 所指定的初始化。 一旦進入建構子,所有值型別欄位即設為預設值,所有參考型別欄位皆設為 null。 緊接著執行一連串對應 於 variable_initializers 的指派。

直接在具有struct_modifierstruct_declaration內宣告的readonly應具有field_modifierreadonly

16.5.9 製造商

結構體可以宣告實例建構子,參數為零或多個。 若結構體沒有明確宣告的無參數實例建構子,則會被合成並公開可及性,且將所有值型態欄位設為預設值,所有參考型態欄位設為 null§8.3.3)後的值。 在這種情況下,當建構子執行時,任何實例欄位初始化器都會被忽略。

明確宣告的無參數實例建構器應具備公開可存取性。

範例:給定以下內容:

using System;
struct Point
{
    int x = -1, y = -2;

    public Point(int x, int y) 
    {
        this.x = x;
        this.y = y;
    }

    public override string ToString()
    {
        return "(" + x + ", " + y + ")";
    }
}

class A
{
    static void Main()
    {
        Console.WriteLine($"Point is {new Point()}");
        Console.WriteLine($"Point is {new Point(0,0)}");
    }
}
Point is (0, 0)
Point is (0, 0)

這些陳述式同時建立 Pointxy初始化為零,這在呼叫無參數實例建構子時可能會令人驚訝,因為兩個實例欄位都有初始化器,但它們並未被執行。

結束 範例

不允許結構體實例建構子包含形式為base(argument_list)的建構子初始化,其中 argument_list 是可選的。 執行實例建構子時,不應執行結構基底型 System.ValueType態 的建構子。

結構 this 實例建構函式的參數會對應至結構類型的輸出參數。 因此,this 應該在建構函式返回的每個位置,確定指派(§9.4)。 同樣地,在確定指派之前,即使是隱含的也無法在建構函式主體中讀取它。

如果結構實例建構函式指定建構函式初始化表達式,該初始化表達式會被視為在建構函式主體之前發生的明確指派。 因此,系統本身不需要初始化要求。

instance fields(欄位除外 fixed )必須在沒有 this() 初始化器的struct實例建構子中被指派。

範例:請考慮下列實例建構函式實作:

struct Point
{
    int x, y;

    public int X
    {
        set { x = value; }
    }

    public int Y 
    {
        set { y = value; }
    }

    public Point(int x, int y) 
    {
        X = x; // error, this is not yet definitely assigned
        Y = y; // error, this is not yet definitely assigned
    }
}

除非已明確指派建構結構的所有欄位,否則無法呼叫實例函式成員(包括屬性 XY的 set 存取子)。 不過請注意,如果 Point 類別不是結構,則會允許實例建構函式實作。 這有一個例外狀況,這牽涉到自動實作的屬性(~15.7.4)。 確定指派規則(§12.24.2)明確豁免在該結構型別的實例建構子中對該結構型別的自動屬性指派:此類指派被視為對自置屬性隱藏支持欄位的確定指派。 因此,允許下列事項:

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y)
    {
        X = x; // allowed, definitely assigns backing field
        Y = y; // allowed, definitely assigns backing field
   }
}

結束範例]

16.5.10 靜態構造器

結構靜態建構函式會遵循與類別相同的大部分規則。 結構類型的靜態建構函式執行是由應用程式域內發生下列第一個事件所觸發:

  • 參考結構類型的靜態成員。
  • 結構型別的明確宣告的建構函式被呼叫。

注意:建立結構型別的預設值(§16.5.5)不會觸發靜態建構器。 (其中一個範例是陣列中元素的初始值。)end note

16.5.11 屬性

struct_declaration中,實例屬性的property_declaration§15.7.1)可能包含property_modifierreadonly。 不過,靜態屬性不得包含該修飾詞。

嘗試透過該結構中宣告的唯讀屬性修改實例結構變數的狀態,這是編譯時期錯誤。

自動實作屬性在編譯時期會出現錯誤,如果它同時具有 readonly 修飾詞和 set 存取子。

自動實作的屬性若在 readonly 結構中具有 set 存取子,則會導致編譯時錯誤。

在一個結構中宣告的自動實作屬性不需要有修飾詞 readonly,因為其存取子 readonly 被隱含地假設為只讀。

在屬性本身,以及它的readonlyget存取子上都有set修飾詞,是編譯時期的錯誤。

屬性若在其所有的存取子上都具有唯讀修飾詞,會導致編譯時錯誤。

注意:若要更正錯誤,請將 修飾詞從存取子移至屬性本身。 結束註解

針對屬性存取子表示式, s.P

  • 如果在 s.PM 中的進程會建立 的暫存複本T時叫用 型別的 set 存取子s,則為編譯時期錯誤。
  • 如果 s.P 叫用 類型的 Tget 存取子,則會遵循 \12.6.6.1 中的進程,包括視需要建立 的 s 暫存複本。

自動實作的屬性(§15.7.4)會使用隱藏的後備欄位,這些欄位只能由屬性存取子存取。

注意:此存取限制表示包含自動實作屬性之結構中的建構函式通常需要明確建構函式初始化表達式,否則它們不需要一個,以滿足叫用任何函式成員或建構函式傳回之前明確指派的所有字段需求。 結束註解

16.5.12 方法

一個 struct_declaration 中實例方法的 method_declaration§15.6.1)可能會包含 method_modifierreadonly。 不過,靜態方法不得包含該修飾詞。

嘗試透過該結構中宣告的唯讀方法修改實例結構變數的狀態,這是編譯時期錯誤。

雖然只讀方法可能會呼叫同層級的非唯讀方法、屬性或索引器的 get 存取子,但這樣做會促使建立 this 的隱含複本,以防止潛在的副作用。

readonly 方法可能會呼叫唯讀的同層級屬性或索引器 set 存取子。 如果同層級成員的存取子不是明確或隱含唯讀的,就會發生編譯錯誤。

所有部分方法的 method_declaration 都應該有 readonly 修飾詞,或者所有都不應該有。

16.5.13 索引員

struct_declaration的實例索引器中,indexer_declaration§15.9)可能包含indexer_modifierreadonly

嘗試透過該結構中宣告的唯讀索引器修改實例結構變數的狀態,這是編譯時期錯誤。

在索引器本身以及其 readonly 或是 get 存取子上使用set修飾詞為編譯時錯誤。

索引器在所有存取子上都有只讀修飾詞,這是編譯時期錯誤。

注意:若要更正錯誤,請將 修飾詞從存取子移至索引器本身。 結束註解

16.5.14 事件

實例的 event_declaration~15.8.1), struct_declaration 中的非字段式事件可能包含 event_modifierreadonly。 不過,靜態事件不得包含該修飾詞。

16.5.15 安全上下文約束

16.5.15.1 一般

在編譯階段,每個表達式都會與一個上下文相關聯,其中該實例及其所有欄位都可以安全地存取,這就是其安全環境 安全環境是可以包容某個表達式的環境,在這個環境中,值可以安全地被傳遞。

編譯時期類型不是 ref 結構的任何運算式,都具有呼叫者內容的安全上下文。

任何類型的 default 表達式都取決於呼叫端內容的安全上下文。

對於任何編譯時間類型為 ref 結構的非預設表達式,其具有下列各節所定義的安全內容。

安全上下文記錄指出值可以被複製到哪些上下文中。 如果將具有安全內容E1的表達式S1指派給具有安全內容E2的表達式S2,而S2S1具有更廣泛的內容,則這是錯誤的。

有三個不同的安全內容值,與針對參考變數定義的 ref-safe-context 值相同({9.7.2): declaration-blockfunction-membercaller-context。 表達式的安全內容會限制其用法,如下所示:

  • 對於 return 語句 return e1 的安全上下文 e1 應該是呼叫者上下文。
  • 指派 e1 = e2 的安全情境 e2 至少應與安全情境 e1一樣寬。

對於一個方法調用,如果有refout參數的ref struct類型(包括接收者,除非類型是readonly),並且具有安全上下文S1,那麼任何參數(包括接收者)都不可能有比S1更窄的安全上下文。

16.5.15.2 參數安全上下文

ref 結構類型的參數,包含實例方法中的 this 參數,會在呼叫方上下文中具有安全上下文。

16.5.15.3 本地變數安全上下文

ref 結構類型的局部變數具有安全內容,如下所示:

  • 如果變數是迴圈的 foreach 反覆專案變數,則變數的安全內容與迴圈表達式的安全內容 foreach 相同。
  • 否則,如果變數的宣告具有初始化表達式,則變數的安全內容與該初始化表達式的安全內容相同。
  • 否則,變數在宣告點未初始化,且具有呼叫端內容的安全內容。

16.5.15.4 現場安全上下文

欄位 e.F 的參考,其中類型 F 是 ref 結構型別,具有與 e 相同的安全內容。

16.5.15.5 使用者

使用者自訂運算子的應用被視為方法調用(§16.5.15.6)。

對於產生值,例如 e1 + e2c ? e1 : e2的運算符,結果的安全內容是運算符之安全內容中最窄的內容。 因此,對於產生值的一元運算符,例如 +e,結果的安全內容是操作數的安全內容。

注意:條件運算符的第一個操作數是 bool,因此其安全內容是呼叫端內容。 因此,所得的安全上下文是第二和第三個運算元中最狹窄的安全上下文。 結束註解

16.5.15.6 方法與屬性調用

由方法調用 e1.M(e2, ...) 或屬性調用 e.P 所產生的值,其安全上下文為以下上下文中最小者:

  • 呼叫者-語境。
  • 所有引數表達式的安全環境(包括接收者)。

上述規則會將屬性調用 (或 getset) 視為基礎方法的方法調用。

16.5.15.7 stackalloc

stackalloc 運算式的結果具有 function-member 的安全內容。

16.5.15.8 建構者召喚

new 調用建構函式的表達式會遵守與被視為傳回所建構之型別的方法調用相同的規則。

此外,安全環境是所有物件初始化表達式中各自的參數和運算元的最小安全環境,若有任一初始化表達式存在,則遞迴計算之。

注意:這些規則依賴 Span<T> 不具有以下形式的建構函式:

public Span<T>(ref T p)

這類建構函式會使 Span<T> 實例與 ref 欄位無法區分。 本文件中描述的安全規則依賴於 ref 欄位在 C# 或 .NET 中不為有效構造。 結束註解