共用方式為


15 個類別

15.1 一般

類別是數據結構,可能包含數據成員(常數和欄位)、函式成員(方法、屬性、事件、索引器、運算元、實例建構函式、完成項和靜態建構函式),以及巢狀類型。 類別類型支持繼承,衍生類別可以擴充和特製化基類的機制。

結構 (§16) 和介面 (§18) 具有類似類別的成員,但有某些限制。 此子句定義類別和類別成員的宣告。 結構和介面的子句會根據類別類型中的對應宣告來定義這些類型的限制。

類別宣告

15.2.1 一般

class_declaration 是一種 type_declaration§14.7),用來宣告新類別。

class_declaration
    : attributes? class_modifier* 'partial'? 'class' identifier
        type_parameter_list? class_base? type_parameter_constraints_clause*
        class_body ';'?
    ;

class_declaration由一組選擇性屬性§23) 組成,後面接著一組選擇性class_modifiers (§15.2.2) ,後面接著選擇性partial修飾詞 (§15.2.7) ,後面接著命名類別的關鍵字class識別碼,後面接著選擇性type_parameter_list§15.2.3) ,後面接著選擇性class_base規格 (§15.2.4) ,後面接著一組選擇性type_parameter_constraints_clauses (§15.2.5),後面接著class_body§15.2.6),後面接著分號。

類別宣告不應提供型別參數約束子句,除非它也提供型別參數清單

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

15.2.2 類別修飾符

15.2.2.1 一般

class_declaration可以選擇性地包含一系列的類別修飾詞:

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

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

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

可以對巢狀類別使用new修飾詞。 它指定該類別會以相同名稱隱藏繼承的成員,如§15.3.5中所述。 修飾詞 new 出現在非巢狀類別宣告中會造成編譯時錯誤。

publicprotectedinternalprivate 修飾詞可控制類別的可存取性。 根據類別宣告所在的上下文,有些修飾詞可能不被允許(§7.5.2)。

當部分類型宣告(§15.2.7)包含可存取性規格(透過publicprotectedinternalprivate 修飾詞),該規格必須與所有包含可存取性規格的其他部分一致。 如果局部類型的各部分都未指定存取權限規格,則會使用適當的預設存取權限規格(§7.5.2)。

abstractsealedstatic 修飾詞將在以下小節中討論。

15.2.2.2 抽象類

修飾 abstract 詞用來指出類別不完整,而且它只做為基類使用。 抽象類非抽象類有下列不同之處:

  • 抽象類無法直接實例化,並且在抽象類上使用new運算符會導致編譯時期錯誤。 即使可能存在編譯時類型是抽象的變數和數值,這些變數和數值必須要不是null,就是包含對衍生自抽象類型的非抽象類別實例的參考。
  • 抽象類允許包含抽象成員,但不強制要求包含。
  • 抽象類別不能是密封的。

當一個非抽象類別從抽象類別衍生時,該非抽象類別應包含所有繼承的抽象成員的實際實作,並且覆蓋這些抽象成員。

範例:在下列程式代碼中

abstract class A
{
    public abstract void F();
}

abstract class B : A
{
    public void G() {}
}

class C : B
{
    public override void F()
    {
        // Actual implementation of F
    }
}

抽象類 A 引進抽象方法 F。 類別 B 引進了一個額外的方法 G,但由於未提供 F 的實作,因此 B 也應該宣告為抽象。 Class C 覆蓋了 F 並提供了實際的實現。 由於 中 C沒有抽象成員, C 因此允許(但不需要)為非抽象成員。

結束範例

如果一個類別的部分類型宣告(§15.2.7)中包含abstract修飾詞,那麼該類別為抽象類別。 否則,類別為非抽象。

15.2.2.3 密封類別

修飾符 sealed 用於防止類別被繼承。 如果密封類別指定為另一個類別的基類,就會發生編譯時期錯誤。

密封類別不能同時是抽象類別。

注意sealed 修飾詞主要用於防止非預期的衍生,但也啟用特定的運行時間優化。 特別是,由於已知密封類別永遠不會有任何衍生類別,因此可以將密封類別實例上的虛擬函式成員調用轉換成非虛擬調用。 結尾註釋

如果類別的一個或多個部分類型宣告 ({15.2.7) 包含 sealed 修飾詞,則類別會密封。 否則,該類別將被解除封印。

15.2.2.4 靜態類別

15.2.2.4.1 一般

修飾 static 詞用於將類別標記為靜態類別。 靜態類別不得具現化,不得做為型別使用,且只包含靜態成員。 只有靜態類別可以包含擴充方法的宣告({15.6.10)。

靜態類別宣告受限於下列限制:

  • 靜態類別不得包含 sealedabstract 修飾詞。 不過,由於靜態類別無法具現化或衍生自,因此其行為就像是密封和抽象一樣。
  • 靜態類別不得包含class_base規格(~15.2.4),且無法明確指定基類或實作介面的清單。 靜態類別隱含繼承自 類型 object
  • 靜態類別應只包含靜態成員(~15.3.8)。

    注意:所有常數和巢狀類型都會分類為靜態成員。 結尾註釋

  • 靜態類別不可擁有以protectedprivate protectedprotected internal宣告存取性的成員。

在編譯時違反上述任何限制都是錯誤的。

靜態類別沒有實例建構函式。 無法在靜態類別中宣告實例建構函式,而且靜態類別未提供任何預設實例建構函式(~15.11.5)。

靜態類別的成員不會自動為靜態,而且成員宣告應明確包含 static 修飾詞(常數和巢狀類型除外)。 當類別巢狀於靜態外部類別內時,除非巢狀類別明確包含 static 修飾詞,否則巢狀類別不是靜態類別。

如果類別的部分類型宣告(§15.2.7)中包括static修飾詞,則該類別是靜態的。 否則,類別不是靜態的。

15.2.2.4.2 參考靜態類別類型

namespace_or_type_name§7.8)被允許參考靜態類別時

  • namespace_or_type_name 是在形式為 Tnamespace_or_type_name 中的 T.I,或
  • `namespace_or_type-name 是形式為 Ttypeof_expression (§12.8.18) 中的 typeof(T)。`

primary_expression§12.8)允許參考靜態類別時,

  • primary_expression 是格式 E (§12.8.7) 中的 member_access

在任何其他情境中,參考靜態類別是編譯時錯誤。

注意:例如,將靜態類別當做基類、成員的構成型別(§15.3.7)、泛型型別參數或型別參數限制的錯誤。 同樣地,靜態類別不能用於陣列型別、新運算式、轉型運算式、is 運算式、as 運算式、 sizeof 運算式或預設值運算式。 結尾註釋

15.2.3 類型參數

類型參數是一個簡單的標識符,表示用來建立具體型別之型別引數的占位元。 相反地,類型引數 (§8.4.2) 是在建立建構類型時取代類型參數的類型。

type_parameter_list
    : '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
    ;

decorated_type_parameter
    : attributes? type_parameter
    ;

type_parameter定義於 •8.5 中。

類別宣告中的每個類型參數都會在該類別的宣告空間 ({7.3) 中定義名稱。 因此,它不能與該類別的另一個類型參數或在該類別中宣告的成員具有相同的名稱。 類型參數的名稱不能與類型本身相同。

如果兩個部分泛型型別宣告(在相同的程式中)具有相同的完整名稱(包括類型參數的數目的generic_dimension_specifier§12.8.18),則會產生相同的未綁定的泛型型別。§7.8.3) 兩個這類部分類型宣告應依序為每個類型參數指定相同的名稱。

15.2.4 類別基底規格

15.2.4.1 一般

類別宣告可以包含 class_base 規範,該規範定義了類別的直接基類和類別直接實現的介面(§19)。

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 基類

當class_type包含在class_base時,它會指定所宣告類別的直接基類。 如果一個非部分類別的宣告沒有class_base,或如果class_base中只列出了介面類型,則直接基類假定為object。 當部分類別宣告包含基類規格時,該基類規格應該參考與包含基類規格之該部分所有其他部分相同的類型。 如果部分類別中沒有任何部分包含基類規格,則基類為 object。 類別繼承其直接基類的成員,如§15.3.4中所述。

範例:在下列程式代碼中

class A {}
class B : A {}

類別 A 據說是 的 B直接基類,據說 B 衍生自 A。 由於 A 不會明確指定直接基類,因此其直接基類是隱含的 object

結束範例

針對建構類別型別,包括在泛型型別宣告中宣告的巢狀型別(§15.3.9.7),如果在泛型類別宣告中有指定基類,則此建構型別的基類是透過將基類宣告中的每個 type_parameter,替換為建構型別中相對應的 type_argument 來取得。

範例:假設有以下泛型類別宣告

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

構造型別 G<int> 的基礎類別是 B<string,int[]>

結束範例

類別宣告中指定的基類可以是建構類別類型 ({8.4)。 基類不能是本身的類型參數(~8.5),不過它可以涉及範圍中的類型參數。

範例:

class Base<T> {}

// Valid, non-constructed class with constructed base class
class Extend1 : Base<int> {}

// Error, type parameter used as base class
class Extend2<V> : V {}

// Valid, type parameter used as type argument for base class
class Extend3<V> : Base<V> {}

結束範例

類別類型的直接基類應該至少可以和類別類型本身一樣可存取(~7.5.5)。 例如,若公用類別從私用或內部類別繼承,就會在編譯時期發生錯誤。

類別類型的直接基類不得為下列任何類型:System.ArraySystem.DelegateSystem.EnumSystem.ValueType或 型別dynamic。 此外,泛型類別宣告不得用作 System.Attribute 直接或間接基底類別 (§23.2.1)。

在判斷類別A之直接基類規格B的意義時,會暫時假設的直接基B類為 object,這可確保基類規格的意義不能以遞歸方式相依於本身。

範例:下列內容

class X<T>
{
    public class Y{}
}

class Z : X<Z.Y> {}

發生錯誤,因為在基類規格X<Z.Y>中,Z的直接基類被視為 object,因此(根據第 7.8 節的規則Z不被認為有成員Y

結束範例

類別的基類是直接基類及其基類。 換句話說,基類集合是直接基類關係的傳遞閉包。

範例:在下列內容中:

class A {...}
class B<T> : A {...}
class C<T> : B<IComparable<T>> {...}
class D<T> : C<T[]> {...}

的基類 D<int>C<int[]>B<IComparable<int[]>>Aobject

結束範例

除了 類別 object之外,每個類別都有一個直接基類。 類別 object 沒有直接基類,而且是所有其他類別的最終基類。

類別相依於本身時會出現編譯期錯誤。 就此規則而言,類別 直接相依 於其直接基底類別 (如果有的話) ,並 直接相依 於它立即巢狀的類型 (如果有的話) 。

範例:範例

class A : A {}

是錯誤的,因為類別相依於本身。 同樣地,這個例子

class A : B {}
class B : C {}
class C : A {}

有錯誤,因為這些類別依賴於自己形成了一個循環。 最後,範例

class A : B.C {}
class B : A
{
    public class C {}
}

這會產生編譯時期錯誤,因為 A 取決於 B.C(其直接基類),該類別又取決於 B(其立即封入類別),而這個類別會迴圈取決於 A

結束範例

類別不相依於其內巢狀的類別。

範例:在下列程式代碼中

class A
{
    class B : A {}
}

B 依賴於 A(因為 A 同時是其直接基類和其直接包圍類別),但 A 並不依賴於 B(因為 B 既不是 A 的基類,也不是其包圍類別)。 因此,此範例是有效的。

結束範例

從封閉類別衍生是不可能的。

範例:在下列程式代碼中

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

類別 B 發生錯誤,因為它嘗試衍生自密封類別 A

結束範例

15.2.4.3 介面實作

class_base規格可能包含介面類型清單,在此情況下,類別據說會實作指定的介面類型。 針對構造的類別類型,包括在泛型型別宣告中宣告的巢狀類型(§15.3.9.7),每個實作的介面類型是透過替換指定介面中的每個 type_parameter 為構造類型的對應 type_argument 來獲取的。

在多個元件中宣告之型別的介面集合(\15.2.7)是每個元件上指定之介面的聯集。 一個特定的介面在每個部件上只能命名一次,但多個部件可以命名相同的基本介面。 每個指定介面的成員只能有一個實作。

範例:在下列內容中:

partial class C : IA, IB {...}
partial class C : IC {...}
partial class C : IA, IB {...}

類別的基底介面 C 集合為 IAIBIC

結束範例

通常,每個部分都會提供該部分所宣告介面的實作;然而,這並不是必要的要求。 一個部件可以為另一個部件中聲明的介面提供實現。

範例:

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

結束範例

類別宣告中指定的基底介面可以是建構介面類型 (§8.4§19.2) 。 基底介面本身不能是型別參數,不過它可以牽涉到範圍中的型別參數。

範例:下列程式代碼說明類別如何實作和擴充建構的類型:

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

結束範例

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

15.2.5 類型參數條件約束

泛型型別和方法宣告可以選擇性地藉由包含 type_parameter_constraints_clause來指定類型參數條件約束。

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

每個 type_parameter_constraints_clause 都包含標記 where,後面接著類型參數的名稱,後面接著冒號和該類型參數的條件約束清單。 每個類型參數最多可以有一個 where 子句,而且 where 子句可以依任何順序列出。 與屬性訪問器中 getset 令牌類似,where 令牌不是關鍵字。

子句中 where 提供的條件約束清單可以包含下列任何元件,順序如下:單一主要條件約束、一個或多個次要條件約束,以及建構函式條件約束 new()

主要條件約束可以是類別類型、參考類型約束class值類型約束struct非 null 約束notnull非受控型別約束unmanaged。 類別類型和參考型別限制可以包含 nullable_type_annotation

次要條件約束可以是interface_typetype_parameter,並且可以選擇性地接著nullable_type_annotationnullable_type_annotation的存在表示類型引數可以是符合條件約束的非可空參考型別所對應的可空參考型別。

參考型別條件約束會指定用於型別參數的類型自變數應該是參考型別。 所有類別類型、介面類型、委派類型、數位類型和類型參數已知為參考類型(如下所述)都滿足此條件約束。

類別類型、參考型別限制和次要限制可以包含可為空的型別註解。 類型參數上此註釋的存在與否表示類型引數的可為空性預期。

  • 如果該限制條件不包含可空類型註釋,則預期類型參數為非可空引用類型。 如果類型自變數是可為 Null 的參考型別,編譯程式可能會發出警告。
  • 如果約束條件包含可空性類型註解,則不可為 Null 的參考型別和可為 Null 的參考型別都符合約束條件。

型別引數的可空性不需要與型別參數的可空性相符。 如果類型參數的 Null 性不符合類型自變數的 Null 屬性,編譯程式可能會發出警告。

注意:若要指定類型自變數是可為 Null 的參考型別,請勿將可為 Null 的類型批注新增為條件約束(使用 T : classT : BaseClass),但在整個泛型宣告中使用 T? 來指出類型自變數的對應可為 Null 參考型別。 結尾註釋

可為 Null 的類型註解 ?只能用於具有值類型條件約束、不含 nullable_type_annotation的參考類型條件約束,或沒有 nullable_type_annotation的類別類型條件約束。

範例:下列範例顯示類型引數的可為空性如何影響其類型參數宣告的可為空性:

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

當類型自變數是不可為 Null 的類型時, ? 類型批注會指出參數是對應的可為 Null 的類型。 當類型引數已經是一個可為 null 的引用類型時,參數就是相同的可為 null 類型。

結束範例

非 Null 條件約束會指定用於類型參數的類型引數應該是不可為 Null 的值類型或不可為 Null 的參考類型。 允許類型自變數不是不可為 Null 的實值型別或不可為 Null 的參考型別,但編譯器可能會產生診斷警告。

由於 notnull 不是關鍵詞,因此在 primary_constraint 中,非空條件約束一律與 class_type語法上模棱兩可。 基於相容性考量,如果名稱的查詢(notnull)成功,則必須將其視為class_type。 否則,它應被視為非 Null 條件約束。

範例:下列類別示範針對不同條件約束使用各種類型自變數,指出編譯程式可能會發出的警告。

#nullable enable
public class C { }
public class A<T> where T : notnull { }
public class B1<T> where T : C { }
public class B2<T> where T : C? { }
class Test
{
    static void M()
    {
        // nonnull constraint allows nonnullable struct type argument
        A<int> x1;
        // possible warning: nonnull constraint prohibits nullable struct type argument
        A<int?> x2;
        // nonnull constraint allows nonnullable class type argument
        A<C> x3;
        // possible warning: nonnull constraint prohibits nullable class type argument
        A<C?> x4;
        // nonnullable base class requirement allows nonnullable class type argument
        B1<C> x5;
        // possible warning: nonnullable base class requirement prohibits nullable
        // class type argument
        B1<C?> x6;
        // nullable base class requirement allows nonnullable class type argument
        B2<C> x7;
        // nullable base class requirement allows nullable class type argument
        B2<C?> x8;
    }
}

實值型別條件約束指定用於類型參數的類型自變數應該是不可為 Null 的實值型別。 具有非 Null 結構類型的所有不可為 Null 的結構類型、列舉型別和具有實值型別約束的型別參數都滿足此約束。 請注意,雖然分類為實值型別,但具可空性的實值型別(~8.3.12)不符合實值型別條件約束。 具有值類型約束的類型參數不應該有constructor_constraint,但它可以用作另外一個具有constructor_constraint的類型參數的型別實參。

注意System.Nullable<T> 類型指定了 T 的不可為 Null 的實值型別約束。 因此,遞歸構造的類型 T??Nullable<Nullable<T>> 是被禁止的。 結尾註釋

非受控類型約束會指定用於型別參數的型別引數必須是不可為 Null 的非受控類型(§8.8)。

因為 unmanaged 不是一個關鍵字,在 primary_constraint 中,非受管理的約束總是與 class_type 存在語法上的模棱兩可。 基於相容性考量,如果對名稱 `` 進行的名稱查詢(unmanaged)成功,則會將其視為 `class_type`。 否則會將其視為未受管理的限制。

指標類型永遠不允許成為型別引數,而且即使是非受控類型,也無法滿足任何型別限制。

如果條件約束是類別類型、介面類型或類型參數,該類型會指定用於該類型參數之每個類型自變數都應該支援的最小「基底類型」。 每當使用構造型別或泛型方法時,就會在編譯期間針對型別參數的約束條件檢查型別引數。 提供的型別自變數應符合 \8.4.5 中所述的條件。

class_type條件約束應滿足下列規則:

  • 此類型應該是類別類型。
  • 類型不得為 sealed
  • 類型不得為下列其中一種類型: System.ArraySystem.ValueType
  • 類型不得為 object
  • 指定型別參數的一個條件約束最多可以是類別類型。

指定為 interface_type 條件約束的類型應符合下列規則:

  • 此類型應該是介面類型。
  • 在特定的 where 子句中,類型不得被指定多次。

不論是哪一種情況,條件約束都可能牽涉到關聯型別或方法宣告的任何型別參數作為建構型別的一部分,而且可能涉及所宣告的類型。

任何指定為型別參數條件約束的類別或介面類型,至少都必須以宣告的泛型型別或方法一樣可存取 (~7.5.5)。

指定為 type_parameter 條件約束的類型應符合下列規則:

  • 此類型應該是類型參數。
  • 在特定的 where 子句中,類型不得被指定多次。

此外,類型參數的相依性圖表中不得有迴圈,其中相依性是所定義的可轉移關聯:

  • 如果類型參數T被用作類型參數S的約束,則SST
  • 如果類型參數S相依於類型參數T,而T相依於類型參數U,那麼S相依於U

根據此關聯,類型參數依賴自身(直接或間接)是編譯時錯誤。

任何條件約束都應該在相依型別參數之間保持一致。 如果類型參數 S 相依於類型參數 T ,則:

  • T 不應有實值型別條件約束。 否則,T將被有效地封閉,因此S將被迫與T是相同類型,消除了需要兩個類型參數的必要性。
  • 如果 S 具有實值型別條件約束,則 T 不應該有 class_type 條件約束。
  • 如果 S 具有 class_type 條件約束,且 A 具有 T 條件約束,則應該有從 B 的識別轉換或隱含參考轉換,或從 AB 的隱含參考轉換。
  • 如果S也依賴於型別參數U,並且U具有class_type約束,且A具有T約束,那麼必須存在從B的識別轉換或隱性參考轉換,或者從AB的隱性參考轉換。

擁有值型別約束的S和擁有參考型別約束的T是有效的。 實際上,此限制 T 類型 System.ObjectSystem.ValueTypeSystem.Enum和 任何介面類型。

where如果型別參數的 子句包含建構函式條件約束(其格式new()為 ),您可以使用 new 運算符來建立型別的實例(^12.8.17.2)。 任何用於具有建構函式條件約束之類型參數的類型自變數都應該是實值型別、具有公用無參數建構函式的非抽象類,或是具有實值型別條件約束或建構函式條件約束的類型參數。

type_parameter_constraints包含primary_constraintstructunmanaged且具有constructor_constraint的情況下,將會產生編譯時錯誤。

範例:以下是條件約束的範例:

interface IPrintable
{
    void Print();
}

interface IComparable<T>
{
    int CompareTo(T value);
}

interface IKeyProvider<T>
{
    T GetKey();
}

class Printer<T> where T : IPrintable {...}
class SortedList<T> where T : IComparable<T> {...}

class Dictionary<K,V>
    where K : IComparable<K>
    where V : IPrintable, IKeyProvider<K>, new()
{
    ...
}

下列範例發生錯誤,因為它會在類型參數的相依性圖形中造成迴圈:

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

下列範例說明其他無效的情況:

class Sealed<S,T>
    where S : T
    where T : struct // Error, `T` is sealed
{
    ...
}

class A {...}
class B {...}

class Incompat<S,T>
    where S : A, T
    where T : B // Error, incompatible class-type constraints
{
    ...
}

class StructWithClass<S,T,U>
    where S : struct, T
    where T : U
    where U : A // Error, A incompatible with struct
{
    ...
}

結束範例

一種類型的動態抹除是以下列方式建構的:

  • 如果 C 是巢狀類型 Outer.Inner ,則 Cₓ 為巢狀類型 Outerₓ.Innerₓ
  • 如果 CCₓ是具有型別自變數G<A¹, ..., Aⁿ>的建構型A¹, ..., Aⁿ別,則 Cₓ 為建構型別 G<A¹ₓ, ..., Aⁿₓ>
  • 如果 C 是陣列型態 E[] ,則 Cₓ 為陣列型態 Eₓ[]
  • 如果 C 為動態,則 Cₓobject
  • 否則,CₓC

類型參數的有效基類定義如下:

讓我們 R 成為一組類型,以便:

  • 對於類型參數的每個限制 TR 都包含其實際的基類。
  • 對於每個結構型別的約束 TR 都包含 System.ValueType
  • 對於每個屬於枚舉類型的T約束,R包含System.Enum
  • 對於T的每個作為委派類型的約束,R包含其動態刪除。
  • 針對每個是陣列類型的約束 TR 包含 System.Array
  • 對於類別類型的每個限制 TR 都包含其動態刪除。

然後

  • 如果 T 具有實值型別條件約束,則其有效基類為 System.ValueType
  • 否則,如果 R 是空的,則有效的基類為 object
  • 否則,T 的有效基類是集合 中最包含的類型(參見 R)。 如果集合沒有包含的類型,則的有效基類 Tobject。 一致性規則可確保最包含的類型存在。

如果類型參數是方法類型參數,其條件約束繼承自基底方法,則有效的基類會在類型替代之後計算。

這些規則可確保有效的基類一律是 class_type

型參數的有效接口集定義如下:

  • 如果沒有Tsecondary_constraints,則其有效介面集是空的。
  • 如果 T 具有 interface_type 條件約束,但沒有 type_parameter 條件約束,則其有效介面集是其 interface_type 條件約束的動態清除集合。
  • 如果沒有interface_type條件約束,但具有type_parameter條件約束,則其有效介面集是其type_parameter條件約束的有效介面集的聯集。
  • 如果 T 同時具有 interface_type 條件約束和 type_parameter 條件約束,則其有效介面集是其 interface_type 條件約束的動態清除集合與其 type_parameter 條件約束之有效介面集合的聯集。

如果類型參數具有參考型別約束或其有效的基類不是 object,則該類型參數被認為是參考型別。 已知類型參數為不可為 Null 的參考型別,是指當它確定為參考型別且具有不可為 Null 的參考型別約束時。

限制型別參數類型的值可用來存取條件約束所隱含的實例成員。

範例:在下列內容中:

interface IPrintable
{
    void Print();
}

class Printer<T> where T : IPrintable
{
    void PrintOne(T x) => x.Print();
}

IPrintable 方法可以直接在 x 上調用,因為 T 被限制必須始終實作 IPrintable

結束範例

當部分泛型型別宣告包含條件約束時,條件約束應與包含條件約束的所有其他部分一致。 具體來說,包含條件約束的每個部分都應該有相同類型參數集的條件約束,而且對於每個類型參數,主要、次要和建構函式條件約束的集合應相等。 如果條件約束包含相同的成員,則兩組條件約束相等。 如果部分泛型類型沒有任何部分指定類型參數條件約束,則類型參數會被視為不受限制。

範例:

partial class Map<K,V>
    where K : IComparable<K>
    where V : IKeyProvider<K>, new()
{
    ...
}

partial class Map<K,V>
    where V : IKeyProvider<K>, new()
    where K : IComparable<K>
{
    ...
}

partial class Map<K,V>
{
    ...
}

正確,因為包含條件約束的元件(前兩個)會分別針對同一組類型參數指定相同的主要、次要和建構函式條件約束集。

結束範例

15.2.6 類別主體

類別 的class_body 會定義該類別的成員。

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 部分型別宣告

在多個元件中定義類別、結構或介面類型時,會使用 修飾 partial 詞。 partial 修飾詞是內容關鍵詞 (),而且在關鍵詞 classstructinterface之前具有特殊意義。 (部分類型可能包含部分方法宣告 (15.6.9)。

每個部分型別宣告都應包含 partial 修飾詞,並且應在與其他部分相同的命名空間或包含型別中宣告。 partial修飾詞表示類型宣告的其他部分可能存在於別處,但並非必定需要存在此類額外部分;僅包含partial修飾詞的類型宣告也是有效的。 僅有一個部分類型的宣告可以包含基類或已實作的介面是有效的。 然而,所有基底類別或實作介面的宣告必須匹配,包括任何指定型別參數的可空性。

部分類型的所有部分都應該一起編譯,以便元件可以在編譯階段合併。 部分類型特別不允許已經編譯的型別進行擴充。

巢狀類型可以使用 partial 修飾詞,在多個部分中宣告。 一般而言,包含型別也會使用 partial 宣告,而且巢狀類型的每個部分都會在包含型別的不同部分中宣告。

範例:下列部分類別會在位於不同編譯單位的兩個部分中實作。 第一部分是由資料庫對應工具機器生成的,而第二部分則是手動撰寫的。

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

當上述兩個部分一起編譯時,產生的程式代碼的行為就如同已撰寫為單一單元一樣,如下所示:

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

結束範例

§23.3 討論了部分類型聲明不同部分的類型或類型參數上指定的屬性的處理。

15.3 類別成員

15.3.1 一般

某類別的成員包含其 class_member_declaration 所引進的成員,以及繼承自直接的基底類別的成員。

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

一個班級的成員被分為以下幾個類別:

  • 常數,代表與 類別相關聯的常數值(~15.4)。
  • 欄位,這是類別的變數(~15.5)。
  • 方法,實作 類別可執行的計算和動作(~15.6)。
  • 屬性,定義具名特性,以及與讀取和寫入這些特性相關聯的動作(~15.7)。
  • 事件,定義可由 類別產生的通知(~15.8)。
  • 索引器允許類別實例像陣列一樣進行索引(§15.9)。
  • 運算子,這些運算子定義了可以應用於類別實例的表達式運算子(§15.10)。
  • 實例建構函式,實作初始化 類別實例所需的動作 (~15.11
  • 終結器實現類別實例在永久捨棄前所需執行的動作(§15.13)。
  • 靜態建構函式,實作初始化類別本身所需的動作(~15.12)。
  • 型別,代表類別內的局部型別(§14.7)。

class_declaration創建一個新的宣告空間(§7.3),type_parameterclass_member_declaration由class_declaration立即引入新的成員到此宣告空間。 下列規則適用於 class_member_declarations:

  • 實例建構函式、完成項和靜態建構函式的名稱應該與立即封入類別相同。 所有其他成員的名稱必須與最接近的包裝類的名稱不同。

  • 類別宣告type_parameter_list型別參數的名稱應該與相同type_parameter_list中所有其他類型參數的名稱不同,而且與類別的名稱和類別的所有成員名稱不同。

  • 型別的名稱應該與相同類別中宣告的所有非類型成員的名稱不同。 如果兩個或多個類型宣告共用相同的完整名稱,宣告應具有 partial 修飾詞 ({15.2.7),而且這些宣告會結合來定義單一類型。

注意:因為類型宣告的完整名稱會編碼類型參數的數目,所以只要兩個不同的類型參數數目不同,就可能會共用相同的名稱。 結尾註釋

  • 常數、欄位、屬性或事件的名稱應該與相同類別中宣告的所有其他成員名稱不同。

  • 方法的名稱應該與相同類別中宣告的所有其他非方法名稱不同。 此外,方法的簽章 ({7.6) 應該與相同類別中宣告之所有其他方法的簽章不同,而相同類別中宣告的兩個方法不得具有與 、 inout完全ref不同的簽章。

  • 實例建構函式的簽章應與相同類別中宣告的所有其他實例建構函式的簽章不同,而相同類別中宣告的兩個建構函式其簽章不能僅僅因refout的不同而有所區別。

  • 索引器的簽章應該與相同類別中宣告之所有其他索引器的簽章不同。

  • 運算子的簽章應該與相同類別中宣告之所有其他運算符的簽章不同。

類別的繼承成員 ({15.3.4) 不是類別宣告空間的一部分。

注意:因此,允許衍生類別宣告與繼承成員同名或具有相同簽章的成員(這實際上會隱藏繼承的成員)。 結尾註釋

宣告在多個部分中的類型之成員集合(§15.2.7)是每個部分中所宣告成員的聯集。 類型宣告之所有部分的主體會共用相同的宣告空間({7.3),而每個成員的範圍({7.7)則延伸到所有元件的主體。 任何成員的可及性範圍一律包含其所屬類型的所有部分。一個部分中宣告的私人成員可以從其他部分自由存取。 除非該成員具有 partial 修飾詞,否則在定義型別的不同區塊中宣告相同的成員是編譯時期錯誤。

範例:

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M();        // Ok, defining partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int y;
    }
}

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M() { }     // Ok, implementing partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int z;
    }
}

結束範例

欄位初始化順序在 C# 程式代碼中可能相當重要,而且會提供一些保證,如 \15.5.6.1 中所定義。 否則,類型內成員的順序很少重要,但在與其他語言和環境交集時可能會相當重要。 在這些情況下,在多個元件中宣告的類型內成員順序是未定義的。

15.3.2 實例類型

每個類別宣告都有相關聯的實例類型 針對泛型類別宣告,實例類型是藉由從類型宣告中建構一個構造類型(§8.4)來形成,提供的每個型別參數都是對應的型別參數。 由於實例類型使用型別參數,因此只能在型別參數有效範圍內使用,即在類別宣告內。 實例型別是適用於類別宣告中撰寫的程式碼的this。 對於非泛型類別,實例類型只是宣告的類別。

範例:下列範例顯示數個類別宣告及其實例類型:

class A<T>             // instance type: A<T>
{
    class B {}         // instance type: A<T>.B
    class C<U> {}      // instance type: A<T>.C<U>
}
class D {}             // instance type: D

結束範例

15.3.3 建構型別的成員

建構型別的非繼承成員是藉由用建構型別的對應 type_argument 替換成員宣告中的每個 type_parameter 來獲得的。 替代程式是以類型宣告的語意意義為基礎,而不只是文字替代。

範例:給定泛型類別宣告

class Gen<T,U>
{
    public T[,] a;
    public void G(int i, T t, Gen<U,T> gt) {...}
    public U Prop { get {...} set {...} }
    public int H(double d) {...}
}

建構的類型 Gen<int[],IComparable<string>> 具有下列成員:

public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}

泛型類別宣告中成員a的類型為「二維陣列Gen」,因此上述建構型別中成員T的類型為「單維陣列的二維數組a」,或 intint[,][]

結束範例

在實例函式成員內,this 的型別是包含宣告的實例類型(§15.3.2)。

泛型類別的所有成員都可以直接從任何封入類別使用類型參數,或做為建構型別的一部分。 當運行時間使用特定的封閉式建構型別(~8.4.3)時,每次使用類型參數時,都會以提供給建構型別的類型自變數取代。

範例:

class C<V>
{
    public V f1;
    public C<V> f2;

    public C(V x)
    {
        this.f1 = x;
        this.f2 = this;
    }
}

class Application
{
    static void Main()
    {
        C<int> x1 = new C<int>(1);
        Console.WriteLine(x1.f1);              // Prints 1

        C<double> x2 = new C<double>(3.1415);
        Console.WriteLine(x2.f1);              // Prints 3.1415
    }
}

結束範例

15.3.4 繼承

類別 繼承其直接基類的成員。 繼承表示類別隱含地包含其直接基類的所有成員,但基類的實例建構函式、完成項和靜態建構函式除外。 繼承的一些重要層面包括:

  • 繼承是可轉移的。 如果 C 衍生自 B,而 B 衍生自 A,則會 C 繼承 中 B 宣告的成員,以及中 A宣告的成員。

  • 衍生類別 擴充其直接基類。 衍生類別可以在其繼承的成員中新增新的成員,但無法移除所繼承成員的定義。

  • 實例建構函式、完成項和靜態建構函式不會繼承,但所有其他成員都是,不論其宣告的存取範圍(~7.5)。 但是,根據它們宣告的可存取性,繼承的成員在衍生類別中可能無法存取。

  • 衍生類別可以藉由宣告具有相同名稱或簽章的新成員來隱藏繼承的成員(§7.7.2.3)。 不過,隱藏繼承的成員不會移除該成員,它只會讓該成員無法直接透過衍生類別存取。

  • 類別的實例包含類別及其基底類別中宣告的所有實例屬性集合,從衍生類別類型到其任何基底類別類型的隱式轉換(§10.2.8)是存在的。 因此,某些衍生類別之實例的參考可以視為其任何基類之實例的參考。

  • 類別可以宣告虛擬方法、屬性、索引器和事件,而衍生類別可以覆寫這些函式成員的實作。 這可讓類別顯示多型行為,其中函式成員調用所執行的動作會根據叫用該函式成員的實例運行時間類型而有所不同。

建構類別類型的繼承成員是直接基類型的成員(§15.2.4.2),其可透過將建構型別的型別自變數替換到基類規範base_class_specification中每個對應的型別參數來找到。 接著,這些成員會被轉換為,以base_class_specification中的每個type_argument替換成員宣告中的相應type_parameter。

範例:

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

在上述程式碼中,建構型別D<int>具有一個公用的非繼承成員intG(string s),通過將型別參數int替換為型別自變數T而獲得。 D<int> 也具有類別宣告 B的繼承成員。 この繼承的成員首先是透過在基類規格B<int[]>中將D<int>取代int來判斷基類類型T以及B<T[]>。 然後,作為 B 的型別參數,int[] 會取代 Upublic U F(long index) 中,從而生成繼承的成員 public int[] F(long index)

結束範例

15.3.5 新修飾詞

類別成員宣告可以宣告一個與繼承成員具有相同名稱或簽章的成員。 發生這種情況時,會說衍生類別成員會 隱藏 基類成員。 如需成員隱藏繼承成員時的精確規格,請參閱 •7.7.2.3

繼承成員 M 被認為是 可用 的條件是 M 可被訪問,並且沒有其他繼承且可被訪問的成員 N 已經隱藏了 M。 隱含隱藏繼承的成員不會被視為錯誤,但編譯程式應該發出警告,除非衍生類別成員的宣告包含 new 修飾詞,以明確指出衍生成員是要隱藏基底成員。 如果巢狀類型的一或多個部分宣告 ({15.2.7) 包含 new 修飾詞,如果巢狀類型隱藏可用的繼承成員,則不會發出任何警告。

如果在一個宣告中包含了 new 修飾符,但該修飾符並未隱藏任何可用的繼承成員,則會發出相關的警告。

15.3.6 存取修飾詞

class_member_declaration可以具有任何一種允許的宣告可見度(第 7.5.2 節):publicprotected internalprotectedprivate protectedinternal,或 private。 除了 protected internalprivate protected 的組合之外,指定多於一個存取修飾詞會導致編譯時錯誤。 當class_member_declaration不包含任何存取修飾詞時,會假定為private

15.3.7 組成類型

成員宣告中使用的類型稱為該成員的 組成類型s。 可能的組成類型是常數、字段、屬性、事件或索引器、方法或運算元的傳回型別,以及方法、索引器、運算符或實例建構函式的參數類型。 成員的組成類型至少可以和該成員本身一樣可存取(~7.5.5)。

15.3.8 靜態和實例成員

類別的成員是 靜態成員s 或 實例成員s。

注意:一般而言,將靜態成員視為屬於類別和實例成員屬於物件(類別的實例)會很有用。 結尾註釋

當欄位、方法、屬性、事件、運算元或建構函式宣告包含 static 修飾詞時,它會宣告靜態成員。 此外,常數或類型宣告會隱含宣告靜態成員。 靜態成員具有下列特性:

  • 當靜態成員M成員訪問§12.8.7)中被引用時,E.M應表示具有成員E的類型。 E 表示實例時會產生編譯時錯誤。
  • 非泛型類別中的靜態字段會確切識別一個儲存位置。 無論建立多少個非泛型類別的實例,靜態欄位始終只有一個副本。 不論封閉式建構型別的實例數目為何,每個相異的封閉式建構型別 (~8.4.3) 都有自己的靜態字段集。
  • 靜態函式成員(方法、屬性、事件、運算符或建構函式)不會在特定實例上運作,而且在這類函式成員中參考這個是編譯時間錯誤。

當欄位、方法、屬性、事件、索引器、建構函式或完成項宣告不包含靜態修飾詞時,它會宣告實例成員。 (實例成員有時稱為非靜態成員。實例成員具有下列特性:

  • 在形式 M§12.8.7)中參考實例成員 時,E.M 應表示具有成員 E 的類型實例。 這是 E 在表示型別時的綁定時間錯誤。
  • 類別的每個實例都包含類別之所有實例欄位的個別集合。
  • 實例函式成員(方法、屬性、索引器、實例建構函式或完成項)會在類別的指定實例上運作,而且這個實例可以存取為 this~12.8.14)。

範例:下列範例說明存取靜態和實例成員的規則:

class Test
{
    int x;
    static int y;
    void F()
    {
        x = 1;               // Ok, same as this.x = 1
        y = 1;               // Ok, same as Test.y = 1
    }

    static void G()
    {
        x = 1;               // Error, cannot access this.x
        y = 1;               // Ok, same as Test.y = 1
    }

    static void Main()
    {
        Test t = new Test();
        t.x = 1;       // Ok
        t.y = 1;       // Error, cannot access static member through instance
        Test.x = 1;    // Error, cannot access instance member through type
        Test.y = 1;    // Ok
    }
}

方法 F 顯示,在實例函式成員中, simple_name12.8.4) 可用來存取實例成員和靜態成員。 方法G指出,在靜態函式成員中,透過「simple_name」存取實例成員會在編譯時發生錯誤。 Main方法顯示,在member_access§12.8.7)中,實例成員應透過實例存取,而靜態成員則應透過類型存取。

結束範例

15.3.9 巢狀類型

15.3.9.1 一般

在類別、結構或介面內宣告的類型稱為 巢狀類型。 在編譯單位或命名空間內宣告的類型稱為 非巢狀類型

範例:在下列範例中:

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

類別 B 是巢狀類型,因為它是在 類別 A內宣告,而 類別 A 是非巢狀類型,因為它是在編譯單位內宣告。

結束範例

15.3.9.2 完全限定名稱

巢狀類型宣告的完整名稱(§7.8.3)是 S.N,其中 S 是類型 N 所在宣告的完整名稱,而 N 是巢狀類型宣告的不合格名稱(§7.8.2),包括任何 generic_dimension_specifier§12.8.18)。

15.3.9.3 宣告的存取性

非巢狀類型可以宣告 publicinternal 存取權限,且預設會宣告 internal 存取權限。 巢狀類型也可以有這些形式的宣告協助工具,以及一或多種宣告的協助工具,視包含類型是類別、結構還是介面而定:

  • 在類別中宣告的巢狀類型可以有任何允許的宣告性可存取性,且與其他類別成員一樣,預設為 private 宣告的可存取性。
  • 在結構中宣告的巢狀類型可以有三種存取層級形式之一:publicinternalprivate,並且與其他結構成員一樣,預設為private的存取層級。

範例:範例

public class List
{
    // Private data structure
    private class Node
    {
        public object Data;
        public Node? Next;

        public Node(object data, Node? next)
        {
            this.Data = data;
            this.Next = next;
        }
    }

    private Node? first = null;
    private Node? last = null;

    // Public interface
    public void AddToFront(object o) {...}
    public void AddToBack(object o) {...}
    public object RemoveFromFront() {...}
    public object RemoveFromBack() {...}
    public int Count { get {...} }
}

宣告私人巢狀類別 Node

結束範例

15.3.9.4 隱藏

巢狀類型可能會隱藏基底成員(~7.7.2.2)。 允許在巢狀類型的宣告中使用new修飾詞(§15.3.5),以便能夠明確表達隱藏的意圖。

範例:範例

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

顯示巢狀類別M,隱藏 中M定義的方法Base

結束範例

15.3.9.5 此存取

巢狀型別及其包含的類型在 this_access 方面沒有特殊關聯性(§12.8.14)。 具體而言, this 在巢狀類型內無法用來參考包含型別的實例成員。 如果巢狀類型需要存取其包含型別的實例成員,則可以透過將包含型別的實例作為建構函式參數,為巢狀型別提供存取。

範例:下列範例

class C
{
    int i = 123;
    public void F()
    {
        Nested n = new Nested(this);
        n.G();
    }

    public class Nested
    {
        C this_c;

        public Nested(C c)
        {
            this_c = c;
        }

        public void G()
        {
            Console.WriteLine(this_c.i);
        }
    }
}

class Test
{
    static void Main()
    {
        C c = new C();
        c.F();
    }
}

顯示此技術 一個 C 的實例會建立一個 Nested 的實例,並將其自身的 Nested 傳遞給 Nested 的建構函式,以便後續存取 的實例成員。

結束範例

15.3.9.6 存取封裝類型的私有及受保護成員

巢狀類型可以存取其包含型別可存取的所有成員,包括那些具有 privateprotected 宣告的存取性的包含型別成員。

範例:範例

class C
{
    private static void F() => Console.WriteLine("C.F");

    public class Nested
    {
        public static void G() => F();
    }
}

class Test
{
    static void Main() => C.Nested.G();
}

顯示一個類別C,其包含一個巢狀類別Nested。 在Nested中,方法G會呼叫F中定義的靜態方法C,而F具有私有宣告的存取權限。

結束範例

巢狀類型也可以存取在其包含類型之基底類型中定義的受保護成員。

範例:在下列程式代碼中

class Base
{
    protected void F() => Console.WriteLine("Base.F");
}

class Derived: Base
{
    public class Nested
    {
        public void G()
        {
            Derived d = new Derived();
            d.F(); // ok
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.Nested n = new Derived.Nested();
        n.G();
    }
}

巢狀類別Derived.Nested透過F的實例來呼叫Derived的基類Base中定義的受保護方法Derived

結束範例

泛型類別中的 15.3.9.7 巢狀類型

泛型類別宣告可能包含巢狀類型宣告。 封入類別的類型參數可以在巢狀類型內使用。 巢狀類型宣告可能包含僅適用於巢狀類型的其他類型參數。

泛型類別宣告中包含的每個類型宣告都是隱含泛型型別宣告。 撰寫泛型型別內巢狀型別的引用時,應該命名包含具體化型別,包括其型別參數。 不過,從外部類別內,無需限定符即可使用巢狀類型; 構造巢狀類型時可以隱式使用外部類別的實例類型。

範例:下列顯示三種不同的正確方式來參考從 Inner建立的建構型別;前兩個是相等的:

class Outer<T>
{
    class Inner<U>
    {
        public static void F(T t, U u) {...}
    }

    static void F(T t)
    {
        Outer<T>.Inner<string>.F(t, "abc");    // These two statements have
        Inner<string>.F(t, "abc");             // the same effect
        Outer<int>.Inner<string>.F(3, "abc");  // This type is different
        Outer.Inner<string>.F(t, "abc");       // Error, Outer needs type arg
    }
}

結束範例

雖然程式設計樣式不正確,但巢狀類型中的類型參數可以隱藏在外部類型中宣告的成員或類型參數。

範例:

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

結束範例

15.3.10 保留成員名稱

15.3.10.1 一般

為了方便基礎 C# 運行時間實作,針對屬於屬性、事件或索引器的每個來源成員宣告,實作應根據成員宣告、其名稱及其類型 ({15.3.10.2, \15.3.10.3, \15.3.10.3, {15.3.10.4) 來保留兩個方法簽章。 程序在編譯時如果宣告的成員,其簽名與相同範圍內已宣告的成員所保留的簽名一致,即使基礎的執行時期實作不使用這些保留,也會出現錯誤。

保留名稱不會引入宣告,因此不會參與成員查找。 不過,宣告的相關聯保留方法簽章會參與繼承(§15.3.4),而且可以使用new修飾詞隱藏(§15.3.5)。

注意:這些名稱的保留有三個用途:

  1. 若要允許基礎實作使用一般標識碼做為方法名稱,以取得或設定 C# 語言功能的存取權。
  2. 為了讓其他語言能夠使用普通識別符號作為方法名稱與 C# 語言功能進行互操作,以獲得擷取或設定存取。
  3. 為了協助確保一個符合編譯程式接受的來源是由另一個編譯程式所接受,方法是讓所有 C# 實作中的保留成員名稱細節保持一致。

結尾註釋

終結器的宣告(§15.13)也會導致保留簽名(§15.3.10.5)。

某些名稱會保留作為運算符方法名稱(§15.3.10.6)。

15.3.10.2 成員名稱保留給屬性

針對屬性P)的類型T,保留以下簽章:

T get_P();
void set_P(T value);

這兩個簽章都會保留,即使屬性是只讀或唯寫的。

範例:在下列程式代碼中

class A
{
    public int P
    {
        get => 123;
    }
}

class B : A
{
    public new int get_P() => 456;

    public new void set_P(int value)
    {
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        Console.WriteLine(a.P);
        Console.WriteLine(b.P);
        Console.WriteLine(b.get_P());
    }
}

類別A會定義唯讀屬性P,因此會保留 和 get_P 方法的set_P簽章。 A 類別 B 衍生自 A ,並隱藏這兩個保留簽章。 此範例會產生輸出:

123
123
456

結束範例

15.3.10.3 為事件保留的成員名稱

對於委派類型E的事件T),保留以下簽章:

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 為索引保留的成員名稱

對於類型§15.9且參數清單為T的索引器L,保留以下簽章:

T get_Item(L);
void set_Item(L, T value);

即使索引器是只讀或僅可寫的,這兩個簽章仍然會被保留。

此外,成員名稱 Item 已保留。

成員名稱保留給終結器

針對包含終結器(§15.13)的類別,保留以下簽章:

void Finalize();

15.3.10.6 保留給運算子的方法名稱

下列方法名稱是保留的。 雖然在此規格中有許多運算符都有相對應的運算符,但有些運算符會保留給未來版本使用,而有些則保留供與其他語言的互操作性之用。

方法名稱 C# 運算子
op_Addition + (二進位)
op_AdditionAssignment (保留)
op_AddressOf (保留)
op_Assign (保留)
op_BitwiseAnd & (二進位)
op_BitwiseAndAssignment (保留)
op_BitwiseOr \|
op_BitwiseOrAssignment (保留)
op_CheckedAddition (保留供日後使用)
op_CheckedDecrement (保留供日後使用)
op_CheckedDivision (保留供日後使用)
op_CheckedExplicit (保留供日後使用)
op_CheckedIncrement (保留供日後使用)
op_CheckedMultiply (保留供日後使用)
op_CheckedSubtraction (保留供日後使用)
op_CheckedUnaryNegation (保留供日後使用)
op_Comma (保留)
op_Decrement -- (前置詞和後置詞)
op_Division /
op_DivisionAssignment (保留)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (保留)
op_Explicit 明確的(縮小範圍的)強制
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit 隱式(擴展)強制轉換
op_Increment ++ (前置詞和後置詞)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (保留)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (保留)
op_LogicalNot !
op_LogicalOr (保留)
op_MemberSelection (保留)
op_Modulus %
op_ModulusAssignment (保留)
op_MultiplicationAssignment (保留)
op_Multiply * (二進位)
op_OnesComplement ~
op_PointerDereference (保留)
op_PointerToMemberSelection (保留)
op_RightShift >>
op_RightShiftAssignment (保留)
op_SignedRightShift (保留)
op_Subtraction - (二進位)
op_SubtractionAssignment (保留)
op_True true
op_UnaryNegation -(一元)
op_UnaryPlus +(一元)
op_UnsignedRightShift (保留供日後使用)
op_UnsignedRightShiftAssignment (保留)

15.4 常數

常數是代表常數值的類別成員:可在編譯時期計算的值。 constant_declaration 用於宣告一個或多個指定類型的常數。

constant_declaration
    : attributes? constant_modifier* 'const' type constant_declarators ';'
    ;

constant_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    ;

constant_declaration可以包含一組屬性§23)、new修飾符 (§15.3.5) ,以及任何一種允許的宣告協助工具類型 (§15.3.6) 。 屬性和修飾詞會應用於由 constant_declaration 宣告的所有成員。 即使常數被視為靜態成員, constant_declaration 不需要也不允許 static 修飾詞。 同一個修飾詞在常數宣告中出現多次是錯誤的。

constant_declaration的類型會指定宣告所引進的成員類型。 類型之後是一個constant_declarator列表(§13.6.3),每個都引入一個新成員。 constant_declarator是由命名成員的識別碼所組成,後面接著 「=“ 權杖,後面接著提供成員值的constant_expression§12.25)。

常數宣告中指定的類型應該是sbytebyteshortushortintuintlongulongcharfloatdoubledecimalboolstringenum_type,或reference_type。 每個 constant_expression 都應產生目標型別的值,或可由隱含轉換轉換成目標型別的類型值(§10.2)。

數的類型 至少可以和常數本身一樣可存取(~7.5.5)。

常數的值是在表達式 中使用simple_nameὖ12.8.4) 或 member_access 取得 (~12.8.7)。

常數本身可以參與 constant_expression。 因此,常數可用於任何需要 constant_expression的建構中。

注意:這類建構的範例包括 case 標籤、 goto case 語句、 enum 成員宣告、屬性和其他常數宣告。 結尾註釋

附註: 如 §12.25 所述, constant_expression 是可以在編譯階段完全評估的運算式。 由於除了以外,建立string類型的非空值的唯一方法是套用new運算子,而且因為new運算子不允許在constant_expression中使用,所以除了以外,string型常數的唯一可能值是null結尾註釋

當需要為常數值指定一個符號名稱時,但該值的類型在常數宣告中不被允許,或 constant_expression 無法在編譯時期計算出該值時,可以改用只讀欄位(第15.5.3節)。

注意constreadonly 的版本語意存在不同(§15.5.3.3)。 結尾註釋

宣告多個常數的常數宣告相當於具有相同屬性、修飾詞和型別之單一常數的多個宣告。

範例:

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

等同於

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

結束範例

只要相依性不是循環性質,常數就允許相依於相同程式中的其他常數。

範例:在下列程式代碼中

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

編譯程式必須先評估 A.Y,然後評估 B.Z,最後評估 A.X,產生值 101112

結束範例

常數宣告可能相依於其他程式的常數,但這類相依性只能在單一方向進行。

範例:參考上述範例,如果 AB 在個別程式中被宣告,則可能 A.X 會相依於 B.Z,但 B.Z 無法同時相依於 A.Y結束範例

15.5 欄位

15.5.1 一般

欄位是代表與對象或類別相關聯的變數的成員。 field_declaration 引進指定類型的一個或多個欄位。

field_declaration
    : attributes? field_modifier* type variable_declarators ';'
    ;

field_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'readonly'
    | 'volatile'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

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

field_declaration可以包含一組屬性§23)、修new飾符 (§15.3.5)、四個存取修飾符的有效組合 (§15.3.6) 和修static飾符 (§15.5.2)。 此外,field_declaration可能包含readonly修飾詞(§15.5.3)或volatile修飾詞(§15.5.4),但不能同時包含兩者。 屬性和修飾詞會套用至field_declaration宣告的所有成員。 同一 個修飾詞在field_declaration中出現多次是錯誤的。

field_declarationtype 指定了由該宣告引入的成員類型。 此類型後面接著一份variable_declarator的清單,其中每個宣告器都會引進新的成員。 變數宣告符包含一個識別碼,該識別碼命名該成員,選擇性地後面接著「=」標記以及變數初始化器§15.5.6),該初始化器提供該成員的初始值。

欄位的類型應該至少可以和字段本身一樣可存取(~7.5.5)。

域的值是在表達式中使用simple_name取得的(\12.8.4)、member_access\12.8.7)或base_access(\12.8.15)。 非唯讀欄位的值會使用 指派§12.23) 進行修改。 非唯讀欄位的值可以使用後置詞遞增和遞減運算子 (§12.8.16) 以及前置詞遞增和遞減運算子 (§12.9.7) 來取得和修改。

宣告多個字段的欄位宣告相當於具有相同屬性、修飾詞和類型的單一字段的多個宣告。

範例:

class A
{
    public static int X = 1, Y, Z = 100;
}

等同於

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

結束範例

15.5.2 靜態和實例欄位

當欄位宣告包含 static 修飾符時,宣告引入的欄位是 靜態欄位s。 當不存在修 static 飾符時,宣告引入的欄位是 實例欄位s。 靜態欄位和實例欄位是 C# 支援的數種變數 (§9) 中的兩種,有時它們分別稱為 靜態變數s 和 實例變數s。

如 •15.3.8 中所述,類別的每個實例都包含 類別的完整實例字段集,而每個非泛型類別或封閉建構型別只有一組靜態字段,不論類別或封閉式建構型別的實例數目為何。

15.5.3 唯讀欄位

15.5.3.1 一般

field_declaration 包含 readonly 修飾符時,宣告引入的欄位是 唯讀欄位s。 直接指派至只讀欄位只能當做該宣告的一部分,或在同一類別的實例建構函式或靜態建構函式中發生。 在這些情境中,可以多次指派值給唯讀欄位。具體來說,只有在以下情境中才允許直接指派至唯讀欄位:

  • 在引入欄位的 variable_declarator 中(即在宣告中包含 variable_initializer)。
  • 對於實例欄位,在包含欄位宣告之類別的實例建構函式中,不包括本機函式和匿名函式,且僅在正在建構的實例上。 針對靜態欄位,在靜態建構函式或包含欄位宣告之類別中的靜態欄位或屬性初始化運算式中,不包括區域函式和匿名函式。 這些也是唯一有效的情境,可以作為輸出或參考參數傳遞唯讀欄位。

嘗試賦值給唯讀欄位,或將它作為輸出或參考參數傳遞到其他上下文,是編譯時錯誤。

15.5.3.2 針對常數使用靜態只讀字段

當需要為常數值提供符號名稱時,靜態唯讀字段很有用,但當該值的類型不被允許在 const 宣告中使用,或該值無法在編譯時期計算時,靜態唯讀字段更為適合。

範例:在下列程式代碼中

public class Color
{
    public static readonly Color Black = new Color(0, 0, 0);
    public static readonly Color White = new Color(255, 255, 255);
    public static readonly Color Red = new Color(255, 0, 0);
    public static readonly Color Green = new Color(0, 255, 0);
    public static readonly Color Blue = new Color(0, 0, 255);

    private byte red, green, blue;

    public Color(byte r, byte g, byte b)
    {
        red = r;
        green = g;
        blue = b;
    }
}

BlackWhiteRedGreenBlue 成員無法宣告為 const 成員,因為無法在編譯時期計算其值。 不過,宣告它們 static readonly 反而具有完全相同的效果。

結束範例

15.5.3.3 常數和靜態只讀字段的版本控制

常數和只讀欄位有不同的二進位版本設定語意。 當表達式參考常數時,常數的值會在編譯階段取得,但是當表達式參考只讀欄位時,直到運行時間才會取得域的值。

範例:請考慮由兩個不同的程式所組成的應用程式:

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Program1Program2 命名空間表示兩個個別編譯的程式。 由於 Program1.Utils.X 被宣告為 static readonly 欄位,因此 Console.WriteLine 語句在編譯時期無法得知其輸出值,而是在運行時間才取得。 因此,如果的值 X 已變更並 Program1 重新編譯,即使未重新編譯, Console.WriteLine 語句也會輸出新的值 Program2 。 不過,如果X為常數,則在編譯X時會取得Program2的值,並且在重新編譯Program1之前,不會受到Program2中的變更影響。

結束範例

15.5.4 易失性字段

field_declaration 包含 volatile 修飾符時,該宣告所引入的欄位是 揮發性欄位s。 對於非易失性欄位,重新排序指令的優化技術可能會在多線程程式中導致非預期且無法預測的結果,尤其是這些程式在存取欄位時,未使用諸如 lock_statement 所提供的同步處理(§13.13)。 這些優化可由編譯程式、運行時間系統或硬體來執行。 對於揮發性欄位,這類重新排序優化會受到限制:

  • 易失性欄位的讀取稱為volatile 讀取。 揮發性讀取具有「取得語意」;也就是說,在指令序列中,它保證會先於任何後續的記憶體參考發生。
  • 動態欄位的寫入稱為動態寫入 揮發性寫入具有「釋放語意」;也就是說,在指令序列中,保證在寫入指令之前的任何記憶體參考之後發生。

這些限制確保所有的執行緒將會觀察到其他任何執行緒所執行的 volatile 寫入,並按照其執行的順序進行。 符合規範的實作不需要提供從所有執行緒觀察到的揮發性寫入的單一總排序。 揮發性欄位的類型應該是下列其中一項:

  • 一個參考類型
  • 被確認為參考型別的type_parameter§15.2.5)。
  • 類型bytesbyteshortushortintuintcharfloatboolSystem.IntPtrSystem.UIntPtr
  • 具有 enum_base 類型的 enum_type,其類型可以是 bytesbyteshortushortintuint

範例:範例

class Test
{
    public static int result;
    public static volatile bool finished;

    static void Thread2()
    {
        result = 143;
        finished = true;
    }

    static void Main()
    {
        finished = false;

        // Run Thread2() in a new thread
        new Thread(new ThreadStart(Thread2)).Start();    

        // Wait for Thread2() to signal that it has a result
        // by setting finished to true.
        for (;;)
        {
            if (finished)
            {
                Console.WriteLine($"result = {result}");
                return;
            }
        }
    }
}

產生下列輸出:

result = 143

在此範例中,方法 Main 會啟動執行 方法 Thread2的新線程。 這個方法會將值儲存到稱為 result的非揮發性欄位,然後將 儲存 true 在 volatile 欄位中 finished。 主線程會等候欄位 finished 設定為 true,然後讀取 欄位 result。 由於 finished 已宣告 volatile,因此主線程應該從欄位143讀取 值result。 如果欄位finished尚未宣告volatile,那麼允許在對result的存放操作之後,存放到的可見性確保給主線程,並因此主線程可以從欄位finished讀取值 0。 宣告 finishedvolatile 欄位可防止任何這類不一致。

結束範例

15.5.5 欄位初始化

欄位的初始值,無論是靜態字段還是實例欄位,都是欄位類型的預設值 (~9.3)。 在發生這個預設初始化之前,無法觀察欄位的值,因此字段永遠不會「未初始化」。

範例:範例

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

產生輸出

b = False, i = 0

因為 bi 都會自動初始化為預設值。

結束範例

15.5.6 變數初始值

15.5.6.1 一般

欄位宣告可能包含 variable_initializer。 針對靜態欄位,變數初始化表達式會對應至類別初始化期間執行的指派語句。 針對實例欄位,變數初始化表達式會對應至建立 類別實例時所執行的指派語句。

範例:範例

class Test
{
    static double x = Math.Sqrt(2.0);
    int i = 100;
    string s = "Hello";

    static void Main()
    {
        Test a = new Test();
        Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}");
    }
}

產生輸出

x = 1.4142135623730951, i = 100, s = Hello

因為當靜態欄位初始化運算式執行時,x 會被指派,而當實例欄位初始化運算式執行時,is 會被指派。

結束範例

§15.5.5 中描述的預設值初始化適用於所有欄位,包括那些具有變數初始化器的欄位。 因此,當類別初始化時,該類別中的所有靜態欄位都會先初始化為預設值,然後以文字順序執行靜態字段初始化表達式。 同樣地,建立類別的實例時,該實例中的所有實例欄位都會先初始化為預設值,然後以文字順序執行實例字段初始化表達式。 當在相同類型的多個局部類型宣告中有欄位宣告時,各部分的順序是未指定的。 不過,在每個部分內,字段初始化表達式會依序執行。

可以觀察到具有變數初始化器的靜態欄位處於其預設值的狀態。

範例:然而,由於樣式的考量,強烈不建議這樣做。 範例

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

表現此行為。 儘管ab都有循環定義,但程式仍然有效。 這會產生結果輸出

a = 1, b = 2

因為靜態欄位 ab 會在執行初始化表示式之前初始化為 0 (預設值 int) 。 當 a 的初始化器運行時,b 的值是零,因此 a 被初始化為 1。 當 b 執行初始化時,a 的值已經是 1,因此 b 被初始化為 2

結束範例

15.5.6.2 靜態欄位初始化

類別的靜態欄位初始化器對應於按照它們在類別宣告中出現的文字順序執行的賦值順序(§15.5.6.1)。 在部分類別中,「文字順序」的意義是由 \15.5.6.1 指定。 如果類別中存在靜態建構函式 (~15.12),則在執行該靜態建構函式之前,會立即執行靜態字段初始化表達式。 否則,靜態欄位初始化器會在第一次使用該類別的靜態欄位之前,於依實作而定的時間執行。

範例:範例

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    public static int X = Test.F("Init A");
}

class B
{
    public static int Y = Test.F("Init B");
}

可能會產生兩者之一的輸出:

Init A
Init B
1 1

或者輸出:

Init B
Init A
1 1

X的初始化表達式和Y的初始化表達式可能會依任意順序執行;它們只需在被這些欄位引用之前執行。 不過,在範例中:

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    static A() {}
    public static int X = Test.F("Init A");
}

class B
{
    static B() {}
    public static int Y = Test.F("Init B");
}

輸出應為:

Init B
Init A
1 1

因為當靜態建構函式執行時的規則(如 §15.12中所定義)規定, 的靜態建構函式(因此 的靜態欄位初始化器)應在 的靜態建構函式及欄位初始化器之前執行。

結束範例

15.5.6.3 實例欄位初始化

類別的實例欄位變數的初始化設定會對應到一個在進入該類別的任何一個實例建構函式(§15.11.3)時立即執行的指派序列。 在部分類別中,「文字順序」的意義是由 \15.5.6.1 指定。 變數初始化表達式會以出現在類別宣告中的文字順序執行({15.5.6.1)。 類別實例建立和初始化程式會在 •15.11進一步說明。

實例欄位的變數初始化表達式無法參考所建立的實例。 因此,在變數初始化時參考this會產生編譯時錯誤,因為變數初始化表達式中若使用簡單名稱來參考任何實例成員也會產生編譯時錯誤。

範例:在下列程式代碼中

class A
{
    int x = 1;
    int y = x + 1;     // Error, reference to instance member of this
}

y 的變數初始化表達式會導致編譯時期錯誤,因為它引用了正在建立的實例的成員。

結束範例

15.6 方法

15.6.1 一般

§15.6 及其子句涵蓋類別中的方法宣告。 該文本由結構 (§16.4) 和接口 (§19.4.3) 中宣告方法的相關信息增強。

「方法」是實作物件或類別所能執行之計算或動作的成員。 方法使用method_declaration宣告。

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

文法注意事項:

  • unsafe_modifier§24.2) 僅適用於不安全代碼 (§24)。
  • 辨識 method_body 時,如果 null_conditional_invocation_expression表達式 兩者的替代方案都適用,則應選擇前者。

注意:此處的替代方案重疊與優先順序完全是出於描述上的方便;文法規則可以更詳細地闡述以消除重疊。 ANTLR 和其他文法系統採用相同的便利性,因此 method_body 自動具有指定的語意。 結尾註釋

method_declaration可以包括一組屬性§23) 和允許的聲明無障礙類型之一 (§15.3.6)、new§15.3.5)、 static§15.6.3)、 virtual§15.6.4)、 override§15.6.5)、sealed§15.6.6)、(§15.6.7abstract)、extern§15.6.8) 和 async§15.14)。 此外,直接由struct_declaration包含的method_declaration可能包含readonly修飾詞(§16.4.12)。

如果下列所有條件都成立,宣告就有有效的修飾符組合:

  • 宣告包含有效存取修飾詞的組合(§15.3.6)。
  • 宣告不會多次包含相同的修飾詞。
  • 宣告最多包含下列其中一個修飾詞: staticvirtualoverride
  • 宣告最多包含下列其中一個修飾詞: newoverride
  • 如果宣告包含 abstract 修飾詞,則宣告不包含下列任何修飾詞: staticvirtualsealedextern
  • 宣告可以包含 和abstractoverride修飾詞,讓抽象成員可以覆寫虛擬成員。
  • 如果宣告包含 private 修飾詞,則宣告不包含下列任何修飾詞: virtualoverrideabstract
  • 如果宣告包含 sealed 修飾詞,則宣告也會包含 override 修飾詞。
  • 如果宣告包含 partial 修飾詞,則不包含下列任何修飾詞:newpublicprotectedinternalprivatevirtualsealedoverrideabstract或 。extern

方法會根據它們是否傳回任何內容來進行分類:

  • 如果 ref 存在,該方法是 returns-by-ref 並返回一個變數引用,這個引用可以選擇性地設為唯讀;
  • 否則,如果 return_typevoid,則該方法為 不傳回值 並且不會傳回任何值。
  • 否則,該方法會以 傳回值方式 傳回並返回一個值。

某個傳回值或不返回值的方法宣告中的return_type會指定該方法所傳回結果的類型,如果有結果的話。 只有不會傳回值的方法可以包含 partial 修飾詞 (§15.6.9)。 如果宣告包含 async 修飾詞,則 return_type 應該是 void 或方法按值傳回,而傳回類型是 任務類型§15.14.1)。

在方法中,返回引用的返回方式的ref_return_type宣告,指定了由variable_reference所引用的變數類型。

泛型方法是方法,其宣告包含 type_parameter_list。 這會指定 方法的類型參數。 選擇性 type_parameter_constraints_clause指定型別參數的條件約束。

具有修飾詞或用於明示介面成員實作的泛型override會從被覆寫的方法或介面成員分別繼承型別參數的條件約束。 這類宣告可能只有包含 primary_constraints classs ,struct其意義在此內容中分別定義在 §15.6.5§19.6.2 中,以覆寫方法和明確介面實作。

member_name指定方法的名稱。 除非方法是明確的介面成員實作 (§19.6.2) ,否則 member_name 只是 識別碼

針對明確的介面成員實作,member_nameinterface_type 構成,後面跟著 “.” 和 識別字。 在此情況下,宣告不得包含 (可能) externasync以外的修飾詞。

選擇性 parameter_list 會指定方法的參數(~15.6.2)。

return_typeref_return_type,以及方法 parameter_list 中所參考的每個型別,必須至少和方法本身同樣具有可存取性(§7.5.5)。

一個回傳值或不回傳值的方法的 method_body 可以是分號、區塊主體或表達式主體。 區塊主體是由 區塊所組成,它會指定要在叫用 方法時執行的語句。 表達式主體包含 =>,後面接著 null_conditional_invocation_expression表達式,以及分號,並表示叫用 方法時要執行的單一表達式。

對於 abstract 和 extern 方法,method_body 只由分號組成。 對於部分方法, method_body 可能包含分號、區塊主體或表達式主體。 對於所有其他方法, method_body 為區塊主體或表達式主體。

如果method_body包含分號,則宣告不得包含 async 修飾詞。

返回參考方法的ref_method_body可以是分號、區塊體或運算式體。 區塊主體是由 區塊所組成,它會指定要在叫用 方法時執行的語句。 表達式主體由 =>refvariable_reference和分號組成,表示在方法被呼叫時需要評估的單一 variable_reference

對於抽象和 extern 方法, ref_method_body 只包含分號;對於所有其他方法, ref_method_body 為區塊主體或表達式主體。

名稱、類型參數數目,以及方法的參數清單會定義方法的簽章 ({7.6)。 具體來說,方法的簽章包含其名稱、其類型參數的數量,以及其參數的數量、parameter_mode_modifier§15.6.2.1),及其參數的類型。 傳回型別不是方法簽章的一部分,也不是參數的名稱、類型參數的名稱或條件約束。 當參數類型參考方法的類型參數時,類型參數的序數位置(而非類型參數的名稱)會用於類型等價。

方法的名稱應該與相同類別中宣告的所有其他非方法名稱不同。 此外,方法的簽章應該與相同類別中宣告之所有其他方法的簽章不同,而在相同類別中宣告的兩個方法,其簽章不可僅因 inoutref 而有所不同。

方法的 type_parameter 在整個 method_declaration 的作用範圍內可用,可用來在此範圍內的 return_typeref_return_typemethod_bodyref_method_body 中形成類型,以及 type_parameter_constraints_clause 中,但不能在 屬性 中使用。

所有參數和類型參數都應該有不同的名稱。

15.6.2 方法參數

15.6.2.1 一般

方法的參數,如果有的話,由方法的 parameter_list宣告。

parameter_list
    : fixed_parameters
    | fixed_parameters ',' parameter_array
    | parameter_array
    ;

fixed_parameters
    : fixed_parameter (',' fixed_parameter)*
    ;

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

default_argument
    : '=' expression
    ;

parameter_modifier
    : parameter_mode_modifier
    | 'this' parameter_mode_modifier?
    | parameter_mode_modifier? 'this'
    ;

parameter_mode_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

parameter_array
    : attributes? 'params' array_type identifier
    ;

參數清單包含一或多個逗號分隔參數,其中只有最後一個 參數可能是parameter_array

fixed_parameter由一組可選的屬性組成 (§23);選用in的 、 outrefthis修飾元、類型識別碼和選用default_argument。 每個 fixed_parameter 都會宣告具有指定名稱之指定型別的參數。 this修飾詞會將 方法指定為擴充方法,而且只能在非泛型非巢狀靜態類別中靜態方法的第一個參數上使用。 如果參數是 struct 型別或受限於 struct 型別的型別參數,則 this 修飾詞可能會與 ref 修飾詞或 in 修飾詞結合,但不能與 out 修飾詞結合。 擴充方法會在 •15.6.10進一步說明。 具有固定參數預設引數稱為選擇性參數,而固定參數不含預設引數則是必要參數。 必要的參數不得出現在parameter_list選擇性參數之後。

具有 refoutthis 修飾詞的參數不能有 default_argument。 輸入參數可能有 default_argumentdefault_argument中的運算式需是下列其中一項:

  • 常數表達式
  • 格式 new S() 的表達式,其中 S 是實值型別
  • 格式 default(S) 的表達式,其中 S 是實值型別

表達式應可隱含地透過識別或可為 Null 的轉換,轉換為參數的類型。

如果選擇性參數出現在實作部分方法宣告 (§15.6.9)、明確介面成員實作 (§19.6.2) 、單一參數索引子宣告 (§15.9) 或運算子宣告 (§15.10.1) 中,編譯器應該發出警告,因為永遠無法以允許省略引數的方式叫用這些成員。

parameter_array由一組可選的屬性§23)、修params飾符、array_type識別碼組成。 參數陣會宣告具有指定名稱之指定數位類型的單一參數。 參數 陣列的array_type 必須是單一維度陣列類型(~17.2)。 在方法調用中,參數陣列允許指定單一的給定陣列類型的引數,或者允許指定零個或多個陣列元素類型的引數。 參數陣列會在 •15.6.2.4進一步說明。

parameter_array可能會在選擇性參數之後出現,但不能有預設值。若省略對parameter_array的參數,則會建立一個空陣列。

範例:下列說明不同類型的參數:

void M<T>(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

parameter_listM中,i是必要的ref參數,d是必要的值參數,bsot是可選值參數,而且a是參數陣列。

結束範例

方法宣告會為參數和類型參數建立個別的宣告空間 ({7.3)。 名稱會透過類型參數清單和方法的參數清單,來導入此宣告空間。 如果有的話,方法的主體會被視為嵌套於此宣告空間中。 方法宣告空間的兩個成員具有相同名稱是錯誤的。

方法調用 (~12.8.10.2) 會建立方法參數和局部變數的特定複本,而調用的自變數清單會將值或變數參考指派給新建立的參數。 在方法的區塊內,參數可以在simple_name表達式中由其標識符參考(§12.8.4)。

下列型態的參數存在:

注意:如 {7.6 中所述inoutref 修飾詞是方法簽章的一部分,但 params 修飾詞不是 。 結尾註釋

15.6.2.2 值參數

沒有修飾詞宣告的參數是值參數 value 參數是局部變數,可從方法調用中提供的對應自變數取得其初始值。

如需明確指派規則,請參閱 \9.2.5

方法調用中的對應自變數應該是隱含轉換成參數型別的表達式(~10.2)。

允許方法將新的值指派給 value 參數。 這類指派只會影響 value 參數所代表的本機儲存位置,這不會影響方法調用中指定的實際自變數。

15.6.2.3 參照參數

15.6.2.3.1 一般

輸入、輸出和參考參數是按引用參數。 參考參數是局部參考變數 (~9.7):初始參考項是從方法調用中提供的對應自變數取得。

注意:參考參數的參考可以使用 ref assignment (= ref) 運算符來變更。

當參數是傳址參數時,方法調用中的對應引數應包含對應的關鍵字inrefout,接著是與參數類型相同的變數引用§9.5)。 不過,當參數是in參數時,傳入的引數可以是一個表達式,而該表達式到對應參數類型的隱含轉換(§10.2)是存在的。

在宣告為迭代器(§15.15)或非同步函數(§15.14)的函數中,不允許使用按參考傳遞的參數。

在採用多個參考參數的方法中,多個名稱可以代表相同的儲存位置。

15.6.2.3.2 輸入參數

使用 in 修飾詞宣告的參數是 輸入參數。 對應至輸入參數的自變數是方法調用點的現有變數,或是方法調用中實作所建立的變數(~12.6.2.3)。 如需明確指派規則,請參閱 \9.2.8

在編譯時期,將輸入參數的值進行修改是錯誤的。

注意:輸入參數的主要用途是提高效率。 當方法參數的類型是大型結構時(就記憶體需求而言),在呼叫 方法時,避免複製自變數的整個值會很有用。 輸入參數可讓方法參考記憶體中現有的值,同時提供保護以防止這些值不必要的變更。 結尾註釋

15.6.2.3.3 參考參數

使用 ref 修飾詞宣告的參數是 參考參數。 如需明確指派規則,請參閱 \9.2.6

範例:範例

class Test
{
    static void Swap(ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Main()
    {
        int i = 1, j = 2;
        Swap(ref i, ref j);
        Console.WriteLine($"i = {i}, j = {j}");
    }
}

產生輸出

i = 2, j = 1

Swap中,Main的使用中,x表示i,而y表示j。 因此,調用的結果是交換 ij 的值。

結束範例

範例:在下列程式代碼中

class A
{
    string s;
    void F(ref string a, ref string b)
    {
        s = "One";
        a = "Two";
        b = "Three";
    }

    void G()
    {
        F(ref s, ref s);
    }
}

對於FG,在s中調用a會傳遞對b的引用。 因此,針對該呼叫,名稱 sab 都參考相同的儲存位置,而三個指派都會修改實例欄位 s

結束範例

在某個struct型別中,於實例方法、實例存取子(§12.2.1)或具有建構函式初始化表達式的實例建構函式中,this關鍵字的行為完全與結構型別的參考參數相同(§12.8.14)。

15.6.2.3.4 輸出參數

使用 out 修飾詞宣告的參數是 輸出參數。 如需明確指派規則,請參閱 \9.2.7

宣告為部分方法的方法 (~15.6.9) 不應該有輸出參數。

注意:輸出參數通常用於產生多個傳回值的方法。 結尾註釋

範例:

class Test
{
    static void SplitPath(string path, out string dir, out string name)
    {
        int i = path.Length;
        while (i > 0)
        {
            char ch = path[i - 1];
            if (ch == '\\' || ch == '/' || ch == ':')
            {
                break;
            }
            i--;
        }
        dir = path.Substring(0, i);
        name = path.Substring(i);
    }

    static void Main()
    {
        string dir, name;
        SplitPath(@"c:\Windows\System\hello.txt", out dir, out name);
        Console.WriteLine(dir);
        Console.WriteLine(name);
    }
}

此範例會產生輸出:

c:\Windows\System\
hello.txt

請注意, dirname 變數可以在傳遞至 SplitPath之前取消指派,而且在呼叫之後會被視為絕對指派。

結束範例

15.6.2.4 參數陣列

使用 params 修飾詞宣告的參數是參數陣列。 如果參數清單包含參數陣列,它應該是清單中的最後一個參數,而且應該為單一維度陣列類型。

範例:類型 string[]string[][] 可以作為參數陣列的類型使用,但類型 string[,] 不能。 結束範例

注意:無法將params修飾詞與inoutref修飾詞結合。 結尾註釋

參數陣語允許在方法調用的兩種方式之一中指定自變數:

  • 為參數陣列指定的自變數可以是可隱含轉換成參數陣列類型的單一表達式(~10.2)。 在此情況下,參數陣列的行為與實值參數類似。
  • 或者,調用可以為參數陣列指定零個或多個引數,其中每個引數都是一個可以隱含轉換為參數陣列元素類型的表達式。 在此情況下,調用會建立參數陣列類型的實例,其長度會對應至參數數目,使用指定的參數值初始化陣列實例的元素,並使用新建立的陣列實例作為實際參數。

除了在調用中允許變數數量的引數之外,參數陣列完全等同於相同類型的值參數(§15.6.2.2)。

範例:範例

class Test
{
    static void F(params int[] args)
    {
        Console.Write($"Array contains {args.Length} elements:");
        foreach (int i in args)
        {
            Console.Write($" {i}");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        int[] arr = {1, 2, 3};
        F(arr);
        F(10, 20, 30, 40);
        F();
    }
}

產生輸出

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

第一次叫用 F 時,僅將陣列 arr 作為值參數傳遞。 F 的第二次調用會自動建立一個具有指定元素值的四個元素的 int[],並將該陣列實例作為值參數傳遞。 同樣地,的第三個調用 F 會建立零元素 int[] ,並將該實例當做值參數傳遞。 第二次和第三次調用完全等同於寫作:

F(new int[] {10, 20, 30, 40});
F(new int[] {});

結束範例

執行多載解析時,具有參數陣列的方法可能適用,可以是以一般形式或展開形式(§12.6.4.2)。 只有在方法的一般形式不適用,以及相同類型中尚未宣告與展開形式相同簽名的適用方法時,才能使用方法的展開形式。

範例:範例

class Test
{
    static void F(params object[] a) =>
        Console.WriteLine("F(object[])");

    static void F() =>
        Console.WriteLine("F()");

    static void F(object a0, object a1) =>
        Console.WriteLine("F(object,object)");

    static void Main()
    {
        F();
        F(1);
        F(1, 2);
        F(1, 2, 3);
        F(1, 2, 3, 4);
    }
}

產生輸出

F()
F(object[])
F(object,object)
F(object[])
F(object[])

在此範例中,具有參數數位之方法的兩種可能展開形式已經包含在類別中,做為一般方法。 因此,在執行重載解析時,不會考慮這些擴展形式,第一個和第三個方法調用會選取一般方法。 當類別宣告具有參數陣語的方法時,也並不罕見地包含某些擴充形式做為一般方法。 如此一來,可以避免在調用含有參數陣列的方法的展開形式時,所發生的陣列實例配置。

結束範例

陣列是參考型別,因此傳遞給參數陣列的值可以是 null

範例:範例:

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

產生下列輸出:

True
False

第二個調用會產生 False ,因為它相當於 F(new string[] { null }) ,並傳遞包含單一 Null 參考的陣列。

結束範例

當參數陣列的類型為 object[]時,方法的一般形式與單 object 一參數的展開形式之間可能會產生模棱兩可。 本身模棱兩可的原因是 object[] 能隱含地轉換為型別 object。 不過,模糊不清並沒有問題,因為可以視需要加入類型轉換來解決。

範例:範例

class Test
{
    static void F(params object[] args)
    {
        foreach (object o in args)
        {
            Console.Write(o.GetType().FullName);
            Console.Write(" ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        object[] a = {1, "Hello", 123.456};
        object o = a;
        F(a);
        F((object)a);
        F(o);
        F((object[])o);
    }
}

產生輸出

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

F的第一次和最後一次使用中,F的常規形式是適用的,因為引數類型到參數類型之間存在隱含轉換(兩者都是object[]型別)。 因此,多載解析會選取的 F一般形式,並將 自變數當做一般值參數傳遞。 在第二和第三個調用中,的一般形式 F 不適用,因為自變數類型沒有隱含轉換成參數類型(類型 object 無法隱含轉換成類型 object[])。 然而,F的擴展形式是適用的,因此通過重載解析選擇了它。 因此,一個元素 object[] 是由調用所建立,而且陣列的單一元素會使用指定的自變數值初始化(這本身是的 object[]參考)。

結束範例

15.6.3 靜態和實例方法

當方法宣告包含 static 修飾詞時,該方法會稱為靜態方法。 當沒有任何 static 修飾詞存在時,方法會說為實例方法。

靜態方法不會在特定實例上運作,在靜態方法中參考 this 將導致編譯時期錯誤。

實例方法會在類別的指定實例上運作,而且該實例可以存取為 this~12.8.14)。

靜態和實例成員之間的差異會在 \15.3.8進一步討論。

15.6.4 虛擬方法

當實例方法宣告包含虛擬修飾詞時,該方法會稱為虛擬方法 當沒有任何虛擬修飾詞存在時,方法會說為 非虛擬方法

非虛擬方法的實作不一致:不論在宣告方法的類別實例或衍生類別的實例上叫用方法,實作都相同。 相反地,虛擬方法的實作可由衍生類別取代。 取代繼承之虛擬方法實作的程序稱為覆寫實作該方法(§15.6.5)

在虛擬方法調用中, 執行該調用的實例運行時間類型 會決定要叫用的實際方法實作。 在非虛擬方法調用中, 實例的編譯時間類型 是判斷因素。 在精確的層面上,當具有編譯時間類型 N 和運行時間類型 A 的實例上以參數列表 C 調用名為 R 的方法時,其調用的處理方式如下:RC 或者從 C 派生的類。

  • 在系結時,多載解析會套用至CNA,以從M所宣告和繼承的方法集合中選取特定方法C。 這在第§12.8.10.2 中說明。
  • 然後在運行時:
    • 如果 M 非虛擬方法, M 則會叫用 。
    • 否則,M是虛擬方法,並且會叫用相對於MR最終衍生的實作。

對於類別所宣告或繼承的每個虛擬方法,該類別都有該方法的 最衍生實作。 與類別M相關的虛擬方法R的最衍生實作是根據以下內容確定的:

  • 如果 R 包含用以引入的虛擬宣告M,那麼這是相較於MR的最衍生實作。
  • 否則,如果R包含對M的覆寫,則這是相對於MR最衍生實作。
  • 否則,M 相對於 R 的最衍生實作與 M 相對於 R 的直接基類的最衍生實作相同。

範例:下列範例說明虛擬和非虛擬方法之間的差異:

class A
{
    public void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public new void F() => Console.WriteLine("B.F");
    public override void G() => Console.WriteLine("B.G");
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        a.F();
        b.F();
        a.G();
        b.G();
    }
}

在此範例中, A 引進非虛擬方法和 F 虛擬方法 G。 類別B加入了新的非虛擬方法F,因此遮蔽了繼承的F,也會覆寫繼承的方法G。 此範例會產生輸出:

A.F
B.F
B.G
B.G

請注意,語句 a.G() 會叫用 B.G,而不是 A.G。 這是因為 實例的運行時間類型 (也就是 B),而不是 實例的編譯時間類型 (也就是 A),會決定要叫用的實際方法實作。

結束範例

因為允許方法隱藏繼承的方法,所以類別可以包含數個具有相同簽章的虛擬方法。 這不會產生歧義問題,因為除了最衍生的方法之外,其他全部都被隱藏。

範例:在下列程式代碼中

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

class B : A
{
    public override void F() => Console.WriteLine("B.F");
}

class C : B
{
    public new virtual void F() => Console.WriteLine("C.F");
}

class D : C
{
    public override void F() => Console.WriteLine("D.F");
}

class Test
{
    static void Main()
    {
        D d = new D();
        A a = d;
        B b = d;
        C c = d;
        a.F();
        b.F();
        c.F();
        d.F();
    }
}

CD 類別包含兩個具有相同簽章的虛擬方法:所A引進的和 所C引進的虛擬方法。 所 C 引進的方法會隱藏繼承自 A的方法。 因此,D 的覆寫宣告會覆寫由 C 引進的方法,而 D 無法覆寫由 A 引進的方法。 此範例會產生輸出:

B.F
B.F
D.F
D.F

請注意,您可以透過一個在該方法不被隱藏的較少衍生類型來存取D的實例,以叫用隱藏的虛擬方法。

結束範例

15.6.5 覆蓋方法

當實例方法宣告包含override修飾詞時,方法會稱為覆寫方法。 覆寫方法使用相同的簽名覆寫繼承的虛擬方法。 雖然虛擬方法宣告 引進 了新的方法,但覆寫方法宣告 會藉由提供該方法的新實作來特製化 現有的繼承虛擬方法。

覆寫宣告所覆寫的方法稱為覆寫基底方法。針對在類別M中宣告的覆寫方法C,覆寫的基底方法取決於檢查每個基類的C,開始於直接基類C,並繼續針對每個後續的直接基類,一直到有一個在指定的基類類型中可存取的方法,其簽章在替換類型參數後與M相同。 為了尋找覆寫的基底方法,如果一個方法是 public,或是 protected,或是 protected internal,或者如果它是 internalprivate protected 並且與 C 在同一個程式中宣告,則該方法被視為可存取。

覆寫方法會繼承覆寫基底方法的任何 type_parameter_constraints_clause

除非覆寫宣告的下列所有條件都成立,否則將發生編譯階段錯誤:

  • 如上所述,可以找到已覆蓋的基礎方法。
  • 正好有一個這樣的覆蓋基礎方法。 只有在基類類型是建構型別時,此限制才會生效,其中型別自變數的替代會讓兩個方法的簽章相同。
  • 被覆寫的基底方法是一個虛擬方法、抽象方法或覆寫方法。 換句話說,被覆蓋的基礎方法不能是靜態的或非虛擬的。
  • 被重寫的基礎方法不是密封方法。
  • 重載基礎方法的返回類型與覆蓋方法之間存在類型一致轉換。
  • 覆寫宣告與被覆寫的基底方法具有相同的宣告可見性。 換句話說,覆寫宣告無法變更虛擬方法的存取範圍。 不過,如果覆寫的基底方法是受保護的內部,並且它是在不同於包含覆寫宣告之組件的元件中宣告,則覆寫宣告的宣告存取性應為受保護。
  • type_parameter_constraints_clause只能包含classstruct,並套用至type_parameter,這些類型參數根據繼承的條件約束被已知為參考型別或值型別。 覆寫方法簽章中表單 T? 的任何類型,其中 T 是類型參數,會解譯如下:
    • 如果為 class 類型參數 T 加入條件約束,則 T? 為可為 Null 的參考型別,否則為
    • 如果沒有新增條件約束,或已新增struct條件約束,則對於類型參數TT?會是可為 Null 的實值型別。

範例:下列示範覆寫規則如何適用於泛型類別:

abstract class C<T>
{
    public virtual T F() {...}
    public virtual C<T> G() {...}
    public virtual void H(C<T> x) {...}
}

class D : C<string>
{
    public override string F() {...}            // Ok
    public override C<string> G() {...}         // Ok
    public override void H(C<T> x) {...}        // Error, should be C<string>
}

class E<T,U> : C<U>
{
    public override U F() {...}                 // Ok
    public override C<U> G() {...}              // Ok
    public override void H(C<T> x) {...}        // Error, should be C<U>
}

結束範例

範例:下列示範當涉及類型參數時,覆寫規則的運作方式:

#nullable enable
class A
{
    public virtual void Foo<T>(T? value) where T : class { }
    public virtual void Foo<T>(T? value) where T : struct { }
}
class B: A
{
    public override void Foo<T>(T? value) where T : class { }
    public override void Foo<T>(T? value) where T : struct { }
}

如果沒有類型參數條件約束 where T : class,就無法覆寫具有參考型別型別參數的基底方法。 結束範例

覆寫宣告可以使用base_access 存取覆寫的基底方法(§12.8.15)。

範例:在下列程式代碼中

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

base.PrintFields() 中的 B 調用會調用在 A 中宣告的 PrintFields 方法。 base_access會停用虛擬調用機制,並只將基底方法視為非virtual方法。 如果 B 中的調用已編寫為 ((A)this).PrintFields(),則它會遞歸地調用在 PrintFields 中宣告的 B 方法,而不是在 A 中宣告的方法,因為 PrintFields 是虛擬的,並且運行時類型 ((A)this)B

結束範例

只有包含override修飾符,方法才能覆蓋另一個方法。 在所有其他情況下,具有與繼承方法相同簽名的方法只會隱藏繼承的方法。

範例:在下列程式代碼中

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

F 中的 B 方法不包含 override 修飾詞,因此無法覆寫 F 中的 A 方法。 而是 F 中的 B 方法會隱藏 A 中的方法,且會回報警告,因為在宣告中未包含新的修飾詞。

結束範例

範例:在下列程式代碼中

class A
{
    public virtual void F() {}
}

class B : A
{
    private new void F() {} // Hides A.F within body of B
}

class C : B
{
    public override void F() {} // Ok, overrides A.F
}

F中的 B 方法會隱藏繼承自 F的虛擬A方法。 由於新的FB中具有私用存取權,因此其範圍只包含B類別主體,而且不會延伸至C。 因此,允許在F中宣告C來覆寫從F繼承的A

結束範例

15.6.6 封閉方法

當實例方法宣告包含 sealed 修飾詞時,該方法會稱為 密封方法。 封閉方法會覆寫具有相同簽章的繼承虛擬方法。 密封方法也應標示為 override 修飾詞。 使用sealed修飾詞可防止衍生類別進一步覆寫該方法。

範例:範例

class A
{
    public virtual void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public sealed override void F() => Console.WriteLine("B.F");
    public override void G()        => Console.WriteLine("B.G");
}

class C : B
{
    public override void G() => Console.WriteLine("C.G");
}

類別 B 提供兩個覆寫方法:一個方法具有 F 修飾詞,而另一個方法則沒有。 B 使用 sealed 修飾符防止 C 進一步覆蓋 F

結束範例

15.6.7 抽象方法

當實例方法宣告包含 abstract 修飾詞時,該方法會稱為 抽象方法。 雖然抽象方法也是隱含的虛擬方法,但它不能有 修飾詞 virtual

抽象方法宣告引進了新的虛擬方法,但不提供該方法的實作。 非抽象衍生類別需要藉由覆寫該方法來提供自己的實作。 抽象方法的 method_body 僅由分號組成。

抽象方法宣告僅允許在抽象類別 (§15.2.2.2) 和介面 (§19.4.3) 中使用。

範例:在下列程式代碼中

public abstract class Shape
{
    public abstract void Paint(Graphics g, Rectangle r);
}

public class Ellipse : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r);
}

public class Box : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r);
}

類別 Shape 會定義可以自行繪製之幾何圖形物件的抽象概念。 該 Paint 方法是抽象的,因為形狀的抽象概念沒有有意義的後援實作。 EllipseBox 類別是具體的Shape實作。 因為這些類別不是抽象的,所以必須重寫 Paint 方法並提供實際的實作。

結束範例

base_access (§12.8.15) 參考抽象方法時,將會出現編譯時間錯誤。

範例:在下列程式代碼中

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

會報告 base.F() 的呼叫為編譯錯誤,因為它參考了抽象方法。

結束範例

抽象方法的宣告被允許用來覆寫虛擬方法。 這可讓抽象類強制在衍生類別中重新實作 方法,並使方法的原始實作無法使用。

範例:在下列程式代碼中

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

abstract class B: A
{
    public abstract override void F();
}

class C : B
{
    public override void F() => Console.WriteLine("C.F");
}

類別會宣告虛擬方法,類別AB會使用抽象方法覆寫此方法,而 類別C會覆寫抽象方法以提供自己的實作。

結束範例

15.6.8 外部方法

當方法宣告包含 extern 修飾詞時,方法會稱為 外部方法。 外部方法會以外部方式實作,通常是使用 C# 以外的語言。 因為外部方法宣告沒有提供實際實作,因此外部方法的方法主體只會包含分號。 外部方法不得為泛型。

實現與外部方法的連結的機制是實作定義。

範例:下列範例示範如何使用 extern 修飾詞和 DllImport 屬性:

class Path
{
    [DllImport("kernel32", SetLastError=true)]
    static extern bool CreateDirectory(string name, SecurityAttribute sa);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool RemoveDirectory(string name);

    [DllImport("kernel32", SetLastError=true)]
    static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool SetCurrentDirectory(string name);
}

結束範例

15.6.9 部分方法

當方法宣告包含 partial 修飾詞時,該方法會說為 部分方法。 部分方法只能宣告為部分類型的成員(~15.2.7),而且受限於一些限制。

部分方法可以在類型宣告的某個部分中定義,並在另一個部分實作。 實作是選擇性的;如果沒有元件實作 partial 方法,則部分方法宣告和所有呼叫都會從元件組合所產生的類型宣告中移除。

部分方法不得定義存取修飾詞;它們是隱含私用的。 其傳回型別應為 void,且其參數不得為輸出參數。 只有當標識符 partial 在方法宣告中立即出現在 關鍵詞之前時,它才會被識別為內容關鍵詞(void)。 局部方法無法明確地實作介面方法。

部分方法宣告有兩種:如果方法宣告的主體是分號,則宣告會稱為 定義部分方法宣告。 如果區塊內容不是僅有一個分號,那麼該宣告被稱為實作部分方法宣告。 在類型宣告的各個部分,只能有一個定義具有指定簽章的部分方法宣告,而且最多只能有一個使用指定簽章實作部分方法宣告。 如果已指定實作部分方法宣告,則對應的定義部分方法宣告應存在,而且宣告應符合下列指定:

  • 宣告應具有相同修飾詞(雖然不一定以相同順序)、方法名稱、類型參數數目和參數數目。
  • 宣告中的對應參數應具有相同修飾詞(雖然不一定以相同順序)和相同的類型,或識別可轉換型別(類型參數名稱中的模數差異)。
  • 宣告中的對應型別參數應具有相同的條件約束(類型參數名稱中的模數差異)。

實作部分方法宣告可以出現在與對應定義部分方法宣告相同的部分。

只有定義的部分方法參與重載決策。 因此,不論是否指定實作宣告,調用表達式都可以解析為部分方法的調用。 由於部分方法一律會傳 void回 ,因此這類調用表達式一律會是 expression 語句。 此外,由於部分方法是隱含的 private,因此這類語句一律會在宣告部分方法之類型宣告的其中一個部分內發生。

注意:匹配部分方法宣告的定義和實現不要求參數名稱匹配。 當使用具名參數時,這可能會產生令人驚訝,但定義良好的行為(§12.6.2.1)。 例如,假設在一個檔案中定義部分方法宣告 M ,並在另一個檔案中實作部分方法宣告:

// File P1.cs:
partial class P
{
    static partial void M(int x);
}

// File P2.cs:
partial class P
{
    static void Caller() => M(y: 0);
    static partial void M(int y) {}
}

無效,因為調用使用實作中的自變數名稱,而不是定義部分方法宣告。

結尾註釋

如果部分類型宣告中沒有任何部分包含指定部分方法的實作宣告,則叫用它的任何表達式語句只會從合併的類型宣告中移除。 因此,調用表達式,包括任何子表達式,在運行時間沒有作用。 部分方法本身也會被移除,而且不會是合併類型宣告的成員。

如果指定部分方法的實作宣告存在,則會保留部分方法的調用。 部分方法會引發一個與實作部分方法宣告類似的方法宣告,但有以下例外:

  • 未包含partial修飾詞。

  • 產生的方法宣告中的屬性是定義和以未指定順序實作部分方法宣告的結合屬性。 重複項目不會被移除。

  • 所產生方法宣告的參數之屬性,是定義和實作這些部分方法宣告之對應參數的合併屬性,順序未指定。 重複項目不會被移除。

如果針對部分方法 M提供定義宣告,但未提供實作宣告,則適用下列限制:

  • M 建立委派時會導致編譯時錯誤(12.8.17.5)。

  • 在匿名函式內參考M,此匿名函式已轉換成表達式樹狀結構類型(§8.6),這是編譯時期錯誤。

  • 在調用 M 期間發生的表達式不會影響明確的指派狀態 (~9.4),這可能會導致編譯時間錯誤。

  • M 不能是應用程式的進入點(~7.1)。

部分方法很適合讓類型宣告的某個部分自定義另一個元件的行為,例如工具所產生的部分。 請考慮以下的部分類別宣告:

partial class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    partial void OnNameChanging(string newName);
    partial void OnNameChanged();
}

如果這個類別在沒有任何其他元件的情況下進行編譯,則會移除定義部分方法宣告及其調用,而產生的合併類別宣告將等於下列專案:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

假設給了另一個部分,然而它提供了部分方法的實施宣告。

partial class Customer
{
    partial void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

然後所產生的合併類別宣告會等於下列:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

15.6.10 擴充方法

當方法的第一個參數包含 this 修飾詞時,該方法會稱為 擴充方法。 擴充方法只能在非泛型、非巢狀靜態類別中宣告。 擴充方法的第一個參數受到限制,如下所示:

  • 只有當它具有值類型時,它才能是輸入參數。
  • 只有在具有值類型或具有限制為結構的泛型類型時,它才能是參考參數。
  • 它不應是輸出參數。
  • 它不應該是指針類型。

範例:以下是宣告兩個擴充方法的靜態類別範例:

public static class Extensions
{
    public static int ToInt32(this string s) => Int32.Parse(s);

    public static T[] Slice<T>(this T[] source, int index, int count)
    {
        if (index < 0 || count < 0 || source.Length - index < count)
        {
            throw new ArgumentException();
        }
        T[] result = new T[count];
        Array.Copy(source, index, result, 0, count);
        return result;
    }
}

結束範例

擴充方法是一般靜態方法。 此外,如果其所屬的靜態類別在作用域內,可以使用實例方法呼叫語法(§12.8.10.3),使用接收者表達式作為第一個引數來叫用擴充方法。

範例:下列程式使用上述宣告的擴充方法:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in strings.Slice(1, 2))
        {
            Console.WriteLine(s.ToInt32());
        }
    }
}

Slice 方法可在 string[] 上取得,ToInt32 方法可在 string 上使用,因為它們被宣告為擴充方法。 程序的意義與下列相同,使用一般靜態方法呼叫:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in Extensions.Slice(strings, 1, 2))
        {
            Console.WriteLine(Extensions.ToInt32(s));
        }
    }
}

結束範例

15.6.11 方法主體

方法宣告的方法主體包含區塊主體、表達式主體或分號。

抽象和外部方法宣告不提供方法實作,因此其方法主體僅由分號構成。 對於任何其他方法,方法主體是區塊 (~13.3),其中包含叫用該方法時要執行的語句。

方法的有效傳回類型void,如果傳回型別為 void,或者方法為異步且傳回型別為 «TaskType»(§15.14.1)。 否則,非異步方法的有效傳回型別是其本身的傳回型別,而具有«TaskType»<T>(§15.14.1)傳回型別的異步方法,其有效傳回型別為T

當方法的有效傳回型別是 void ,而且方法具有區塊主體時, return 區塊中的語句 (~13.10.5) 不得指定表達式。 如果 void 方法的一個區塊正常完成執行(也就是控制流程結束於方法主體的盡頭),那麼該方法就會簡單地返回給呼叫者。

當方法的有效傳回型別是 void 且方法具有表達式主體時,表達式 E 應該是 statement_expression,而該主體完全等同於格式的 { E; }區塊主體。

在依值傳回的方法(§15.6.1),每個位於方法主體中的 return 陳述都應該包含可隱含轉換成有效傳回型別的表達式。

針對傳回的 by-ref 方法(§15.6.1),該方法主體中的每個 return 語句都應該指定類型為有效傳回型別的表達式,而且具有ref 安全內容,其來自於呼叫端內容§9.7.2)。

針對值傳遞(returns-by-value)和引用傳遞(returns-by-ref)的方法,方法主體的結束點應不可到達。 換句話說,流程控制不允許流出方法體的結尾。

範例:在下列程式代碼中

class A
{
    public int F() {} // Error, return value required

    public int G()
    {
        return 1;
    }

    public int H(bool b)
    {
        if (b)
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    public int I(bool b) => b ? 1 : 0;
}

返回值的方法會導致編譯錯誤,因為程式控制可能會流出方法主體的末端。 GH 方法是正確的,因為所有可能的執行路徑都會在指定傳回值的 return 語句中結束。 I 這個方法是正確的,因為它的主體相當於一個只有一個 return 語句的區塊。

結束範例

15.7 屬性

15.7.1 一般

屬性是一個成員,提供對象或類別特性的存取權。 屬性的範例包括字串的長度、字型的大小、視窗的標題,以及客戶的名稱。 屬性是欄位的自然延伸,兩者都是具名成員,且存取欄位和屬性的語法相同。 不過,與欄位不同的是,屬性並不會指示儲存位置。 相反地,屬性具有 存取子,可指定讀取或寫入其值時要執行的陳述式。 因此,屬性提供一種機制,讓動作與對象或類別特性的讀取和寫入產生關聯;此外,它們允許計算這類特性。

屬性是使用 property_declaration來宣告:

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

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

property_declaration可以包括一組屬性§23) 和任何一種允許的聲明無障礙類型 (§15.3.6)、new§15.3.5)、 static§15.7.2)、 virtual§15.6.4§15.7.6)、 override§15.6.5§15.7.6)、 sealed§15.6.6)、(§15.6.7abstract§15.7.6) 和 extern)。 此外,直接包含於struct_declarationproperty_declaration可能包括readonly修飾詞(§16.4.11)。

  • 第一個聲明了一個非 ref 類型值的屬性。 其值的類型是 type。 這類屬性可以是可讀取和/或可寫入的。
  • 第二個宣告具 ref 類型值的屬性。 其值是variable_reference§9.5),可能是 readonly 一個類型為type的變數。 此種類型的屬性僅供讀取。

property_declaration可以包括一組屬性§23) 和任何一種允許的宣告協助工具類型 (§15.3.6)、new§15.3.5)、 static§15.7.2)、 virtual§15.6.4§15.7.6)、 override§15.6.5§15.7.6)、 sealed§15.6.6abstract)、(§15.6.7§15.7.6) 和 extern§15.6.8) 修飾符。

屬性宣告與方法宣告(§15.6)在有效修飾詞組合的規則上相同。

member_name§15.6.1)指定屬性的名稱。 除非 屬性是明確的介面成員實作, 否則member_name 只是 標識符。 針對明確介面成員實作 (§19.6.2) , member_name 是由 interface_type 組成,後面接著 “.” 和 識別碼

屬性 的類型 至少可以和屬性本身一樣可存取(~7.5.5)。

property_body可能由語句主體部分或表達式主體部分組成。 在語句主體中,accessor_declarations應以“{”和“}”標記括住,以宣告屬性的存取子(§15.7.3)。 存取子會指定與讀取和寫入 屬性相關聯的可執行語句。

property_body 中,由=>後接表達式和分號組成的表達式主體與語句主體E完全等同,因此只能用於指定只讀屬性,其中 get 存取子的結果由單一表達式確定。

property_initializer 只能用於自動實作的屬性(§15.7.4),並將這類屬性的基礎欄位初始化為由 expression 所指定的值。

ref_property_body 可能包含語句體或表達式體。 在陳述主體中,get_accessor_declaration 宣告屬性的 get 存取子(§15.7.3)。 訪問子指定了與讀取屬性相關的可執行語句。

ref_property_body中,表達式主體由=>後接ref變數引用V和分號組成,這等同於語句主體{ get { return ref V; } }

注意:即使存取屬性的語法與字段的語法相同,屬性也不會分類為變數。 因此,除非屬性是 ref 的值,否則無法將屬性作為 inoutref 引數傳遞,因為它必須返回變數引用(§9.7)。 結尾註釋

當屬性宣告包含 extern 修飾詞時,屬性會稱為 外部屬性。 因為外部屬性宣告沒有提供實際實作,其accessor_declarations中的每個accessor_body都應該是分號。

15.7.2 靜態和實例屬性

當屬性宣告包含 static 修飾詞時,屬性會稱為 靜態屬性。 當沒有任何 static 修飾詞存在時,屬性會說為 實例屬性

靜態屬性是指未與特定實例相關聯的屬性。如果在靜態屬性的存取子中參考 this,會產生編譯時期錯誤。

實例屬性與類別的指定實例相關聯,而且該實例可以在該屬性的存取子中存取為 this~12.8.14)。

靜態和實例成員之間的差異會在 \15.3.8進一步討論。

15.7.3 存取子

附註: 此子條款適用於屬性 (§15.7) 和索引器 (§15.9)。 子句是根據屬性編寫的,當讀取索引器時,請將索引器/索引器替換為屬性/屬性,並查閱 §15.9.2 中給出的屬性和索引器之間的差異清單。 結尾註釋

屬性 accessor_declarations 指定與寫入和/或讀取該屬性相關聯的可執行語句。

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

accessor_modifier
    : 'protected'
    | 'internal'
    | 'private'
    | 'protected' 'internal'
    | 'internal' 'protected'
    | 'protected' 'private'
    | 'private' 'protected'
    | 'readonly'        // direct struct members only
    ;

accessor_body
    : block
    | '=>' expression ';'
    | ';' 
    ;

ref_get_accessor_declaration
    : attributes? accessor_modifier? 'get' ref_accessor_body
    ;
    
ref_accessor_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

存取子宣告取得存取子宣告設定存取子宣告,或兩者組成。 每個存取子宣告都包含選擇性屬性、選擇性 accessor_modifier、標記 getset,後面接著 accessor_body

對於以 ref 為值的屬性,ref_get_accessor_declaration 包含可選的屬性、可選的 accessor_modifier、標記 get,後面接著 ref_accessor_body

accessor_modifier的使用受下列限制所控管:

  • accessor_modifier只允許在由readonly直接包含的property_declarationindexer_declaration中(§16.4.11, §16.4.13)。
  • 對於沒有override修飾詞的屬性或索引器,只有在屬性或索引器同時具有 get 和 set 存取子時,才允許accessor_modifier,且只能應用在其中一個存取子上。
  • 對於包含 override 修飾詞的屬性或索引器,存取子應符合被覆寫存取子的 accessor_modifier(如果有的話)。
  • accessor_modifier 應宣告的存取權限必須比屬性或索引器本身所宣告的存取權限更嚴格。 精確:
    • 如果屬性或索引器的宣告存取層級是public,那麼由accessor_modifier所宣告的存取層級可以是private protectedprotected internalinternalprotectedprivate
    • 如果屬性或索引器的宣告存取層級是protected internal,那麼由accessor_modifier所宣告的存取層級可以是private protectedprotected privateinternalprotectedprivate
    • 如果屬性或索引器具有internalprotected的宣告存取範圍,則accessor_modifier宣告的存取性應該是private protectedprivate
    • 如果屬性或索引器的宣告可見性為 private protected,則由 accessor_modifier 宣告的可見性應該是 private
    • 如果屬性或索引器的宣告存取權限是 private,則不能使用 任何的accessor_modifier

針對 abstractextern 非 ref 值屬性,任何指定的存取子的 accessor_body 僅是分號。 非抽象且非 extern 的屬性,但不是索引器,可能會把所有指定存取子的 accessor_body 設為分號,此時,它是自動實作的屬性§15.7.4)。 自動實作的屬性至少應該有 get 存取子。 對於任何其他非抽象、非外部屬性的存取子,accessor_body為:

  • 區塊,用於指定當呼叫相應的存取器時要執行的語句;或
  • 表達式本體,由 => 組成,後面接著 表達式 和分號,表示在叫用對應存取子時要執行的單一表達式。

針對 abstractextern 參考型別屬性,ref_accessor_body 僅為分號。 對於任何其他非抽象、非 extern 屬性的存取子,ref_accessor_body 可能是:

  • 一個區塊,指定在調用 get 存取子時要執行的語句;或者
  • 表達式主體的組成部分,包括 =>,接著是 refvariable_reference 和分號。 當叫用 get 存取子時,將會評估變數參照。

非 ref 型別值屬性的 get 存取子對應於一個具有該屬性型別傳回值的無參數方法。 除了指派的目標以外,當表達式中參考這類屬性時,會叫用 get 存取子來計算屬性的值(~12.2.2)。

非 ref 值屬性的 get 存取子的主體應符合 §15.6.11 中所述的值返回方法規則。 特別是, return get 存取子主體中的所有語句都應該指定可隱含轉換成屬性類型的表達式。 此外,get 存取子的終點不應可達。

ref 值屬性的 get 存取子對應於一個無參數的方法,其傳回值是variable_reference,指向屬性類型的變數。 在表達式中參考這類屬性時,會叫用 get 存取子來計算 屬性的variable_reference 值。 該 變數參考,如同其他任何參考一樣,接著會用來讀取變數。對於非只讀的 變數參考,則可以根據上下文需求來寫入變數。

範例:以下範例說明了一個以 ref 為值的屬性作為指派目標的情況:

class Program
{
    static int field;
    static ref int Property => ref field;

    static void Main()
    {
        field = 10;
        Console.WriteLine(Property); // Prints 10
        Property = 20;               // This invokes the get accessor, then assigns
                                     // via the resulting variable reference
        Console.WriteLine(field);    // Prints 20
    }
}

結束範例

ref-valued 屬性的取值存取子之主體應符合 §15.6.11 中所述的 ref 值方法的規則。

set 存取子對應至一種方法,該方法具有一個屬性類型的單一值參數和 void 傳回型別。 set 存取子的隱含參數一律命名為 value。 當屬性參考為指派的目標 (§12.23) 或 or ++–-運算元 (§12.8.16§12.9.7) 時,會使用提供新值的引數來叫用集合存取子 (§12.23.2) 。 設定子的主體應符合在第15.6.11節中描述的方法規則。 特別是,在 set 存取子內的 return 語句不允許指定表達式。 由於 set 存取子隱含有一個名為 value 的參數,因此如果在 set 存取子中宣告具有該名稱的局部變數或常數,將會造成編譯時期錯誤。

根據是否具有 get 和 set 存取子,屬性分類如下:

  • 包含 get 存取子和 set 存取子的屬性稱為讀寫屬性
  • 只有 get 存取子的屬性稱為唯讀屬性。 唯讀屬性作為賦值目標在編譯時會產生錯誤。
  • 屬性如果只有 set 存取子,則稱為 唯寫屬性。 除非作為指派的目標,否則在表達式中參考唯寫屬性會導致編譯時錯誤。

注意:前置和後置 ++ 運算符和 -- 複合指派運算符無法套用至唯寫屬性,因為這些運算符在寫入新運算符之前先讀取其操作數的舊值。 結尾註釋

範例:在下列程式代碼中

public class Button : Control
{
    private string caption;

    public string Caption
    {
        get => caption;
        set
        {
            if (caption != value)
            {
                caption = value;
                Repaint();
            }
        }
    }

    public override void Paint(Graphics g, Rectangle r)
    {
        // Painting code goes here
    }
}

控件 Button 會宣告公用 Caption 屬性。 Caption 屬性的 get 存取子會傳回儲存在私有 string 欄位中的 caption。 set 存取子會檢查新值是否與目前的值不同,如果是,則會儲存新的值並重新繪出控件。 屬性通常會遵循上述模式:get 存取子只會傳回儲存在 private 欄位中的值,而 set 存取子會修改該 private 欄位,然後執行更新物件狀態所需的任何其他動作。 給定上述類別,以下是Button屬性的使用範例:

Button okButton = new Button();
okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

在這裡,會藉由將值指派給 屬性來叫用 set 存取子,而 get 存取子則是藉由在表達式中參考 屬性來叫用。

結束範例

屬性的 get 和 set 存取子不是不同的成員,而且無法個別宣告屬性的存取子。

範例:範例

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

不宣告任何讀寫屬性。 相反地,它會宣告兩個具有相同名稱的屬性,一個唯讀屬性和一個唯寫屬性。 由於在相同類別中宣告的兩個成員不能有相同的名稱,因此此範例會導致發生編譯時間錯誤。

結束範例

當衍生類別以與繼承屬性相同的名稱宣告屬性時,衍生屬性會在讀取和寫入操作中隱藏繼承的屬性。

範例:在下列程式代碼中

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

P中的B屬性會對於讀取和寫入,隱藏P中的A屬性。 因此,在這些陳述中

B b = new B();
b.P = 1;       // Error, B.P is read-only
((A)b).P = 1;  // Ok, reference to A.P

指定給b.P的賦值會導致報告編譯時錯誤,因為P中的B只讀屬性隱藏了P中的A只寫屬性。 請注意,不過,可以使用型別轉換來存取隱藏的 P 屬性。

結束範例

不同於公用欄位,屬性會提供物件內部狀態與其公用介面之間的分隔。

範例:請考慮下列程序代碼,其使用 Point 結構來表示位置:

class Label
{
    private int x, y;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.x = x;
        this.y = y;
        this.caption = caption;
    }

    public int X => x;
    public int Y => y;
    public Point Location => new Point(x, y);
    public string Caption => caption;
}

在這裡,類別 Label 會使用兩個 int 欄位 xy來儲存其位置。 位置會公開為 XY 屬性,並且作為一個 Location 類型的 Point 屬性。 如果在未來的版本中,Label 內部將位置儲存為 Point 會變得更加方便,則可以進行變更,而不會影響類別的公用介面。

class Label
{
    private Point location;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.location = new Point(x, y);
        this.caption = caption;
    }

    public int X => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

xypublic readonly 欄位,則不可能對 Label 類別進行這樣的變更。

結束範例

注意:透過屬性公開狀態不一定比直接公開欄位更有效率。 特別是當屬性為非虛擬且只包含少量程式代碼時,執行環境可能會以存取子的實際程式代碼取代對存取子的呼叫。 此程式稱為 內嵌,可讓屬性存取與欄位存取一樣有效率,但會保留屬性增加的彈性。 結尾註釋

範例:由於叫用 get 存取子在概念上相當於讀取欄位的值,所以 get 存取子的程式設計樣式會被視為有可觀察副作用的不良程式設計樣式。 在範例中

class Counter
{
    private int next;

    public int Next => next++;
}

屬性的值 Next 取決於先前存取屬性的次數。 因此,存取 屬性會產生可觀察的副作用,而 屬性應該改為實作為方法。

get 存取子的「無副作用」慣例並不表示只應該撰寫 get 存取子來傳回儲存在字段中的值。 事實上,get 存取子通常會藉由存取多個字段或叫用方法來計算屬性的值。 不過,正確設計的 get 存取子不會執行任何導致物件狀態可觀察變更的動作。

結束範例

屬性可用來延遲資源的初始化,直到第一次參考它為止。

範例:

public class Console
{
    private static TextReader reader;
    private static TextWriter writer;
    private static TextWriter error;

    public static TextReader In
    {
        get
        {
            if (reader == null)
            {
                reader = new StreamReader(Console.OpenStandardInput());
            }
            return reader;
        }
    }

    public static TextWriter Out
    {
        get
        {
            if (writer == null)
            {
                writer = new StreamWriter(Console.OpenStandardOutput());
            }
            return writer;
        }
    }

    public static TextWriter Error
    {
        get
        {
            if (error == null)
            {
                error = new StreamWriter(Console.OpenStandardError());
            }
            return error;
        }
    }
...
}

類別Console包含三個屬性、 InOutError,分別代表標準輸入、輸出和錯誤裝置。 藉由將這些成員公開為屬性,類別 Console 可能會延遲其初始化,直到實際使用為止。 例如,第一次參考 Out 屬性時,如 中所示

Console.Out.WriteLine("hello, world");

建立輸出裝置的底層 TextWriter。 不過,如果應用程式沒有參考 InError 屬性,則不會為這些裝置建立任何物件。

結束範例

15.7.4 自動實作屬性

自動實作的屬性(或自動屬性)是非抽象、非外部、非 ref 值類型的屬性,且只有分號的accessor_body。 自動屬性應該有 get 存取子,而且可以選擇性地擁有 set 存取子。

當屬性被指定為自動實作屬性時,將自動提供一個隱藏的支援欄位給該屬性,而存取子會被實作成從該支援欄位讀取和寫入資料。 隱藏的備份欄位是無法存取的,它只能透過自動實作的屬性存取子讀取和寫入,即使在包含的類型內也是如此。 如果 auto-property 沒有 set 存取子,則備用欄位會被視為readonly§15.5.3)。 就像readonly欄位一樣,唯讀自動屬性也可以在封閉類別的建構函式主體中被指派。 這類指派會直接賦值於屬性的唯讀後備欄位。

自動屬性可以選擇性地有一個 property_initializer,這會直接歸屬於備用欄位,作為 variable_initializer§17.7)。

範例:

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

相當於下列宣告:

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

結束範例

範例:在下列情况中

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

相當於下列宣告:

public class ReadOnlyPoint
{
    private readonly int __x;
    private readonly int __y;
    public int X { get { return __x; } }
    public int Y { get { return __y; } }

    public ReadOnlyPoint(int x, int y)
    {
        __x = x;
        __y = y;
    }
}

對唯讀字段的分配是有效的,因為它們發生在構造函數之中。

結束範例

雖然支援欄位是隱藏的,但該欄位可以透過自動實作屬性的property_declaration直接應用欄位目標屬性(§15.7.1)。

範例:下列程序代碼

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

會使欄位目標屬性NonSerialized 套用至編譯器產生的備援欄位,彷彿程式碼已被這樣撰寫:

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

結束範例

15.7.5 輔助功能

如果存取子具有accessor_modifier,存取子的存取範圍定義域 (~7.5.3) 會使用accessor_modifier宣告存取範圍來判斷。 如果存取子沒有 accessor_modifier,存取子的存取範圍定義域會從屬性或索引器的宣告存取範圍決定。

accessor_modifier的存在永遠不會影響成員查找(§12.5)或重載解析(§12.6.4)。 不論存取的內容為何,屬性或索引器上的修飾詞一律會決定系結至哪一個屬性或索引器。

選取特定非 ref 值屬性或非 ref 值索引器之後,所涉及的特定存取子的存取範圍會用來判斷該使用是否有效。

  • 如果使用方式是值(§12.2.2),則 get 存取器應存在且可供存取。
  • 如果使用方式是簡單指派的目標 (§12.23.2) ,則集合存取子應該存在且可存取。
  • 如果使用方式是作為複合指派的目標 (§12.23.4) ,或作為 or ++ 運算子的目標 --§12.8.16§12.9.7) ,則 get 存取子和 set 存取子都應該存在且可存取。

範例:在下列範例中,屬性 A.Text 會隱藏 B.Text屬性,即使在只呼叫 set 存取子的內容中也一樣。 相反地,類別 B.Count 無法存取屬性 M,因此會改用可存取的屬性 A.Count

class A
{
    public string Text
    {
        get => "hello";
        set { }
    }

    public int Count
    {
        get => 5;
        set { }
    }
}

class B : A
{
    private string text = "goodbye";
    private int count = 0;

    public new string Text
    {
        get => text;
        protected set => text = value;
    }

    protected new int Count
    {
        get => count;
        set => count = value;
    }
}

class M
{
    static void Main()
    {
        B b = new B();
        b.Count = 12;       // Calls A.Count set accessor
        int i = b.Count;    // Calls A.Count get accessor
        b.Text = "howdy";   // Error, B.Text set accessor not accessible
        string s = b.Text;  // Calls B.Text get accessor
    }
}

結束範例

選取特定的 ref 值屬性或 ref 值索引器之後,不論使用方式是值、簡單指派的目標還是複合指派的目標,都使用所牽涉到之 get 存取子的存取範圍定義域來判斷該使用方式是否有效。

用來實作介面的存取子不得有 accessor_modifier。 如果只使用一個存取子來實作介面,則另一個 存取子可能會使用 accessor_modifier宣告:

範例:

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // Must not have a modifier here
        internal set {...}  // Ok, because I.Prop has no set accessor
    }
}

結束範例

15.7.6 虛擬 (Virtual)、封閉 (sealed)、覆寫 (override) 和抽象 (abstract) 存取子

附註: 此子條款適用於屬性 (§15.7) 和索引器 (§15.9)。 子句是根據屬性編寫的,當讀取索引器時,請將索引器/索引器替換為屬性/屬性,並查閱 §15.9.2 中給出的屬性和索引器之間的差異清單。 結尾註釋

虛擬屬性宣告會指定屬性的存取子是虛擬的。 修飾 virtual 會套用至屬性的所有非私有存取子。 當虛擬屬性的存取子具有privateaccessor_modifier時,私用存取子會隱含地不是虛擬。

抽象屬性宣告會指定屬性的存取子是虛擬的,但不會提供存取子的實際實作。 非抽象的衍生類別需要透過覆寫屬性來為存取子提供自己的實作。 因為抽象屬性宣告的存取子沒有提供實際實作,因此其 accessor_body 只包含分號。 抽象屬性不得有private存取器。

包含 abstractoverride 修飾詞的屬性宣告會指定屬性為抽象屬性,並覆寫基底屬性。 這類屬性的存取子也是抽象的。

抽象屬性宣告僅允許在抽象類別 (§15.2.2.2) 和介面 (§19.4.4) 中使用。 繼承的虛擬屬性的存取子可以在衍生類別中,透過包含指定override指示詞的屬性宣告來覆寫。 這稱為 覆寫屬性宣告。 覆寫屬性宣告並不會宣告新屬性。 相反地,它只會專門化現有虛擬屬性的存取器的實作。

覆寫宣告和被覆寫的基底屬性必須具有相同的宣告可見性。 換句話說,覆寫宣告不得變更基底屬性的存取範圍。 然而,如果被覆寫的基礎屬性是 protected internal 且在與含有覆寫宣告的組不同的組中宣告,那麼覆寫宣告的可見性應該是 protected。 如果繼承的屬性只有單一存取子(亦即,如果繼承的屬性是唯讀或唯寫的),則覆寫屬性應該只包含該存取子。 如果被繼承的屬性包含兩個存取子(例如,該屬性是可讀寫的),則覆蓋屬性可以包含一個存取子或兩個存取子。 應在覆蓋屬性類型和繼承屬性類型之間進行身份轉換。

覆寫屬性宣告可以包含sealed 修飾詞。 使用此修飾詞可防止衍生類別進一步覆寫這個屬性。 封閉屬性的存取子也被封閉。

除了宣告和調用語法的差異之外,虛擬、封閉、覆寫和抽象的存取子的行為與虛擬、封閉、覆寫和抽象的方法完全相同。 具體來說,§15.6.4§15.6.5§15.6.6§15.6.7 中所述的規則適用於存取器,就如同它們是對應形式的方法一樣。

  • get 存取子對應於一個無參數的方法,該方法的傳回值是屬性型別,且具有與包含該屬性相同的修飾詞。
  • set 存取子對應於一個方法,該方法具有一個屬性類型的單一值參數,void 回傳型別,以及與包含其之屬性相同的修飾詞。

範例:在下列程式代碼中

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X 是虛擬唯讀屬性, Y 是虛擬讀寫屬性,而且 Z 是抽象的讀寫屬性。 因為 Z 是抽象的,因此,包含類別 A 也應該宣告為抽象。

衍生自 A 的類別如下所示:

class B : A
{
    int z;

    public override int X
    {
        get => base.X + 1;
    }

    public override int Y
    {
        set => base.Y = value < 0 ? 0: value;
    }

    public override int Z
    {
        get => z;
        set => z = value;
    }
}

在這裡,XYZ 的宣告是覆寫屬性宣告。 每個屬性宣告的存取修飾詞、類型和名稱都完全匹配對應繼承屬性的內容。 X 的 get 存取子和 Y 的 set 存取子使用 base 關鍵詞來存取繼承的存取子。 宣告Z會覆蓋這兩個抽象存取子,因此,在abstract中沒有任何突出的B函式成員,並且允許B成為非抽象類別。

結束範例

當屬性被宣告為覆蓋時,任何被覆蓋的存取子應該可以被覆蓋的代碼存取。 此外,屬性或索引器本身及其存取子方法的宣告可存取性,應符合被覆寫的成員及其存取子方法的可存取性。

範例:

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D: B
{
    public override int P
    {
        get {...}            // Must not have a modifier here
        protected set {...}  // Must specify protected here
    }
}

結束範例

15.8 事件

15.8.1 一般

事件是讓類別或物件提供通知的成員。 用戶端可以透過提供 事件處理常式來附加事件的可執行程式碼。

事件是使用 event_declaration來宣告:

event_declaration
    : attributes? event_modifier* 'event' type variable_declarators ';'
    | attributes? event_modifier* 'event' type member_name
        '{' event_accessor_declarations '}'
    ;

event_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

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

event_declaration可以包括一組屬性§23) 和任何一種允許的宣告協助工具類型 (§15.3.6)、new§15.3.5)、 static§15.6.3§15.8.4)、 virtual§15.6.4§15.8.5)、 override§15.6.5§15.8.5)、 sealed§15.6.6abstract)、(§15.6.7§15.8.5) 和 extern§15.6.8) 修飾符。 此外,由 struct_declaration 直接包含的 event_declaration 可以包含 readonly 修飾詞(16.4.12)。

事件宣告與方法宣告(§15.6)的有效修飾詞組合遵循相同的規則。

事件宣告的類型應該是delegate_type§8.2.8),而且delegate_type的存取性至少應該和事件本身一樣高(§7.5.5)。

事件宣告可以包含事件存取子宣告。 不過,如果是非 extern、非抽象事件,編譯器會自動提供它們(§15.8.2);而針對 extern 事件,存取子由外部提供。

省略event_accessor_declaration的事件宣告定義了一個或多個事件,即每個variable_declarator對應一個事件。 所有的屬性和修飾詞皆適用於由某個 event_declaration 所宣告的所有成員。

這是一個編譯時期錯誤,因為event_declaration同時包含abstractevent_accessor_declaration

當事件宣告包含 extern 修飾詞時,事件會稱為 外部事件。 因為外部事件宣告沒有提供實際的實作,如果包含extern修飾詞和event_accessor_declaration,這是一個錯誤。

具有 abstract 修飾詞的事件宣告中的 external 包含 variable_initializer 時,這是編譯時期錯誤。

事件可以作為+=-=運算子的左操作數。 這些運算符分別用來附加事件處理程式,或從事件中移除事件處理程式,以及事件存取修飾詞控制允許這類作業的內容。

在一個事件的聲明之外的類型中,只有+=-=操作是由代碼允許的。 因此,雖然這類程式代碼可以新增和移除事件的處理程式,但它無法直接取得或修改事件處理程式的基礎清單。

在形式為 x += yx –= y的作業中,當 是事件時x,作業的結果具有類型 void§12.23.5) (與具有 的類型 x相反,在賦值之後具有 的值x,如在非事件類型上定義的其他 和+=-=運算子)。 這可以防止外部程式碼間接檢查事件的底層委派。

範例:下列範例示範如何將事件處理程式附加至Button類別的實例:

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;
}

public class LoginDialog : Form
{
    Button okButton;
    Button cancelButton;

    public LoginDialog()
    {
        okButton = new Button(...);
        okButton.Click += new EventHandler(OkButtonClick);
        cancelButton = new Button(...);
        cancelButton.Click += new EventHandler(CancelButtonClick);
    }

    void OkButtonClick(object sender, EventArgs e)
    {
        // Handle okButton.Click event
    }

    void CancelButtonClick(object sender, EventArgs e)
    {
        // Handle cancelButton.Click event
    }
}

在這裡,實例建 LoginDialog 構函式會建立兩 Button 個實例,並將事件處理程式附加至 Click 事件。

結束範例

類似場地的事件

在包含事件宣告的類別或結構程序文字中,可以使用某些事件,例如字段。 若要以此方式使用,事件不得為抽象或外部,不得明確含有 event_accessor_declaration。 這類事件可用於任何允許欄位的上下文。 欄位包含委派 (§21),它會參考已新增至事件的事件處理常式清單。 如果沒有新增事件處理程式,欄位會包含null

範例:在下列程式代碼中

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;

    protected void OnClick(EventArgs e)
    {
        EventHandler handler = Click;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void Reset() => Click = null;
}

Click 會當做 類別內的 Button 欄位使用。 如範例所示,欄位可以檢查、修改及用於委派呼叫表達式。 類別OnClick中的Button方法「引發」Click事件。 提出事件的概念完全等同於呼叫事件所代表的委派函式,因此,在引發事件方面,並沒有任何特殊的語言結構。 請注意,委託調用前會先進行檢查,以確保委託為非 Null,並且會在本地複本上進行檢查以確保執行緒安全。

在類別的Button宣告之外,Click成員只能用在+=–=運算子的左側。

b.Click += new EventHandler(...);

將委派附加至Click事件的調用清單中。

Click –= new EventHandler(...);

將委派從 Click 事件的調用清單中移除。

結束範例

編譯類似欄位的事件時,編譯器將自動建立儲存機制以保存委託,並為該事件建立可存取子以新增或移除事件處理常式到委託欄位。 新增和移除操作是線程安全的,可以在保持鎖定的情況下完成:為實例事件時鎖定包含的物件(§13.13),或為靜態事件時鎖定System.Type物件(§12.8.18)。

注意:因此,實例事件宣告:

class X
{
    public event D Ev;
}

應該編譯成等同於以下內容:

class X
{
    private D __Ev; // field to hold the delegate

    public event D Ev
    {
        add
        {
            /* Add the delegate in a thread safe way */
        }
        remove
        {
            /* Remove the delegate in a thread safe way */
        }
    }
}

在類別X中,位於Ev+=運算符左側的–=參考會導致叫用新增和移除存取子。 所有其他關於 Ev 的參考都會編譯為參考隱藏欄位 __Ev§12.8.7)。 名稱 「__Ev是任意的;隱藏的欄位可能具有任何名稱或完全沒有名稱。

結尾註釋

15.8.3 事件存取子

注意:事件宣告通常會省略 event_accessor_declaration,如上述範例所示 Button 。 例如,如果無法接受每個事件一個欄位的儲存成本,可能會將它們包含在內。 在這種情況下,類別可以包含 event_accessor_declaration,並使用私人機制來儲存事件處理程式清單。 結尾註釋

事件的 event_accessor_declarations 會指定與新增和移除事件處理程式相關聯的可執行語句。

存取子宣告是由add_accessor_declarationremove_accessor_declaration 所組成。 每個存取子宣告由標記「add」或「remove」組成,後面接著一個區塊add_accessor_declaration相關聯的區塊會指定要在加入事件處理程式時執行的語句,而與 remove_accessor_declaration相關聯的區塊會指定要在移除事件處理程式時執行的語句。

每個 add_accessor_declarationremove_accessor_declaration 都對應到一個有單一值參數(事件類型)和 void 傳回型別的方法。 事件存取子的隱含參數名為 value。 當事件用於事件指派時,會使用適當的事件存取子。 具體來說,如果指派運算符是 += ,則會使用 add 存取子,如果指派運算符是 –= ,則會使用 remove 存取子。 在任何一種情況下,賦值運算子的右操作數會作為事件存取子的參數使用。 add_accessor_declaration或remove_accessor_declaration的區塊應符合 方法在第15.6.9節中描述的規則。 特別是, return 這類區塊中的語句不允許指定表達式。

由於事件存取子隱含具有名為 value 的參數,因此在事件存取子中宣告的局部變數或常數如果使用這個名稱,將會發生編譯時期錯誤。

範例:在下列程式代碼中


class Control : Component
{
    // Unique keys for events
    static readonly object mouseDownEventKey = new object();
    static readonly object mouseUpEventKey = new object();

    // Return event handler associated with key
    protected Delegate GetEventHandler(object key) {...}

    // Add event handler associated with key
    protected void AddEventHandler(object key, Delegate handler) {...}

    // Remove event handler associated with key
    protected void RemoveEventHandler(object key, Delegate handler) {...}

    // MouseDown event
    public event MouseEventHandler MouseDown
    {
        add { AddEventHandler(mouseDownEventKey, value); }
        remove { RemoveEventHandler(mouseDownEventKey, value); }
    }

    // MouseUp event
    public event MouseEventHandler MouseUp
    {
        add { AddEventHandler(mouseUpEventKey, value); }
        remove { RemoveEventHandler(mouseUpEventKey, value); }
    }

    // Invoke the MouseUp event
    protected void OnMouseUp(MouseEventArgs args)
    {
        MouseEventHandler handler;
        handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey);
        if (handler != null)
        {
            handler(this, args);
        }
    }
}

類別 Control 會實作事件的內部儲存機制。 方法 AddEventHandler 會將委派值與索引鍵產生關聯, GetEventHandler 方法會傳回目前與索引鍵相關聯的委派,而 RemoveEventHandler 方法會移除委派做為指定事件的事件處理程式。 大概是基礎儲存機制的設計,使得將 null 委派值與鍵產生關聯並不需要任何成本,因此未處理的事件不會佔用額外的儲存空間。

結束範例

15.8.4 靜態和實例事件

當事件宣告包含 static 修飾詞時,事件會稱為 靜態事件。 當沒有任何 static 修飾詞存在時,事件會稱為 實例事件

靜態事件不與特定實例相關聯,若在靜態事件的存取子中參考 this ,則會引發編譯時期錯誤。

實例事件與類別的指定實例相關聯,而且此實例可以在該事件的存取子中存取為 this~12.8.14)。

靜態和實例成員之間的差異會在 \15.3.8進一步討論。

15.8.5 虛擬、封閉、覆蓋及抽象存取子

虛擬事件宣告會指定該事件的存取子是虛擬的。 修飾符 virtual 同時適用於事件的兩個存取子。

抽象事件宣告會指定事件的存取子是虛擬的,但不提供存取子的實際實作。 相反地,非抽象衍生類別必須通過覆寫事件來提供存取子的自己的實作。 因為抽象事件宣告的存取子沒有提供實際的實作,所以不應提供 event_accessor_declaration

包含 abstractoverride 修飾詞的事件宣告會指定事件是抽象的,並覆寫基底事件。 這類事件的存取器也是抽象的。

抽象事件宣告僅允許在抽象類別 (§15.2.2.2) 和介面 (§19.4.5) 中使用。

繼承的虛擬事件的存取器可以在衍生類別中通過包含一個指定override修飾符的事件宣告來覆寫。 這稱為 覆寫事件宣告。 覆寫事件宣告不會宣告新的事件。 相反,它只是針對現有虛擬事件的訪問器實現進行專門化。

覆寫事件宣告應指定與被覆寫事件完全相同的存取修飾詞和名稱,覆寫事件與被覆寫事件的類型之間應該有一致轉換,並且新增和移除存取子都應在宣告內明確指定。

覆寫事件宣告可以包含sealed修飾詞。 this使用修飾詞可防止衍生類別進一步覆寫事件。 已封裝事件的存取器也會被封裝。

覆寫事件宣告中包含 new 修飾詞時會發生編譯時錯誤。

除了宣告和調用語法的差異之外,虛擬、封閉、覆寫和抽象的存取子的行為與虛擬、封閉、覆寫和抽象的方法完全相同。 具體來說,在 §15.6.4§15.6.5§15.6.6§15.6.7 中所述的規則會套用,就像存取子是相應形式的方法一樣。 每個存取子都會對應到一個方法,此方法具有事件類型的單一值參數、void 回傳型別,以及與包含事件相同的修飾詞。

15.9 索引器

15.9.1 一般

索引 是一個成員,可讓物件以與陣列相同的方式編制索引。 索引器是使用 indexer_declaration來宣告:

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

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

indexer_declaration可以包括一組屬性§23) 和任何一種允許的宣告協助工具類型 (§15.3.6)、new§15.3.5)、 virtual§15.6.4)、 override§15.6.5)、 sealed§15.6.6abstract)、(§15.6.7) 和 extern§15.6.8) 修飾符。 此外,由struct_declaration直接包含的indexer_declaration可以包含readonly修飾詞(16.4.12)。

  • 第一個宣告非引用值索引器。 其值的類型是 type。 這種索引器可以是可讀取和/或可寫入的。
  • 第二個宣告了一個引用類型的索引子。 其值是variable_reference§9.5),可能是 readonly 一個類型為type的變數。 這種索引器只能讀取。

indexer_declaration可以包含一組屬性§23) 和任何一種允許的宣告協助工具類型 (§15.3.6)、new§15.3.5)、 virtual§15.6.4)、 override§15.6.5)、 sealed§15.6.6abstract)、(§15.6.7) 和 extern§15.6.8) 修飾符。

索引器宣告與方法宣告({15.6)在有效的修飾詞組合上受限於相同的規則,但有一個例外狀況是 static 索引器宣告不允許修飾詞。

索引器宣告的類型會指定宣告所引進之索引器的項目類型。

注意:由於索引器被設計用於類似陣列元素的上下文,因此針對陣列定義的名詞元素類型也會與索引器搭配使用。 結尾註釋

除非索引器是顯式的介面成員實作,否則 類型 後接著關鍵字 this。 對於明確的介面成員實作,類型 後面接著 介面類型、“.”和關鍵字 this。 不同於其他成員,索引器沒有使用者定義的名稱。

parameter_list會指定索引器的參數。 索引器的參數清單會對應至方法 (~15.6.2),但至少應指定一個參數,不允許 thisref、 和 out 參數修飾詞。

索引器的類型和parameter_list參考的每個型別至少可以和索引器本身一樣可存取(~7.5.5)。

indexer_body可能由語句主體(~15.7.1)或表達式主體(15.6.1)組成。 在語句主體中,accessor_declarations 必須被“”和“{”標記括起來,以宣告索引器的存取子(參見§15.7.3)。 存取子會指定與讀取和寫入索引器項目相關聯的可執行語句。

indexer_body中,由「=>」所組成的表達式主體,後面接著表達式E和分號,完全相當於語句主體{ get { return E; } },因此僅可用於指定只讀索引器,其中 get 存取子的結果是由單一表達式所指定。

ref_indexer_body可能由語句內容或表達式內容組成。 在語句主體中,get_accessor_declaration 宣告索引器的 get 存取子(§15.7.3)。 存取子會指定與讀取索引器相關聯的可執行語句。

ref_indexer_body中,由=>ref組成的表達式主體,包含variable_referenceV和分號,正好等於語句主體{ get { return ref V; } }

注意:即使存取索引器項目的語法與陣列元素的語法相同,索引器元素也不會分類為變數。 因此,除非索引器是 ref 值並且因此傳回參考(in),否則不可能將索引器元素作為 outref 自變數傳遞。 結尾註釋

索引器的參數列表 定義了索引器的簽名(§7.6)。 具體而言,索引器簽章是由其參數的數目和類型所組成。 參數的專案類型和名稱不是索引器簽章的一部分。

索引器的簽章應該與相同類別中宣告之所有其他索引器的簽章不同。

當索引器宣告包含extern修飾詞時,索引器會稱為外部索引器。 因為外部索引器宣告沒有提供實際實作,因此其accessor_declarations中的每個accessor_body都應該是分號。

範例:下列範例會宣告類別 BitArray ,這個類別會實作索引器來存取位數組中的個別位。

class BitArray
{
    int[] bits;
    int length;

    public BitArray(int length)
    {
        if (length < 0)
        {
            throw new ArgumentException();
        }
        bits = new int[((length - 1) >> 5) + 1];
        this.length = length;
    }

    public int Length => length;

    public bool this[int index]
    {
        get
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            return (bits[index >> 5] & 1 << index) != 0;
        }
        set
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            if (value)
            {
                bits[index >> 5] |= 1 << index;
            }
            else
            {
                bits[index >> 5] &= ~(1 << index);
            }
        }
    }
}

類別的 BitArray 實例會耗用比對應的 bool[] 記憶體少得多(因為前者的每個值只佔用一個位,而不是後者的位 byte),但它允許與 bool[]相同的作業。

下列 CountPrimes 類別會使用 BitArray 和傳統 “篩選” 演算法來計算介於 2 到指定最大值之間的質數:

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        for (int i = 2; i <= max; i++)
        {
            if (!flags[i])
            {
                for (int j = i * 2; j <= max; j += i)
                {
                    flags[j] = true;
                }
                count++;
            }
        }
        return count;
    }

    static void Main(string[] args)
    {
        int max = int.Parse(args[0]);
        int count = Count(max);
        Console.WriteLine($"Found {count} primes between 2 and {max}");
    }
}

請注意,存取 項目的 BitArray 語法與的 bool[]語法完全相同。

下列範例顯示具有兩個參數之索引器之 26×10 方格類別。 第一個參數必須是範圍 A–Z 中的大寫或小寫字母,而第二個參數必須是範圍 0–9 中的整數。

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

結束範例

15.9.2 索引器和屬性差異

索引器和屬性在概念上非常類似,但以下列方式有所不同:

  • 屬性會以其名稱來識別,而索引器則透過其簽章來識別。
  • 屬性是透過 simple_name§12.8.4) 或 member_access§12.8.7) 存取,而索引子元素是透過 element_access§12.8.12.4) 存取。
  • 屬性可以是靜態成員,而索引器一律是實例成員。
  • 屬性的 get 存取子會對應至沒有參數的方法,而索引器的 get 存取子則對應至與索引器具有相同參數清單的方法。
  • 屬性的 set 存取子會對應至一個具有單一名為value參數的方法,而索引器的 set 存取子會對應至一個具有與索引器相同的參數清單且外加一個名為value的其他參數的方法。
  • 在索引器存取子中,如果宣告局部變數或局部常數與索引器參數同名,會造成編譯時錯誤。
  • 在覆寫屬性宣告中,會使用語法 base.P存取繼承的屬性,其中 P 是屬性名稱。 在覆寫索引器宣告中,會使用語法 base[E]來存取繼承的索引器,其中 E 是以逗號分隔的表達式清單。
  • 沒有「自動實作索引器」的概念。 非抽象且非外部的索引器如果具有分號accessor_body,那就是錯誤的。

除了這些差異之外,在 \15.7.3\15.7.5\15.7.6 中定義的所有規則也適用於索引器存取子以及屬性存取子。

在閱讀§15.7.3§15.7.5§15.7.6 時,將屬性替換為索引器這一規則也適用於定義的術語。 具體而言,讀寫屬性會變成讀寫索引器只讀屬性會變成唯讀索引器唯寫屬性會變成唯寫索引器。

15.10 運算子

15.10.1 一般

運算子是定義表達式運算子的意義的成員,可以套用至 類別的實例。 運算子是透過operator_declaration宣告的:

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

operator_body
    : block
    | '=>' expression ';'
    | ';'
    ;

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

注意:前綴邏輯否定(§12.9.4)和後綴 null 寬容運算符(§12.8.9),雖然以相同的語彙標記(!)表示,但它們是不同的。 後者不是可重載的運算子。 結尾註釋

多載運算符有三種類別:一元運算符(•15.10.2)、二元運算元(~15.10.3)和轉換運算符(~15.10.4)。

operator_body 可以是分號、區塊體 (§15.6.1) 或運算式體 (§15.6.1)。 區塊主體是由 區塊所組成,它會指定要在叫用 運算符時執行的語句。 區塊應符合 \15.6.11 中所述之傳值方法的規則。 一個表達式主體由=>組成,接著是一個表達式和分號,並表示當運算子被調用時要執行的一個表達式。

對於extern運算子,運算子本體僅由分號組成。 對於所有其他運算符, operator_body 為區塊主體或表達式主體。

下列規則適用於所有運算符宣告:

  • 運算符宣告應同時包含publicstatic修飾詞。
  • 營運子的參數不應有其他修飾符,除了in之外。
  • 運算符的簽章({15.10.2\15.10.3\15.10.4)與相同類別中宣告之所有其他運算符的簽章不同。
  • 運算符宣告中參考的所有型別,至少可以和運算符本身一樣可存取({7.5.5)。
  • 同一個修飾詞在運算符宣告中出現多次是錯誤的。

每個運算符類別都會施加額外的限制,如下列子集所述。

和其他成員一樣,在基類中宣告的運算符是由衍生類別繼承。 因為運算符宣告一律需要宣告運算子參與運算符簽章的類別或結構,所以在衍生類別中宣告的運算符無法隱藏基類中宣告的運算符。 因此,在運算符宣告中,new 修飾詞永遠不需要,因此也不允許使用。

如需一元運算符和二元運算子的其他資訊,請參閱 <12.4>。

如需轉換運算子的其他資訊,請參閱 §10.5

15.10.2 一元運算符

下列規則適用於一元運算符宣告,其中 T 表示包含運算符宣告的類別或結構實例類型:

  • 一元+-!(僅限邏輯否定)或~運算符應採用類型TT?的單一參數,而且可以傳回任何類型。
  • 一元 ++-- 運算符應採用類型 T 為 或 T? 的單一參數,並傳回衍生自它的相同類型或型別。
  • 一元truefalse運算子應該接受類型TT?的單一參數,並回傳型別bool

一元運算子的簽章包含運算元 Token (+-!~++、、--truefalse) 和單一參數的類型。 傳回型別不是一元運算符簽章的一部分,也不是參數的名稱。

一元運算子 truefalse 需要成對宣告。 如果類別宣告其中一個運算符而不宣告另一個運算符,就會發生編譯時期錯誤。 truefalse運算子在 §12.26 中進一步描述。

範例:下列範例顯示整數向量類別之 operator++ 的實作和後續使用方式:

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // Read-write indexer

    public static IntVector operator++(IntVector iv)
    {
        IntVector temp = new IntVector(iv.Length);
        for (int i = 0; i < iv.Length; i++)
        {
            temp[i] = iv[i] + 1;
        }
        return temp;
    }
}

class Test
{
    static void Main()
    {
        IntVector iv1 = new IntVector(4); // Vector of 4 x 0
        IntVector iv2;
        iv2 = iv1++;              // iv2 contains 4 x 0, iv1 contains 4 x 1
        iv2 = ++iv1;              // iv2 contains 4 x 2, iv1 contains 4 x 2
    }
}

請注意運算子方法如何傳回將 1 新增至運算元所產生的值,就像後置詞遞增和遞減運算子 (§12.8.16) ,以及前置詞遞增和遞減運算子 (§12.9.7) 一樣。 不同於C++,這個方法不應該直接修改其操作數的值,因為這會違反後置遞增運算符的標準語意(^12.8.16)。

結束範例

15.10.3 二元運算符

下列規則適用於二元運算符宣告,其中 T 表示包含運算符宣告的類別或結構實例類型:

  • 二進位非移位運算符應採用兩個參數,其中至少一個參數應具有 類型 TT?,而且可以傳回任何類型。
  • 二進位檔 <<>> 運算子 (§12.13) 應採用兩個參數,第一個參數應具有類型 T OR T? ,第二個參數應具有類型 int OR int?,並且可以傳回任何類型。

二元運算子的簽章包含運算子標記 (+-*/%&|^<<>>==!=><>=<=兩個參數的類型。 傳回型別和參數的名稱不是二元運算符簽章的一部分。

某些二進位運算子需要配對宣告。 對於一對運算子中的任一個運算子的每個宣告,應該有與該對中另一運算子相匹配的宣告。 如果識別轉換存在於其傳回型別與其對應的參數類型之間,則兩個運算符宣告相符。 下列運算子需要配對宣告:

  • 運算符 == 及運算符 !=
  • 運算符 > 及運算符 <
  • 運算符 >= 及運算符 <=

15.10.4 轉換運算元

轉換運算符宣告引進 使用者定義轉換§10.5),這增強了預先定義的隱含和明確轉換。

包含 implicit 關鍵詞的轉換運算符宣告引入使用者定義的隱式轉換。 隱含轉換可能發生在各種情況下,包括函數成員呼叫、類型轉換和賦值。 這會在§10.2中進一步說明。

包含 explicit 關鍵詞的轉換運算符宣告引入使用者定義的顯式轉換。 明確轉換可能出現在類型轉換運算式中,並在 §10.3 中進一步說明。

轉換運算子會從轉換運算子的參數類型所表示的來源類型轉換成目標類型,以轉換運算符的傳回型別表示。

對於給定的來源類型 S 和目標類型 T,如果 ST 是可為 Null 的值類型,則讓 S₀T₀ 參考其基礎類型;否則,S₀T₀ 分別等於 ST。 只有在下列所有條件都成立時,才允許類別或結構宣告從來源類型 S 轉換成目標類型 T

  • S₀T₀ 是不同的類型。

  • S₀T₀ 是包含運算符宣告的類別或結構實例類型。

  • S₀T₀ 都不是interface_type

  • 排除使用者定義的轉換,從 ST 或從 TS 的轉換不存在。

基於這些規則的目的,與或 S 相關聯的T任何型別參數都會被視為與其他類型沒有任何繼承關聯性的唯一型別,而且會忽略這些類型參數的任何條件約束。

範例:在下列內容中:

class C<T> {...}

class D<T> : C<T>
{
    public static implicit operator C<int>(D<T> value) {...}     // Ok
    public static implicit operator C<string>(D<T> value) {...}  // Ok
    public static implicit operator C<T>(D<T> value) {...}       // Error
}

允許前兩個運算符宣告,因為 Tintstring分別被視為沒有關聯性的唯一型別。 不過,第三個運算符是錯誤,因為 C<T> 是的 D<T>基類。

結束範例

根據第二個規則,轉換運算子應該將運算元轉換為或從運算元宣告的類別或結構類型。

範例:類別或結構類型C可以定義從 Cint 和從 intC 的轉換,但不能從 int 轉換成 bool結束範例

無法直接重新定義預先定義的轉換。 因此,不允許轉換運算符從object轉換為其他類型或從其他類型轉換為object,因為與所有其他類型之間已存在隱含和明確的轉換。 同樣地,轉換的來源和目標類型都不能是另一種轉換的基底類型,因為轉換會已經存在。 不過,可以在泛型型別上宣告運算子,針對特定型別引數,指定已經存在的轉換作為預先定義的轉換。

範例:

struct Convertible<T>
{
    public static implicit operator Convertible<T>(T value) {...}
    public static explicit operator T(Convertible<T> value) {...}
}

當類型 object 被指定為 T 的類型參數時,第二個運算符聲明了一種已存在的轉換(從任何類型到類型物件的轉換已有隱含轉換,因此也存在明確轉換)。

結束範例

如果預先定義的轉換存在於兩種類型之間,則會忽略這些類型之間的任何使用者定義的轉換。 具體而言:

  • 如果預先定義的隱含轉換 (~10.2) 從 類型 S 到類型 T存在,則會忽略從 ST 的所有使用者定義轉換(隱含或明確)。
  • 如果預先定義的明確轉換 (~10.3) 從類型 S 到類型 T存在,則會忽略從 ST 的任何使用者定義明確轉換。 此外:
    • S如果 或 T 是介面類型,則會忽略從 ST 的使用者定義隱含轉換。
    • 否則,仍會考慮從 ST 的使用者定義隱含轉換。

對於除 object 外的所有型別,上述 Convertible<T> 類型所宣告的運算符不會與預先定義的轉換衝突。

範例:

void F(int i, Convertible<int> n)
{
    i = n;                    // Error
    i = (int)n;               // User-defined explicit conversion
    n = i;                    // User-defined implicit conversion
    n = (Convertible<int>)i;  // User-defined implicit conversion
}

不過,針對類型 object,預定義的轉換會在所有情況下隱藏使用者定義的轉換,但有一種情況除外:

void F(object o, Convertible<object> n)
{
    o = n;                       // Pre-defined boxing conversion
    o = (object)n;               // Pre-defined boxing conversion
    n = o;                       // User-defined implicit conversion
    n = (Convertible<object>)o;  // Pre-defined unboxing conversion
}

結束範例

不允許使用者定義從或到interface_type的轉換。 特別是,這項限制可確保在轉換成interface_type時,不會發生任何使用者定義的轉換,並且只有當被轉換的確實實作了指定的object時,這樣的轉換才會成功。

轉換運算子的簽章包含來源類型和目標類型。 (這是傳回類型參與簽章的唯一成員形式。轉換運算子的隱含或明確分類不是運算子簽章的一部分。 因此,類別或結構無法同時宣告具有相同來源和目標類型的隱含和明確轉換運算元。

注意:一般而言,使用者定義隱含轉換的設計應該永遠不會擲回例外狀況,且永遠不會遺失資訊。 如果使用者定義轉換可能會引發例外狀況(例如,因為來源自變數超出範圍)或資訊遺失(例如捨棄高階位),則該轉換應該定義為明確的轉換。 結尾註釋

範例:在下列程式代碼中

public struct Digit
{
    byte value;

    public Digit(byte value)
    {
        if (value < 0 || value > 9)
        {
            throw new ArgumentException();
        }
        this.value = value;
    }

    public static implicit operator byte(Digit d) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

Digit 轉換成 byte 是隱含的,因為它永遠不會拋出異常或丟失資訊。但從 byte 轉換成 Digit 的轉換是明確的,因為 Digit 只能代表 byte 的可能值子集。

結束範例

15.11 實例建構子

15.11.1 一般

實例建構函式是負責實作某個類別實例所需初始化操作的成員。 實例建構函式是使用 constructor_declaration來宣告:

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

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

constructor_declaration可以包含一組屬性§23)、任何一種允許的宣告協助工具類型 (§15.3.6) ,以及 extern§15.6.8) 修飾詞。 不允許建構函式宣告包含相同的修飾詞多次。

constructor_declarator標識碼應命名該實例建構函式所屬的類別。 如果指定任何其他名稱,就會發生編譯時期錯誤。

實例建構函式的選擇性parameter_list受限於與方法parameter_list相同的規則(§15.6)。 由於參數的this修飾詞只適用於擴充方法(§15.6.10),建構函式的參數列表中沒有任何參數應包含this修飾詞。 參數清單會定義實例建構函式的簽章 ({7.6),並控管進程,其中多載解析 ({12.6.4) 會選取調用中的特定實例建構函式。

實例建構函式的parameter_list中所參考的每個型別,至少應該和建構函式本身一樣可存取(§7.5.5)。

選擇性constructor_initializer會指定要叫用的另一個實例建構函式,再執行這個實例建構函式constructor_body中指定的語句。 這會在§15.11.2中進一步說明。

當建構函式宣告包含extern修飾詞時,建構函式會稱為外部建構函式。 由於外部建構函式宣告沒有提供實際實作,因此其 constructor_body 包含分號。 對於所有其他建構函式,構造函數主體 是由任一個建構函式所組成

  • 區塊,其指定語句用於初始化一個新的類別實例;或
  • 表達式主體由 => 組成,後接 表達式 和分號,用於表示單一表達式,初始化類別的新實例。

一個作為區塊或表達式主體的constructor_body恰好對應於具有返回型別的實例方法的void§15.6.11)。

實例建構函式不會繼承。 因此,類別除了類別中實際宣告的實例建構函式之外,沒有實例建構函式,但例外狀況是,如果類別不包含任何實例建構函式宣告,則會自動提供預設實例建構函式({15.11.5)。

實例建構函式會由 object_creation_expressions§12.8.17.2)和 constructor_initializers 來調用。

15.11.2 建構函式起始子

所有實例建構函式(除了類別object的建構函式外)都會隱含地在constructor_body之前調用另一個實例建構函式。 根據constructor_initializer決定要隱含呼叫的建構子:

  • 實例建構函式初始化器 base(argument_list) (其中 argument_list 是可選擇的)會導致從直接基類調用實例建構函式。 使用argument_list選取該建構函式,並依據§12.6.4的重載解析規則。 候選實例建構函式集合是由直接基類的所有可存取實例建構函式所組成。 如果此集合是空的,或無法識別單一最佳實例建構函式,就會發生編譯時期錯誤。
  • 同一類別中,格式為 this(argument_list) (其中 argument_list 是可選的)的實例構造函數初始器會調用另一個實例構造函數。 建構函式是使用argument_list§12.6.4的多載解析規則來選取。 候選實例建構函式集合是由類別本身宣告的所有實例建構函式所組成。 如果產生的一組適用的實例建構函式是空的,或無法識別單一最佳實例建構函式,就會發生編譯時期錯誤。 如果實例建構函式宣告透過一或多個建構函式初始化表達式的鏈結叫用自己,就會發生編譯時期錯誤。

如果實例建構函式沒有建構式初始化,則會隱含提供形式為 base() 的初始化。

注意:因此,"實例構造函式" 的宣告形式

C(...) {...}

完全等同於

C(...) : base() {...}

結尾註釋

實例建構函式宣告parameter_list所提供的參數範圍包括該宣告的建構函式初始化表達式。 因此,允許建構函式的初始化器存取建構函式的參數。

範例:

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

結束範例

實例建構函式初始化表達式無法存取所建立的實例。 因此,在構造函式初始化引數表達式中引用這個會在編譯時報錯,因為引數表達式中通過simple_name引用任何實例成員也是編譯時錯誤。

15.11.3 實例變數初始化表達式

當一個非 extern 的實例建構函式沒有建構函式初始化器,或其建構函式初始化器的形式為 base(...) 時,該建構函式會隱含地執行其類別中所宣告的實例欄位的 variable_initializer 所指定的初始化操作。 這相當於在進入建構函式時立即執行,並在直接基類建構函式隱式調用之前的指派序列。 變數初始化表達式會以出現在類別宣告 ({15.5.6) 中的文字順序執行。

不需要由 extern 實例建構函式執行變數初始化表達式。

15.11.4 建構子執行

變數初始化表達式會轉換成指派語句,而且這些指派語句會在基類實例建構函式的調用之前執行。 此順序可確保所有實例欄位都會透過其變數初始化器進行初始化,才執行任何有存取該實例權限的語句。

範例:給定以下內容:

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

    public override void PrintFields() =>
        Console.WriteLine($"x = {x}, y = {y}");
}

當 new B() 用來建立 的 B實體時,會產生下列輸出:

x = 1, y = 0

的值 x 是 1,因為變數初始化運算式是在叫用基類實例建構函式之前執行。 不過,y 的值是 0(即 int 的預設值),因為對 y 的指派操作不會在基類建構函式返回之前執行。 將實例變數初始化表達式和建構函式初始化表達式視為在constructor_body之前自動插入的語句很有用。 範例

class A
{
    int x = 1, y = -1, count;

    public A()
    {
        count = 0;
    }

    public A(int n)
    {
        count = n;
    }
}

class B : A
{
    double sqrt2 = Math.Sqrt(2.0);
    ArrayList items = new ArrayList(100);
    int max;

    public B(): this(100)
    {
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        max = n;
    }
}

包含數個變數初始化,並且也包含這兩種形式的建構函式初始化(basethis)。 此範例會對應至以下所示的程式代碼,其中每個批注都表示自動插入的語句(自動插入建構函式調用所使用的語法無效,而只是用來說明機制)。

class A
{
    int x, y, count;
    public A()
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = 0;
    }

    public A(int n)
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = n;
    }
}

class B : A
{
    double sqrt2;
    ArrayList items;
    int max;
    public B() : this(100)
    {
        B(100);                      // Invoke B(int) constructor
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        sqrt2 = Math.Sqrt(2.0);      // Variable initializer
        items = new ArrayList(100);  // Variable initializer
        A(n - 1);                    // Invoke A(int) constructor
        max = n;
    }
}

結束範例

15.11.5 預設建構函式

如果類別不包含實例建構函式宣告,則會自動提供預設實例建構函式。 該預設建構函式只是叫用直接基類的建構函式,如同它具有形式為 base() 的建構函式初始化器一樣。 如果類別是抽象的,則預設建構函式的宣告可見性是保護的。 否則,默認建構函式的可訪問性是公開的。

注意:因此,預設建構子一律具有此格式。

protected C(): base() {}

public C(): base() {}

其中 C 是類別的名稱。

結尾註釋

如果多載解析無法判斷基類建構函式初始化器的唯一最佳候選人,則會產生編譯時期錯誤。

範例:在下列程式代碼中

class Message
{
    object sender;
    string text;
}

提供預設建構函式,因為類別不包含實例建構函式宣告。 因此,此範例完全相當於

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

結束範例

15.12 靜態建構函式

靜態建構函式是一個成員,可實作初始化封閉類別所需的動作。 靜態建構函式是使用 static_constructor_declarations 宣告:

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

static_constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

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

static_constructor_declaration可以包含一組屬性§23) 和修extern飾符 (§15.6.8)。

static_constructor_declaration的標識碼應命名宣告靜態建構函式的類別。 如果指定任何其他名稱,就會發生編譯時期錯誤。

當靜態建構函式宣告包含 extern 修飾詞時,靜態建構函式會稱為 外部靜態建構函式。 由於外部靜態建構函式宣告沒有提供實際的實作,因此其 static_constructor_body 是由分號所組成。 針對所有其他靜態建構函式宣告, static_constructor_body 是由任一項所組成

  • 區塊,指定要執行以初始化 類別的語句;或
  • 表達式主體由 =>表達式 組成,並以分號作結,表示要執行的單一表達式以初始化類別。

static_constructor_body區塊或表達式主體,對應於具有傳回型別的靜態方法的void§15.6.11)。

靜態建構函式不會繼承,而且無法直接呼叫。

封閉類別的靜態建構函式最多會在指定的應用程式域中執行一次。 靜態建構函式的執行是由應用程式域內發生下列第一個事件所觸發:

  • 創建一個類的實例。
  • 類別的任何靜態成員都會被參考。

如果類別包含執行開始的方法(§7.1),則該類別的靜態建構函式會在呼叫Main方法之前執行。

若要初始化新的封閉式類別類型,應該為該特定封閉類型建立一組新的靜態字段 (~15.5.2)。 每個靜態字段都應該初始化為其預設值(~15.5.5)。 接下來:

  • 如果沒有靜態建構函式或非外部靜態建構函式,則:
    • 靜態欄位初始化運算式 (~15.5.6.2) 應針對這些靜態字段執行:
    • 然後,若有非外部的靜態建構函式,應予以執行。
  • 否則,如果有外部靜態建構函式,則應該執行它。 外部靜態建構函式不需要執行靜態變數初始化表達式。

範例:範例

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

    public static void F()
    {
        Console.WriteLine("A.F");
    }
}

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

    public static void F()
    {
        Console.WriteLine("B.F");
    }
}

必須產生輸出:

Init A
A.F
Init B
B.F

因為的靜態建構函式執行 A是由呼叫 A.F所觸發,而的靜態建構函式執行 B是由呼叫 B.F所觸發。

結束範例

建構迴圈相依性,允許以預設值狀態觀察具有變數初始化表達式的靜態字段。

範例:範例

class A
{
    public static int X;

    static A()
    {
        X = B.Y + 1;
    }
}

class B
{
    public static int Y = A.X + 1;

    static B() {}

    static void Main()
    {
        Console.WriteLine($"X = {A.X}, Y = {B.Y}");
    }
}

產生輸出

X = 1, Y = 2

若要執行 Main 方法,系統會先執行 B.Y的初始化表達式,再執行 類別 B的靜態建構函式。 Y的初始化器會執行Astatic建構函式,因為參考了A.X的值。 在A的靜態建構式中,接著會計算X的值,而在過程中,它會取得Y的預設值,其值為零。 A.X 因此初始化為 1。 接著,執行 A 的靜態字段初始化器和靜態建構子的程式會完成,然後返回以計算 Y 的初始值,結果為 2。

結束範例

由於靜態建構函式只針對每個封閉的建構類別類型執行一次,所以對無法透過條件約束 (~15.2.5) 在編譯階段無法檢查的類型參數強制執行運行時間檢查是一個便利的地方。

範例:下面的類別使用靜態建構函式來強制類型參數是枚舉類型:

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

結束範例

15.13 終結器

注意:在此規格的舊版中,現在所謂的「終結器」曾被稱為「解構器」。 經驗表明,「解構子」一詞會造成混淆,而且往往導致不正確的預期,尤其是熟悉C++的程式設計人員。 在C++中,解構函式是以具決定性的方式呼叫,而在 C# 中,完成項則不是。 若要從 C# 取得判斷行為,應該使用 Dispose結尾註釋

終結器是用於實現完成類別實例所需活動的成員。 使用 finalizer_declaration 宣告終結器:

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

finalizer_body
    : block
    | '=>' expression ';'
    | ';'
    ;

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

finalizer_declaration可以包括一組屬性§23)。

終結器宣告器的標識符應與終結器宣告所在的類別同名。 如果指定任何其他名稱,就會發生編譯時期錯誤。

當完成項宣告包含extern修飾詞時,完成項會稱為外部完成項。 因為外部完成項宣告沒有提供實際的實作,因此其 finalizer_body 是由分號所組成。 對於所有其他的完成程序,finalizer_body 的組成為

  • 程式區塊,指定要執行的語句,以對類別實例進行最終處理。
  • 或者是表達式體,它由 => 後跟 表達式 和分號組成,表示需執行的單一表達式,以完成類別實例的建立。

finalizer_body 作為 區塊 或表達式主體,精確對應到回傳型別為 的實例方法的 method_body (參見 §15.6.11)。

終結器不會被繼承。 因此,類別除了可以在該類別中宣告的終結器以外,沒有其他終結器。

注意:因為完成項不需要參數,所以不能多載,因此類別最多可以有一個完成項。 結尾註釋

終結器會自動調用,且無法被顯式調用。 當任何程式代碼都無法使用該實例時,實例就有資格進行最終處理。 實例的終結器執行可能會在該實例符合終結條件後的任何時間發生(§7.9)。 當實例被釋放時,該實例繼承鏈中的終結器會依序從最衍生到最不衍生被呼叫。 最終化器可以在任何執行緒上執行。 如需進一步了解管理完成項的執行時機和方式的規則,請參閱§7.9

範例:範例的輸出

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

B's finalizer
A's finalizer

因為繼承鏈中的終結器會依序被呼叫,從最衍生到最不衍生。

結束範例

完成項是藉由在Finalize上覆寫System.Object虛擬方法來實作。 不允許 C# 程式覆寫此方法,或直接呼叫此方法及其覆寫版本。

範例:例如,程式

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

包含兩個錯誤。

結束範例

編譯器應該表現得如同這個方法及其覆寫完全不存在一樣。

範例:因此,此程式:

class A
{
    void Finalize() {}  // Permitted
}

有效且顯示的方法會隱藏 System.ObjectFinalize 方法。

結束範例

如需從終結器擲回例外狀況時行為的討論,請參閱 §22.4

15.14 異步函式

15.14.1 一般

具有修飾詞的方法 §15.6)、匿名函式 (§12.21) 或本機函式 (async) 稱為非同步函式。 一般而言,「async」是用來描述具有 修飾詞的任何函式。

異步函式的參數列表中指定任何 inoutref 參數,或任何 ref struct 型別的參數都會導致編譯時期錯誤。

異步方法 的return_type 應該是 void工作類型異步反覆運算器類型~15.15)。 對於產生結果值的異步方法,工作類型或異步反覆運算器類型 (\15.15.3) 應該是泛型。 對於不會產生結果值的異步方法,工作類型不得為泛型。 這類類型會分別在此規格 «TaskType»<T> 中稱為 和 «TaskType»。 從 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> 建構的標準連結庫類型和System.Threading.Tasks.ValueTask<T>類型是工作類型,以及透過 屬性相關聯的類別、結構或介面類型。 這類類型在此規格 «TaskBuilderType»<T> 中稱為 和 «TaskBuilderType»。 工作類型最多可以有一個類型參數,而且不能巢狀於泛型類型中。

傳回任務類型的異步方法稱為返回任務

工作類型在確切定義中可能會有所不同,但從語言的觀點來看,工作類型處於其中一個不完整成功錯誤的狀態故障的工作會記錄相關的例外狀況。 成功的«TaskType»<T> 記錄T類型的結果。 工作類型是可等待的,因此工作可以是等待運算式的運算元 (§12.9.9)。

範例:工作類型 MyTask<T> 與工作產生器類型和 MyTaskMethodBuilder<T> awaiter 類型 Awaiter<T>相關聯:

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

結束範例

工作產生器類型是對應至特定工作類型的類別或結構類型(~15.14.2)。 工作產生器類型應該完全符合其對應工作類型的宣告存取範圍。

注意: 如果宣告 internal工作類型,則對應的產生器類型也必須宣告 internal 並在相同的元件中定義。 如果工作類型巢狀在另一個類型內,則工作產生器類型也必須巢狀在相同的類型中。 結尾註釋

非同步函式能夠透過其本文中的 await 運算式 (§12.9.9) 暫停評估。 暫停的 await 表達式可以稍後通過 繼續委派 恢復評估。 繼續委派的類型為 System.Action,而當它被叫用時,異步函式調用的評估將會從離開時的等候表達式處繼續。 非同步函式調用的目前呼叫端,如果該調用從未被暫停,則是原始呼叫端;否則是恢復委派的最近呼叫端。

15.14.2 工作類型產生器模式

工作產生器類型最多可以有一個類型參數,而且不能巢狀於泛型類型中。 工作產生器類型應具有下列成員(針對非泛型工作產生器類型, SetResult 沒有參數),具有宣告 public 的存取範圍:

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<T> Create();
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
                where TStateMachine : IAsyncStateMachine;
    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public «TaskType»<T> Task { get; }
}

編譯程式應該產生使用 «TaskBuilderType» 的程序代碼,以實作暫停和繼續異步函式評估的語意。 編譯程式應使用 «TaskBuilderType» ,如下所示:

  • «TaskBuilderType».Create() 被調用來建立名為 builder 的 «TaskBuilderType» 實例於此清單中。
  • builder.Start(ref stateMachine) 被呼叫以將建構器與編譯器生成的狀態機器實例stateMachine建立關聯。
    • 建置者應在 stateMachine.MoveNext() 中 或在 Start() 返回後調用 Start(),以推進狀態機器。
  • Start() 傳回後,async方法會叫用builder.Task,使工作從非同步方法中返回。
  • 每次呼叫 stateMachine.MoveNext() 都會推進狀態機器。
  • 如果狀態機器順利完成,將調用 builder.SetResult(),並使用方法的返回值(如果有)。
  • 否則,如果在狀態機中拋出異常,將調用e
  • 如果狀態機器到達await expr表示式,則會調用expr.GetAwaiter()
  • 如果 awaiter 實作 ICriticalNotifyCompletionIsCompleted 為 false,則狀態機器會叫用 builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
    • AwaitUnsafeOnCompleted() 應在等待者完成時,使用含有呼叫 awaiter.UnsafeOnCompleted(action)Action 來呼叫 stateMachine.MoveNext()
  • 否則,狀態機調用builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
    • AwaitOnCompleted() 應在等待者完成時,使用含有呼叫 awaiter.OnCompleted(action)Action 來呼叫 stateMachine.MoveNext()
  • SetStateMachine(IAsyncStateMachine) 編譯器產生的 IAsyncStateMachine 實作可能會被呼叫,以識別與狀態機實例相關的生成器實例,特別是在狀態機以值類型實作的情況下。
    • 如果建構器呼叫 stateMachine.SetStateMachine(stateMachine),那麼stateMachine將會在與建構器實例相關聯的builder.SetStateMachine(stateMachine)上呼叫 stateMachine

注意:針對 SetResult(T result)«TaskType»<T> Task { get; },參數和引數分別必須可轉換成識別 T。 這可讓任務類型構建器支援元組之類的類型,其中兩個不同類型在識別上是可轉換的。 結尾註釋

15.14.3 工作傳回異步函式的評估

當調用一個返回任務的異步函數時,會生成一個返回任務類型的實例。 這稱為 異步函式的傳回工作 。 工作一開始處於 不完整 的狀態。

然後,async 函數的主體會被評估,直到它暫停(因遇到 await 表達式)或結束為止,這時控制權會連同返回的任務一併交還給調用者。

當異步函式的主體終止時,返回的工作會被移出未完成的狀態:

  • 如果函式主體因為到達 return 陳述式或主體結尾而終止,則在返回任務中會記錄任何結果值,並且該任務會進入 成功 狀態。
  • 如果函式主體因為未攔截 OperationCanceledException 而終止,則例外狀況會記錄在處於已 取消 狀態的返回任務中。
  • 如果函式主體因任何其他未捕獲的例外狀況(§13.10.6)而終止,則會在進入 故障 狀態的傳回工作中記錄例外狀況。

15.14.4 評估回傳 void 的非同步函式

如果異步函式的傳回類型是 void,則評估與上述方法不同:因為不會傳回任何工作,因此函式會改為將完成和例外狀況傳達給目前線程的同步處理內容。 同步上下文的確切定義依實現而定,但它表示當前線程運行的位置。 在開始評估、成功完成或導致擲回未捕捉的例外狀況時,會通知回傳void的非同步函式的同步處理內容。

zh-TW: 這可讓上下文追蹤有多少個 void 傳回的異步函式正在其下執行,並決定如何處理它們引發的例外。

15.15 同步和異步反覆運算器

15.15.1 一般

使用反覆運算器區塊實作的函式成員(~12.6)或區域函式(~13.6.4)稱為反覆運算器 只要對應函式的傳回類型是其中一個列舉器介面 (§15.15.2) 或其中一個可列舉介面 (§15.15.3),就可以使用反覆運算器區塊作為函式的主體。

使用反覆運算器區塊 (§13.3) 實作的非同步函式 (§15.14) 或本機函式 (§13.6.4) 稱為非同步反覆運算器 只要對應函式的傳回類型是非同步列舉器介面 (§15.15.2) 或非同步列舉式介面 (§15.15.3),就可以使用非同步反覆運算器區塊作為函式的主體。

反覆運算器區塊可能會以 method_bodyoperator_bodyaccessor_body的形式發生,而事件、實例建構函式、靜態建構函式和完成項不得實作為同步或異步反覆運算器。

當使用迭代器區塊實作函式時,函式的參數清單會指定任何 inoutref 參數,或類型的參數 ref struct ,這是編譯階段錯誤。

非同步反覆運算器應該支援取消非同步作業。 這在 §23.5.8 中進行了描述。

15.15.2 列舉器介面

列舉值介面s 是非泛型介面System.Collections.IEnumerator和泛型介面System.Collections.Generic.IEnumerator<T>

非同步枚舉器介面是泛型介面System.Collections.Generic.IAsyncEnumerator<T>

為了簡潔起見,在此子命令及其同層級中,這些介面分別參考為 IEnumeratorIEnumerator<T>IAsyncEnumerator<T>

15.15.3 可列舉介面

可列舉的介面s 是非泛型介面System.Collections.IEnumerable和泛型介面。System.Collections.Generic.IEnumerable<T>

非同步列舉介面是泛型介面System.Collections.Generic.IAsyncEnumerable<T>

為了簡潔起見,在此子命令及其同層級中,這些介面分別參考為 IEnumerableIEnumerable<T>IAsyncEnumerable<T>

15.15.4 傳輸類型

反覆運算器會產生一連串的值,這些值全都相同。 此類型稱為反覆運算器的產值類型

  • 傳回 IEnumeratorIEnumerable 之反覆運算器的 yield 類型為 object
  • 傳回IEnumerator<T>IAsyncEnumerator<T>IEnumerable<T>IAsyncEnumerable<T>之反覆運算器的 yield 型別為T

15.15.5 枚舉器物件

15.15.5.1 一般

當使用反覆運算器區塊實作傳回列舉值介面類型或非同步列舉值介面類型的函式成員或本機函式時,叫用函式不會立即執行反覆運算器區塊中的程式代碼。 相反地,會建立並傳回列舉器物件。 這個物件會封裝反覆運算器區塊中指定的程序代碼,並在叫用列舉值物件的 MoveNextMoveNextAsync 方法時,於反覆運算器區塊中執行程序代碼。 列舉值物件具有下列特性:

  • 它會實作 System.IDisposableIEnumeratorIEnumerator<T>System.IAsyncDisposableIAsyncEnumerator<T>,其中 T 是反覆運算器的 yield 類型。
  • 它會使用傳遞給函式的引數值 (如果有) 和實例值的複本來初始化。
  • 它有四個潛在狀態, 在之前執行中、 暫停之後,且一開始處於 之前 狀態。

列舉值物件通常是編譯程式產生的列舉值類別的實例,可將程式代碼封裝在反覆運算器區塊中,並實作列舉值介面,但可能會有其他實作方法。 如果編譯器產生列舉值類別,則該類別會直接或間接巢狀在包含函式的類別中,具有私人可存取性,而且會保留名稱供編譯器使用 (§6.4.3) 。

列舉值物件可能會實作比上述指定的介面更多。

下列子句描述成員所需遵循的行為,以推進列舉器、從列舉器擷取目前值,以及處置列舉器所使用的資源。 這些分別定義於同步和異步列舉者的成員中:

  • 若要推進列舉器: MoveNextMoveNextAsync
  • 若要擷取目前的值: Current
  • 若要處置資源: DisposeDisposeAsync

列舉值物件不支援 IEnumerator.Reset 方法。 調用此方法會導致拋出System.NotSupportedException

同步和異步反覆運算器區塊的差異在於異步反覆運算器成員會傳回工作類型,而且可能會等候。

15.15.5.2 推進列舉器

MoveNext列舉值物件的 和 MoveNextAsync 方法會封裝反覆運算器區塊的程序代碼。 呼叫 MoveNextMoveNextAsync 方法會執行反覆運算器區塊中的程式碼,並適當地設定列舉物件的 Current 屬性。

MoveNextbool 回值,其意義如下所述。 MoveNextAsync 會傳回一個 ValueTask<bool>§15.14.3)。 從MoveNextAsync傳回的工作結果值具有與MoveNext傳回結果值相同的意義。 在下列描述中,描述的 MoveNext 動作會套用至 MoveNextAsync ,但有下列差異:其中表示 MoveNext 會傳回 truefalseMoveNextAsync 將其工作設定為 已完成 狀態,並將工作的結果值設定為對應的 truefalse 值。

MoveNextMoveNextAsync 所執行的精確動作取決於被叫用時列舉值物件的狀態。

  • 如果列舉值物件的狀態是 之前,則叫用 MoveNext
    • 將狀態變更為 執行中。
    • 將反覆運算器區塊的參數 (包括 this) 初始化為列舉值物件初始化時所儲存的自變數值和實例值。
    • 從頭開始執行反覆運算器區塊,直到執行中斷為止(如下所述)。
  • 如果列舉值物件的狀態正在 執行,則叫用的結果 MoveNext 未指定。
  • 如果列舉值物件 的狀態已暫止,則叫用MoveNext:
    • 將狀態變更為 執行中。
    • 將所有局部變數和參數的值(包括 this)還原至上次暫停反覆運算器區塊執行時所儲存的值。

      注意:這些變數所參考之任何對象的內容,在先前呼叫 MoveNext之後可能已變更。 結尾註釋

    • 立即在導致執行暫停的 yield return 語句後繼續執行迭代器區塊,並持續執行直至被中斷執行為止(如下所述)。
  • 如果列舉物件的狀態為 after,呼叫 MoveNext 會傳回 false。

MoveNext 執行反覆運算器區塊時,執行可以透過四種方式中斷:透過 yield return 語句、透過 yield break 語句、遇到反覆運算器區塊的結尾,以及擲回並傳播出反覆運算器區塊的例外狀況。

附註MoveNextAsync 如果評估 await 等待尚未完成之工作類型的運算式,則會暫停。 結尾註釋

  • yield return語句在遇到時(§9.4.4.20):
    • 語句中指定的表達式會被評估,隱含轉換成產生類型,並指派給列舉器物件的Current屬性。
    • 迭代器主體的執行已暫停。 所有局部變數和參數(包括 this)的值會被儲存,這個 yield return 語句的位置也同樣會被記錄。 yield return如果語句位於一或多個try區塊內,則相關聯的 finally 區塊此時不會被執行
    • 列舉值物件的狀態會變更為 暫止
    • 方法 MoveNexttrue 傳回其呼叫端,指出反覆專案已成功前進到下一個值。
  • yield break語句在遇到時(§9.4.4.20):
    • yield break如果語句位於一或多個try區塊內,則會執行相關聯的finally區塊。
    • 列舉值物件的狀態會變更為 之後
    • 方法 MoveNextfalse 傳回其呼叫端,指出反覆專案已完成。
  • 遇到迭代器程式區塊的結尾時:
    • 列舉值物件的狀態會變更為 之後
    • 方法 MoveNextfalse 傳回其呼叫端,指出反覆專案已完成。
  • 當例外被擲出並從疊代器區塊向外傳播時:
    • 在迭代器主體中,適當的finally塊將已因異常傳播而執行。
    • 列舉值物件的狀態會變更為 之後
    • 例外狀況傳播會繼續傳至 MoveNext 方法的呼叫端。

15.15.5.3 擷取目前的值

列舉值對象的 Current 屬性會受到 yield return 反覆運算器區塊中的 語句影響。

注意:屬性 Current 是同步和異步反覆運算器物件的同步屬性。 結尾註釋

當列舉值對象處於 暫止 狀態時,的值 Current 是先前呼叫 MoveNext所設定的值。 當列舉物件處於開始運行結束的狀態時,存取Current的結果是未定義的。

針對具有非object型別的 yield 反覆運算器,透過列舉值物件的Current實作存取的結果,等同於透過列舉值物件的IEnumerableCurrent實作存取IEnumerator<T>後,將結果轉換為object

15.15.5.4 處置資源

DisposeDisposeAsync 方法可用來清除反覆專案,方法是將列舉值物件帶入之後的狀態。

  • 如果列舉值物件的狀態是之前,呼叫Dispose會將狀態變更為之後
  • 如果列舉值物件的狀態正在 執行,則叫用的結果 Dispose 未指定。
  • 如果列舉值物件 的狀態已暫止, 則叫用 Dispose
    • 將狀態變更為 執行中。
    • 執行任何 finally 區塊,就像上次執行的 yield return 語句和 yield break 語句一樣。 如果這導致例外狀況被擲回並從反覆運算器主體傳播出來,則列舉值物件的狀態會設定為after,且該例外狀況將傳播至Dispose方法的呼叫者。
    • 將狀態更改為after
  • 如果列舉值物件的狀態在 之後,叫 Dispose 用不會有任何影響。

15.15.6 可列舉物件

15.15.6.1 一般

當使用反覆運算器區塊實作傳回可列舉介面類型或非同步列舉介面類型的函式成員或本機函式時,叫用函式不會立即執行反覆運算器區塊中的程式代碼。 相反地,會建立可列舉的物件並傳回

同步列舉物件實 IEnumerable 作 和 IEnumerable<T>,其中 T 是疊代器的 yield 類型。 其 GetEnumerator 方法會傳回列舉值物件 (§15.15.5) 。 非同步列舉物件會IAsyncEnumerable<T>T實作其中 是疊代器的 yield 類型。 其 GetAsyncEnumerator 方法會傳回非同步列舉值物件 (§15.15.5) 。

可列舉物件會使用引數值 (如果有的話) 和傳遞給函式的實例值的複本來初始化。

可列舉物件通常是編譯程式產生的可列舉類別的實例,可將程式代碼封裝在反覆運算器區塊中,並實作可列舉的介面,但可能採用其他實作方法。 如果編譯器產生可列舉類別,則該類別會直接或間接巢巢在包含函式的類別中,它將具有私人協助工具,而且會保留名稱供編譯器使用 (§6.4.3) 。

可列舉的物件可能會實作比上述指定的介面更多的介面。

注意:例如,可列舉的物件也可以實作IEnumeratorIEnumerator<T>,使其既作為可列舉物件,也作為列舉器。 一般而言,這類實作會在第一次呼叫 GetEnumerator 時,傳回其自身的實例(以節省分配)。 如果有後續的GetEnumerator調用,它會返回一個新的類別實例,通常屬於同一個類別,因此對不同列舉器實例的呼叫不會互相影響。 結尾註釋

15.15.6.2 GetEnumerator 或 GetAsyncEnumerator 方法

可列舉物件提供 GetEnumeratorIEnumerable 介面方法的IEnumerable<T>實作。 這兩 GetEnumerator 種方法會共用一個通用實作,這個實作會取得並傳回可用的列舉值物件。 枚舉器物件會以初始化可列舉物件時所儲存的參數值和實例值進行初始化,否則枚舉器物件的功能如 §15.15.5 中所述。

異步可列舉物件提供GetAsyncEnumerator 方法的IAsyncEnumerable<T> 介面實作。 這個方法會傳回可用的異步列舉值物件。 列舉值物件會使用初始化可列舉物件時儲存的引數值和實例值來初始化,包括選擇性取消權杖,但除此之外,列舉值物件會如 §15.15.5 中所述。 非同步反覆運算器方法可以使用 (System.Runtime.CompilerServices.EnumeratorCancellationAttribute) 將一個參數標示為取消權杖。 實作應該提供結合取消權杖的機制,以便在取消權杖 (引 GetAsyncEnumerator 數或屬性屬性) System.Runtime.CompilerServices.EnumeratorCancellationAttribute要求取消時取消非同步反覆專案器。