15 個類別
15.1 一般
類別是數據結構,可能包含數據成員(常數和欄位)、函式成員(方法、屬性、事件、索引器、運算元、實例建構函式、完成項和靜態建構函式),以及巢狀類型。 類別類型支持繼承,衍生類別可以擴充和特製化基類的機制。
15.2 類別宣告
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 ';'?
;
除非類別宣告也提供 type_parameter_list,否則類別宣告不得提供 type_parameter_constraints_clause。
提供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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
同一個修飾詞在類別宣告中出現多次是編譯時期錯誤。
可以對巢狀類別使用new
修飾詞。 它指定該類別會以相同名稱隱藏繼承的成員,如§15.3.5中所述。 修飾詞 new
出現在非巢狀類別宣告中會造成編譯時錯誤。
public
、protected
、internal
和 private
修飾詞可控制類別的可存取性。 根據類別宣告所在的上下文,有些修飾詞可能不被允許(§7.5.2)。
當部分類型宣告(§15.2.7)包含可存取性規格(透過public
、protected
、internal
和private
修飾詞),該規格必須與所有包含可存取性規格的其他部分一致。 如果局部類型的各部分都未指定存取權限規格,則會使用適當的預設存取權限規格(§7.5.2)。
abstract
、sealed
和 static
修飾詞將在以下小節中討論。
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
也應該宣告為抽象。 類別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)。
靜態類別宣告受限於下列限制:
- 靜態類別不得包含
sealed
或abstract
修飾詞。 不過,由於靜態類別無法具現化或衍生自,因此其行為就像是密封和抽象一樣。 - 靜態類別不得包含class_base規格(~15.2.4),且無法明確指定基類或實作介面的清單。 靜態類別隱含繼承自 類型
object
。 - 靜態類別應只包含靜態成員(~15.3.8)。
注意:所有常數和巢狀類型都會分類為靜態成員。 結束註釋
- 靜態類別不可擁有以
protected
、private protected
或protected 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是在格式為namespace_or_type_name的
T.I
中T
。 - `namespace_or_type-name 是形式為
typeof(T)
的 typeof_expression (§12.8.18) 中的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 規格,該規格會定義 類別的直接基類,以及類別直接實作的介面 ({18)。
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[]>
。end 範例
類別宣告中指定的基類可以是建構類別類型 ({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.Array
、System.Delegate
、 System.Enum
System.ValueType
或 型別dynamic
。 此外,泛型類別宣告不得作為 System.Attribute
直接或間接基類({22.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
。end 範例
類別的基類是直接基類及其基類。 換句話說,基類集合是直接基類關係的傳遞閉包。
範例:在下列內容中:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}
的基類
D<int>
為C<int[]>
、B<IComparable<int[]>>
、A
和object
。end 範例
除了 類別 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
集合為IA
、IB
和IC
。範例結束
一般而言,每個元件都會提供在該元件上宣告之介面的實作;不過,這不是必要條件。 元件可以提供在不同元件上宣告之介面的實作。
範例:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
範例結束
類別宣告中指定的基底介面可以建構介面類型 ({8.4, {18.2) 。 基底介面本身不能是型別參數,不過它可以牽涉到範圍中的型別參數。
範例:下列程式代碼說明類別如何實作和擴充建構的類型:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
結束 範例
介面實作會在 \18.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
子句可以依任何順序列出。
get
如同屬性存取子中的 和 set
令牌,where
令牌不是關鍵詞。
子句中 where
提供的條件約束清單可以包含下列任何元件,順序如下:單一主要條件約束、一個或多個次要條件約束,以及建構函式條件約束 new()
。
主要條件約束可以是類別類型、參考類型約束class
、值類型約束struct
、非 null 約束notnull
或非受控型別約束unmanaged
。 類別類型和參考型別限制可以包含 nullable_type_annotation。
次要條件約束可以是interface_type或type_parameter,並且可以選擇性地接著nullable_type_annotation。 nullable_type_annotation* 的存在表示類型參數可以是對應至滿足約束條件之不可為 Null 參考型別的可為 Null 參考型別。
參考型別條件約束會指定用於型別參數的類型自變數應該是參考型別。 所有類別類型、介面類型、委派類型、數位類型和類型參數已知為參考類型(如下所述)都滿足此條件約束。
類別類型、參考型別限制和次要限制可以包含可為空的型別註解。 類型參數上存在或不存在此批注,表示類型自變數的可為 Null 預期:
- 如果條件約束不包含可為 Null 的類型批注,則類型自變數必須是不可為 Null 的參考型別。 如果類型自變數是可為 Null 的參考型別,編譯程式可能會發出警告。
- 如果約束條件包含可空性類型註解,則不可為 Null 的參考型別和可為 Null 的參考型別都符合約束條件。
型別引數的可空性不需要與型別參數的可空性相符。 如果類型參數的 Null 性不符合類型自變數的 Null 屬性,編譯程式可能會發出警告。
注意:若要指定類型自變數是可為 Null 的參考型別,請勿將可為 Null 的類型批注新增為條件約束(使用
T : class
或T : BaseClass
),但在整個泛型宣告中使用T?
來指出類型自變數的對應可為 Null 參考型別。 結尾註解
可空類型註解 ?
無法在不受限制的泛型參數上使用。
如果類型參數 T
的自變數是可為 Null 的參考型別 C?
,則實體 T?
會解釋為 C?
,而不是 C??
。
範例:下列範例顯示類型引數的可為空性如何影響其類型參數宣告的可為空性:
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
` 進行的名稱查詢(§12.8.4)成功,則會將其視為 `class_type
`。 否則會將其視為非 Null 約束。
實值型別條件約束指定用於類型參數的類型自變數應該是不可為 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 存在語法上的模棱兩可。 考量到相容性,如果名稱查詢§12.8.4的unmanaged
成功,則將其視為class_type
。 否則會將其視為未受管理的限制。
指標類型永遠不允許成為型別引數,而且即使是非受控類型,也無法滿足任何型別限制。
如果條件約束是類別類型、介面類型或類型參數,該類型會指定用於該類型參數之每個類型自變數都應該支援的最小「基底類型」。 每當使用構造型別或泛型方法時,就會在編譯期間針對型別參數的約束條件檢查型別引數。 提供的型別自變數應符合 \8.4.5 中所述的條件。
class_type條件約束應滿足下列規則:
- 此類型應該是類別類型。
- 類型不得為
sealed
。 - 類型不得為下列其中一種類型:
System.Array
或System.ValueType
。 - 類型不得為
object
。 - 指定型別參數的一個條件約束最多可以是類別類型。
指定為 interface_type 條件約束的類型應符合下列規則:
- 此類型應該是介面類型。
- 在特定的
where
子句中,類型不得被指定多次。
不論是哪一種情況,條件約束都可能牽涉到關聯型別或方法宣告的任何型別參數作為建構型別的一部分,而且可能涉及所宣告的類型。
任何指定為型別參數條件約束的類別或介面類型,至少都必須以宣告的泛型型別或方法一樣可存取 (~7.5.5)。
指定為 type_parameter 條件約束的類型應符合下列規則:
- 此類型應該是類型參數。
- 在給定的
where
子句中,類型不得被指定多次。
此外,類型參數的相依性圖表中不得有迴圈,其中相依性是所定義的可轉移關聯:
- 如果類型參數
T
被用作類型參數S
的約束,則S
S
依賴於T
。 - 如果類型參數
S
相依於類型參數T
,而T
相依於類型參數U
,那麼S
相依於U
。
根據此關聯,類型參數依賴自身(直接或間接)是編譯時錯誤。
任何條件約束都應該在相依型別參數之間保持一致。 如果類型參數 S
相依於類型參數 T
,則:
-
T
不應有實值型別條件約束。 否則,會有效密封,T
因此S
會強制為與T
相同的類型,而不需要兩個類型參數。 - 如果
S
具有實值型別條件約束,則T
不應該有 class_type 條件約束。 - 如果
S
具有 class_type 條件約束,且T
具有 class_type 條件約束,則應該有從A
到B
的識別轉換或隱含參考轉換,或從B
到A
的隱含參考轉換。 - 如果
S
也依賴於型別參數U
,並且U
具有class_type約束,且T
具有B
約束,那麼必須存在從A
到B
的識別轉換或隱性參考轉換,或者從B
到A
的隱性參考轉換。
擁有值型別約束的S
和擁有參考型別約束的T
是有效的。 實際上,此限制 T
類型 System.Object
、 System.ValueType
、 System.Enum
和 任何介面類型。
where
如果型別參數的 子句包含建構函式條件約束(其格式new()
為 ),您可以使用 new
運算符來建立型別的實例(^12.8.17.2)。 任何用於具有建構函式條件約束之類型參數的類型自變數都應該是實值型別、具有公用無參數建構函式的非抽象類,或是具有實值型別條件約束或建構函式條件約束的類型參數。
有type_parameter_constraints包含primary_constraint為struct
或unmanaged
且具有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
動態抹除Cₓ
是以下列方式建構的:
- 如果
C
是巢狀類型Outer.Inner
,則Cₓ
為巢狀類型Outerₓ.Innerₓ
。 - 如果
C
Cₓ
是具有型別自變數G<A¹, ..., Aⁿ>
的建構型A¹, ..., Aⁿ
別,則Cₓ
為建構型別G<A¹ₓ, ..., Aⁿₓ>
。 - 如果
C
是陣列型態E[]
,則Cₓ
為陣列型態Eₓ[]
。 - 如果
C
為動態,則Cₓ
為object
。 - 否則,
Cₓ
為C
。
類型參數的有效基類定義如下:
讓我們 R
成為一組類型,以便:
- 對於類型參數的每個限制
T
,R
都包含其實際的基類。 - 對於每個結構型別的約束
T
,R
都包含System.ValueType
。 - 對於列舉型別的每個條件約束
T
,R
都包含System.Enum
。 - 對於每個為委派類型的條件約束
T
,R
都包含其動態清除。 - 針對每個是陣列類型的約束
T
,R
包含System.Array
。 - 對於類別類型的每個限制
T
,R
都包含其動態刪除。
然後
- 如果
T
具有實值型別條件約束,則其有效基類為System.ValueType
。 - 否則,如果
R
是空的,則有效的基類為object
。 - 否則,
T
的有效基類是集合R
中最包含的類型(參見 §10.5.3)。 如果集合沒有包含的類型,則的有效基類T
為object
。 一致性規則可確保最包含的類型存在。
如果類型參數是方法類型參數,其條件約束繼承自基底方法,則有效的基類會在類型替代之後計算。
這些規則可確保有效的基類一律是 class_type。
型參數的有效接口集定義如下:
-
如果沒有
T
secondary_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
。end 範例
當部分泛型型別宣告包含條件約束時,條件約束應與包含條件約束的所有其他部分一致。 具體來說,包含條件約束的每個部分都應該有相同類型參數集的條件約束,而且對於每個類型參數,主要、次要和建構函式條件約束的集合應相等。 如果條件約束包含相同的成員,則兩組條件約束相等。 如果部分泛型類型沒有任何部分指定類型參數條件約束,則類型參數會被視為不受限制。
範例:
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
修飾詞是內容關鍵詞 (),而且在關鍵詞 class
、struct
和 interface
之前具有特殊意義。 (部分類型可能包含部分方法宣告 (15.6.9)。
每個部分型別宣告都應包含 partial
修飾詞,並且應在與其他部分相同的命名空間或包含型別中宣告。
partial
修飾詞表示類型宣告的其他部分可能存在於別處,但並非必定需要存在此類額外部分;僅包含partial
修飾詞的類型宣告也是有效的。 只有部分類型的一個宣告才有效,才能包含基類或實作的介面。 不過,基類或實作介面的所有宣告都應該相符,包括任何指定型別自變數的可為 Null 性。
部分類型的所有部分都應該一起編譯,以便元件可以在編譯階段合併。 部分類型特別不允許已經編譯的型別進行擴充。
巢狀類型可以使用 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; }
結束範例
部分類型宣告中類型或類型參數上指定屬性的處理方式,會在 §22.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_parameter和class_member_declaration由class_declaration立即引入新的成員到此宣告空間。 下列規則適用於 class_member_declarations:
實例建構函式、完成項和靜態建構函式的名稱應該與立即封入類別相同。 所有其他成員的名稱應與立即封入類別的名稱不同。
類別宣告type_parameter_list中型別參數的名稱應該與相同type_parameter_list中所有其他類型參數的名稱不同,而且與類別的名稱和類別的所有成員名稱不同。
型別的名稱應該與相同類別中宣告的所有非類型成員的名稱不同。 如果兩個或多個類型宣告共用相同的完整名稱,宣告應具有
partial
修飾詞 ({15.2.7),而且這些宣告會結合來定義單一類型。
注意:因為類型宣告的完整名稱會編碼類型參數的數目,所以只要兩個不同的類型參數數目不同,就可能會共用相同的名稱。 註釋結尾
常數、欄位、屬性或事件的名稱應該與相同類別中宣告的所有其他成員名稱不同。
方法的名稱應該與相同類別中宣告的所有其他非方法名稱不同。 此外,方法的簽章 ({7.6) 應該與相同類別中宣告之所有其他方法的簽章不同,而相同類別中宣告的兩個方法不得具有與 、
in
和out
完全ref
不同的簽章。實例建構函式的簽章應與相同類別中宣告的所有其他實例建構函式的簽章不同,而相同類別中宣告的兩個建構函式其簽章不能僅僅因
ref
與out
的不同而有所區別。索引器的簽章應該與相同類別中宣告之所有其他索引器的簽章不同。
運算子的簽章應該與相同類別中宣告之所有其他運算符的簽章不同。
類別的繼承成員 ({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
」,或int
。int[,][]
結束範例
在實例函式成員內,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>
具有一個公用的非繼承成員int
G(string s)
,通過將型別參數T
替換為型別自變數int
而獲得。D<int>
也具有類別宣告B
的繼承成員。 この繼承的成員首先是透過在基類規格B<T[]>
中將int
取代T
來判斷基類類型B<int[]>
以及D<int>
。 然後,作為B
的型別參數,int[]
會取代U
在public U F(long index)
中,從而生成繼承的成員public int[] F(long index)
。結束範例
15.3.5 新修飾詞
類別成員宣告可以宣告一個與繼承成員具有相同名稱或簽章的成員。 發生這種情況時,會說衍生類別成員會 隱藏 基類成員。 如需成員隱藏繼承成員時的精確規格,請參閱 •7.7.2.3 。
如果可存取,則繼承的成員M
會被視為可用,而且沒有其他繼承的可存取成員 N 已隱藏 M
。M
隱含隱藏繼承的成員不會被視為錯誤,但編譯程式應該發出警告,除非衍生類別成員的宣告包含 new
修飾詞,以明確指出衍生成員是要隱藏基底成員。 如果巢狀類型的一或多個部分宣告 ({15.2.7) 包含 new
修飾詞,如果巢狀類型隱藏可用的繼承成員,則不會發出任何警告。
new
如果修飾詞包含在不會隱藏可用繼承成員的宣告中,則會發出該效果的警告。
15.3.6 存取修飾詞
class_member_declaration可以具有任何一種允許的宣告可見度(第 7.5.2 節):public
、protected internal
、protected
、private protected
、internal
,或 private
。
protected internal
除了 和 private protected
組合之外,指定多個存取修飾詞是編譯時期錯誤。 當class_member_declaration不包含任何存取修飾詞時,會假定為private
。
15.3.7 組成類型
成員宣告中使用的類型稱為 該成員的組成類型 。 可能的組成類型是常數、字段、屬性、事件或索引器、方法或運算元的傳回型別,以及方法、索引器、運算符或實例建構函式的參數類型。 成員的組成類型至少可以和該成員本身一樣可存取(~7.5.5)。
15.3.8 靜態和實例成員
類別的成員是靜態成員或實例成員。
注意:一般而言,將靜態成員視為屬於類別和實例成員屬於物件(類別的實例)會很有用。 結尾註釋
當欄位、方法、屬性、事件、運算元或建構函式宣告包含 static
修飾詞時,它會宣告靜態成員。 此外,常數或類型宣告會隱含宣告靜態成員。 靜態成員具有下列特性:
- 當靜態成員
M
在成員訪問(§12.8.7)中被引用時,E.M
應表示具有成員M
的類型。 這是表示 實例的E
編譯時間錯誤。 - 非泛型類別中的靜態字段會確切識別一個儲存位置。 無論建立多少個非泛型類別的實例,靜態欄位始終只有一個副本。 不論封閉式建構型別的實例數目為何,每個相異的封閉式建構型別 (~8.4.3) 都有自己的靜態字段集。
- 靜態函式成員(方法、屬性、事件、運算符或建構函式)不會在特定實例上運作,而且在這類函式成員中參考這個是編譯時間錯誤。
當欄位、方法、屬性、事件、索引器、建構函式或完成項宣告不包含靜態修飾詞時,它會宣告實例成員。 (實例成員有時稱為非靜態成員。實例成員具有下列特性:
- 在形式 member_access(§12.8.7)中參考實例成員
M
時,E
應表示具有成員M
的類型實例。 這是 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_name (12.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 宣告的存取性
非巢狀類型可以宣告 public
或 internal
存取權限,且預設會宣告 internal
存取權限。 巢狀類型也可以有這些形式的宣告輔助功能,再加上一或多個額外的宣告輔助功能形式,視包含類型是否為類別或結構而定:
- 在類別中宣告的巢狀類型可以有任何允許的宣告性可存取性,且與其他類別成員一樣,預設為
private
宣告的可存取性。 - 在結構中宣告的巢狀類型可以有三種存取層級形式之一:
public
、internal
或private
,並且與其他結構成員一樣,預設為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
。end 範例
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
的建構函式,以便後續存取C
的實例成員。範例結束
15.3.9.6 對包含類型之私用和受保護成員的存取
巢狀類型可以存取其包含型別可存取的所有成員,包括那些具有 private
和 protected
宣告的存取性的包含型別成員。
範例:範例
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
會呼叫C
中定義的靜態方法F
,而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
透過Derived
的實例來呼叫Derived
的基類Base
中定義的受保護方法F
。結束範例
泛型類別中的 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)。
注意:這些名稱的保留有三個用途:
- 若要允許基礎實作使用一般標識碼做為方法名稱,以取得或設定 C# 語言功能的存取權。
- 若要允許其他語言使用一般標識碼作為取得或設定 C# 語言功能存取權的方法名稱進行互操作。
- 為了協助確保一個符合編譯程式接受的來源是由另一個編譯程式所接受,方法是讓所有 C# 實作中的保留成員名稱細節保持一致。
註釋結束
終結器的宣告(§15.13)也會導致保留簽名(§15.3.10.5)。
某些名稱會保留作為運算符方法名稱(§15.3.10.6)。
15.3.10.2 保留給屬性的成員名稱
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
end 範例
15.3.10.3 保留給事件的成員名稱
對於委派類型T
的事件E
(§15.8),保留以下簽章:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 保留給索引器的成員名稱
對於類型§15.9且參數清單為L
的索引器T
,保留以下簽章:
T get_Item(L);
void set_Item(L, T value);
即使索引器是只讀或僅可寫的,這兩個簽章仍然會被保留。
此外,成員名稱 Item
已保留。
15.3.10.5 保留給完成項的成員名稱
針對包含終結器(§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'
;
常數宣告可能包含一組屬性(§22)、修飾詞(§15.3.5),以及任何一種允許的可見性宣告類型(§15.3.6)。 屬性和修飾詞會應用於由 constant_declaration 宣告的所有成員。 即使常數被視為靜態成員, constant_declaration 不需要也不允許 static
修飾詞。 同一個修飾詞在常數宣告中出現多次是錯誤的。
constant_declaration的類型會指定宣告所引進的成員類型。 此類型後面接著constant_declarators (13.6.3),每個清單都會引進新的成員。 constant_declarator是由一個標識符所組成,該標識符會命名成員,後面接著一個“=
”標記,再接著是一個constant_expression (§12.23),提供成員的值。
常數宣告中指定的類型應該是sbyte
、byte
、short
、ushort
、int
、uint
、long
、ulong
、char
、float
、double
、decimal
、bool
、string
、enum_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.23中所述,constant_expression是可在編譯時期完整評估的表達式。 由於除了
string
以外,建立reference_type類型的非空值的唯一方法是套用new
運算子,而且因為new
運算子不允許在constant_expression中使用,所以除了string
以外,reference_type型常數的唯一可能值是null
。 註腳結束
當需要為常數值指定一個符號名稱時,但該值的類型在常數宣告中不被允許,或 constant_expression 無法在編譯時期計算出該值時,可以改用只讀欄位(第15.5.3節)。
注意:
const
和readonly
的版本語意存在不同(§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
,產生值10
、11
和12
。結尾範例
常數宣告可能相依於其他程式的常數,但這類相依性只能在單一方向進行。
範例:參考上述範例,如果
A
和B
在個別程式中被宣告,則可能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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
field_declaration 可能包含一組屬性(§22)、修飾詞(§15.3.5)、四個存取修飾詞的有效組合(§15.3.6),以及修飾詞(§15.5.2)。 此外,field_declaration可能包含readonly
修飾詞(§15.5.3)或volatile
修飾詞(§15.5.4),但不能同時包含兩者。 屬性和修飾詞會套用至field_declaration宣告的所有成員。 同一 個修飾詞在field_declaration中出現多次是錯誤的。
field_declaration的類型會指定宣告所引進的成員類型。 此類型後面接著一份variable_declarator的清單,其中每個宣告器都會引進新的成員。
變數宣告符包含一個識別碼,該識別碼命名該成員,選擇性地後面接著「=
」標記以及變數初始化器(§15.5.6),該初始化器提供該成員的初始值。
欄位的類型應該至少可以和字段本身一樣可存取(~7.5.5)。
域的值是在表達式中使用simple_name取得的(\12.8.4)、member_access(\12.8.7)或base_access(\12.8.15)。 使用賦值 (§12.21) 來修改非唯讀欄位的值。 非只讀欄位的值可以使用後置遞增和遞減運算符 (§12.8.16) 和前置遞增和遞減運算符 (§12.9.6) 來取得和修改。
宣告多個字段的欄位宣告相當於具有相同屬性、修飾詞和類型的單一字段的多個宣告。
範例:
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
修飾詞時,宣告所引進的欄位是 靜態欄位。 當沒有任何 static
修飾詞存在時,宣告所引進的欄位是 實例欄位。 靜態欄位和實例字段是 C# 支援的數種變數之兩種,有時它們稱為靜態變數和實例變數。
如 •15.3.8 中所述,類別的每個實例都包含 類別的完整實例字段集,而每個非泛型類別或封閉建構型別只有一組靜態字段,不論類別或封閉式建構型別的實例數目為何。
15.5.3 隻讀欄位
15.5.3.1 一般
當field_declaration包含readonly
修飾詞時,宣告所引進的欄位是唯讀欄位。 直接指派至只讀欄位只能當做該宣告的一部分,或在同一類別的實例建構函式或靜態建構函式中發生。 在這些情境中,可以多次指派值給唯讀欄位。具體來說,只有在以下情境中才允許直接指派至唯讀欄位:
- 在引入欄位的 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; } }
Black
、White
、Red
Green
和Blue
成員無法宣告為 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); } } }
Program1
和Program2
命名空間表示兩個個別編譯的程式。 由於Program1.Utils.X
被宣告為static readonly
欄位,因此Console.WriteLine
語句在編譯時期無法得知其輸出值,而是在運行時間才取得。 因此,如果的值X
已變更並Program1
重新編譯,即使未重新編譯,Console.WriteLine
語句也會輸出新的值Program2
。 不過,如果X
為常數,則在編譯Program2
時會取得X
的值,並且在重新編譯Program2
之前,不會受到Program1
中的變更影響。結束範例
15.5.4 揮發性欄位
當field_declaration包含volatile
修飾詞時,該宣告所引進的字段是揮發性字段。 對於非易失性欄位,重新排序指令的優化技術可能會在多線程程式中導致非預期且無法預測的結果,尤其是這些程式在存取欄位時,未使用諸如 lock_statement 所提供的同步處理(§13.13)。 這些優化可由編譯程式、運行時間系統或硬體來執行。 對於揮發性欄位,這類重新排序優化會受到限制:
- 易失性欄位的讀取稱為volatile 讀取。 揮發性讀取具有「取得語意」;也就是說,在指令序列中,它保證會先於任何後續的記憶體參考發生。
- 動態欄位的寫入稱為動態寫入。 揮發性寫入具有「釋放語意」;也就是說,在指令序列中,保證在寫入指令之前的任何記憶體參考之後發生。
這些限制確保所有的執行緒將會觀察到其他任何執行緒所執行的 volatile 寫入,並按照其執行的順序進行。 符合規範的實作不需要提供從所有執行緒觀察到的揮發性寫入的單一總排序。 揮發性欄位的類型應該是下列其中一項:
- 一個參考類型。
- 被確認為參考型別的type_parameter (§15.2.5)。
- 類型
byte
、sbyte
、short
、ushort
、int
、uint
、char
、float
、bool
、System.IntPtr
或System.UIntPtr
。 - 具有 enum_base 類型的 enum_type,其類型可以是
byte
、sbyte
、short
、ushort
、int
或uint
。
範例:範例
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
,那麼允許在對finished
的存放操作之後,存放到result
的可見性確保給主線程,並因此主線程可以從欄位result
讀取值 0。 宣告finished
為volatile
欄位可防止任何這類不一致。結束範例
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
因為
b
和i
都會自動初始化為預設值。結束範例
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
會被指派,而當實例欄位初始化運算式執行時,i
和s
會被指派。end 範例
在 §15.5.5 中描述的預設值初始化適用於所有欄位,包括那些具有變數初始化器的欄位。 因此,當類別初始化時,該類別中的所有靜態欄位都會先初始化為預設值,然後以文字順序執行靜態字段初始化表達式。 同樣地,建立類別的實例時,該實例中的所有實例欄位都會先初始化為預設值,然後以文字順序執行實例字段初始化表達式。 當在相同類型的多個局部類型宣告中有欄位宣告時,各部分的順序是未指定的。 不過,在每個部分內,字段初始化表達式會依序執行。
可以觀察到具有變數初始化器的靜態欄位處於其預設值的狀態。
範例:然而,由於樣式的考量,強烈不建議這樣做。 範例
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
表現此行為。 儘管
a
和b
都有循環定義,但程式仍然有效。 這會產生結果輸出a = 1, b = 2
因為靜態欄位
a
和b
會在執行初始化表示式之前初始化為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 一般
「方法」是實作物件或類別所能執行之計算或動作的成員。 方法是使用 method_declarations 宣告:
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'
| 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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
- 辨識 method_body 時,如果 null_conditional_invocation_expression 和 表達式 兩者的替代方案都適用,則應選擇前者。
注意:此處的替代方案重疊與優先順序完全是出於描述上的方便;文法規則可以更詳細地闡述以消除重疊。 ANTLR 和其他文法系統採用相同的便利性,因此 method_body 自動具有指定的語意。 結尾註釋
一個method_declaration可能包含一組屬性(§22)和其中一種允許的宣告的存取權限(§15.3.6)、new
(§15.3.5)、static
(§15.6.3)、virtual
(§15.6.4)、override
(§15.6.5)、sealed
(§15.6.6)、abstract
(§15.6.7)和extern
(§15.6.8)修飾詞。
如果下列所有條件都成立,宣告就有有效的修飾符組合:
- 宣告包含有效存取修飾詞的組合(§15.3.6)。
- 宣告不會多次包含相同的修飾詞。
- 宣告最多包含下列其中一個修飾詞:
static
、virtual
和override
。 - 宣告最多包含下列其中一個修飾詞:
new
和override
。 - 如果宣告包含
abstract
修飾詞,則宣告不包含下列任何修飾詞:static
、virtual
、sealed
或extern
。 - 如果宣告包含
private
修飾詞,則宣告不包含下列任何修飾詞:virtual
、override
或abstract
。 - 如果宣告包含
sealed
修飾詞,則宣告也會包含override
修飾詞。 - 如果宣告包含
partial
修飾詞,則不包含下列任何修飾詞:new
、public
、protected
internal
private
virtual
sealed
override
abstract
或 。extern
方法會根據它們是否傳回任何內容來進行分類:
- 如果
ref
存在,則方法會 傳回 by-ref 並傳 回變數參考,這是選擇性只讀的; - 否則,如果 return_type 為
void
,則該方法為 不傳回值 並且不會傳回任何值。 - 否則,該方法會以 傳回值方式 傳回並返回一個值。
某個傳回值或不返回值的方法宣告中的return_type會指定該方法所傳回結果的類型,如果有結果的話。 只有不會傳回值的方法可以包含 partial
修飾詞 (§15.6.9)。 如果宣告包含 async
修飾詞,則return_type應該是 void
,或者是方法傳回的是值,而且傳回類型是工作類型(§15.15.1)。
returns-by-ref 方法宣告的ref_return_type會指定 方法所傳回之variable_reference所參考的變數類型。
泛型方法是方法,其宣告包含 type_parameter_list。 這會指定 方法的類型參數。 選擇性 type_parameter_constraints_clause指定型別參數的條件約束。
明確介面成員實作的泛型 method_declaration 不得有任何 type_parameter_constraints_clause,這些約束將由介面方法的約束繼承。
同樣地,具有 override
修飾詞的方法宣告不應有任何 type_parameter_constraints_clause,而且方法類型參數的條件約束會從被覆寫的虛擬方法中繼承。
member_name指定方法的名稱。 除非該方法是明確的介面成員實作(§18.6.2),member_name 只是個 標識符。
針對明確的介面成員實作,member_name 由 interface_type 構成,後面跟著 “.
” 和 識別字。 在此情況下,宣告不得包含 (可能) extern
或 async
以外的修飾詞。
選擇性 parameter_list 會指定方法的參數(~15.6.2)。
return_type 或 ref_return_type,以及方法 parameter_list 中所參考的每個型別,必須至少和方法本身同樣具有可存取性(§7.5.5)。
一個回傳值或不回傳值的方法的 method_body 可以是分號、區塊主體或表達式主體。 區塊主體是由 區塊所組成,它會指定要在叫用 方法時執行的語句。 表達式主體包含 =>
,後面接著 null_conditional_invocation_expression 或 表達式,以及分號,並表示叫用 方法時要執行的單一表達式。
對於抽象和 extern 方法, method_body 只包含分號。 對於部分方法, method_body 可能包含分號、區塊主體或表達式主體。 對於所有其他方法, method_body 為區塊主體或表達式主體。
如果method_body包含分號,則宣告不得包含 async
修飾詞。
returns-by-ref 方法的ref_method_body為分號、區塊主體或表達式主體。 區塊主體是由 區塊所組成,它會指定要在叫用 方法時執行的語句。 表達式主體由 =>
、 ref
、 variable_reference和分號組成,表示在方法被呼叫時需要評估的單一 variable_reference。
對於抽象和 extern 方法, ref_method_body 只包含分號;對於所有其他方法, ref_method_body 為區塊主體或表達式主體。
名稱、類型參數數目,以及方法的參數清單會定義方法的簽章 ({7.6)。 具體來說,方法的簽章包含其名稱、其類型參數的數量,以及其參數的數量、parameter_mode_modifier(§15.6.2.1),及其參數的類型。 傳回型別不是方法簽章的一部分,也不是參數的名稱、類型參數的名稱或條件約束。 當參數類型參考方法的類型參數時,類型參數的序數位置(而非類型參數的名稱)會用於類型等價。
方法的名稱應該與相同類別中宣告的所有其他非方法名稱不同。 此外,方法的簽章應該與相同類別中宣告之所有其他方法的簽章不同,而在相同類別中宣告的兩個方法,其簽章不可僅因 in
、out
及 ref
而有所不同。
方法的 type_parameter 在整個 method_declaration 的作用範圍內可用,可用來在此範圍內的 return_type 或 ref_return_type、method_body 或 ref_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
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
參數清單包含一或多個逗號分隔參數,其中只有最後一個 參數可能是parameter_array。
fixed_parameter 由一組可選擇的屬性(§22),可選的in
、out
、ref
或this
修飾詞,類型,識別符以及可選的預設參數組成。 每個 fixed_parameter 都會宣告具有指定名稱之指定型別的參數。
this
修飾詞會將 方法指定為擴充方法,而且只能在非泛型非巢狀靜態類別中靜態方法的第一個參數上使用。 如果參數是 struct
型別或受限於 struct
型別的型別參數,則 this
修飾詞可能會與 ref
修飾詞或 in
修飾詞結合,但不能與 out
修飾詞結合。 擴充方法會在 •15.6.10 中進一步說明。 具有固定參數和預設引數稱為選擇性參數,而固定參數不含預設引數則是必要參數。 必要的參數不得出現在parameter_list的選擇性參數之後。
具有 ref
、 out
或 this
修飾詞的參數不能有 default_argument。 輸入參數可能有 default_argument。
default_argument中的運算式需是下列其中一項:
- 常數表達式
- 格式
new S()
的表達式,其中S
是實值型別 - 格式
default(S)
的表達式,其中S
是實值型別
表達式應可隱含地透過識別或可為 Null 的轉換,轉換為參數的類型。
如果在實作部分方法宣告(§15.6.9)、明確介面成員實作(§18.6.2)、單一參數索引器宣告(§15.9),或運算符宣告(§15.10.1)中出現選擇性參數,編譯程式應該發出警告,因為這些成員永遠無法以允許省略自變數的方式被叫用。
parameter_array包含一組選擇性屬性 (~22)、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_list
M
中,i
是必要的ref
參數,d
是必要的值參數,b
、s
、o
和t
是可選值參數,而且a
是參數陣列。end 範例
方法宣告會為參數和類型參數建立個別的宣告空間 ({7.3)。 名稱會透過類型參數清單和方法的參數清單,來導入此宣告空間。 如果有的話,方法的主體會被視為嵌套於此宣告空間中。 方法宣告空間的兩個成員具有相同名稱是錯誤的。
方法調用 (~12.8.10.2) 會建立方法參數和局部變數的特定複本,而調用的自變數清單會將值或變數參考指派給新建立的參數。 在方法的區塊內,參數可以在simple_name表達式中由其標識符參考(§12.8.4)。
下列型態的參數存在:
- 值參數 (~15.6.2.2)。
- 輸入參數 (~15.6.2.3.2)。
- 輸出參數 (~15.6.2.3.4)。
- 參考參數 (~15.6.2.3.3)。
- 參數陣列(~15.6.2.4)。
注意:如 {7.6 中所述,
in
、out
和ref
修飾詞是方法簽章的一部分,但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
) 運算符來變更。
當參數是傳址參數時,方法調用中的對應引數應包含對應的關鍵字in
、ref
或 out
,接著是與參數類型相同的變數引用(§9.5)。 不過,當參數是in
參數時,傳入的引數可以是一個表達式,而該表達式到對應參數類型的隱含轉換(§10.2)是存在的。
宣告為反覆運算器(§15.14)或異步函式(§15.15)的函式不允許使用參考參數。
在採用多個參考參數的方法中,多個名稱可以代表相同的儲存位置。
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
在
Main
中,Swap
的使用中,x
表示i
,而y
表示j
。 因此,調用的結果是交換i
和j
的值。end 範例
範例:在下列程式代碼中
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); } }
對於
a
和b
,在G
中調用F
會傳遞對s
的引用。 因此,針對該呼叫,名稱s
、a
與b
都參考相同的儲存位置,而三個指派都會修改實例欄位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
請注意,
dir
和name
變數可以在傳遞至SplitPath
之前取消指派,而且在呼叫之後會被視為絕對指派。範例結束
15.6.2.4 參數陣列
使用 params
修飾詞宣告的參數是參數陣列。 如果參數清單包含參數陣列,它應該是清單中的最後一個參數,而且應該為單一維度陣列類型。
範例:類型
string[]
和string[][]
可以作為參數陣列的類型使用,但類型string[,]
不能。 結束範例
注意:無法將
params
修飾詞與in
、out
或ref
修飾詞結合。 註釋結尾
參數陣語允許在方法調用的兩種方式之一中指定自變數:
- 為參數陣列指定的自變數可以是可隱含轉換成參數陣列類型的單一表達式(~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 參考的陣列。end 範例。
當參數陣列的類型為 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)。
在虛擬方法調用中, 執行該調用的實例運行時間類型 會決定要叫用的實際方法實作。 在非虛擬方法調用中, 實例的編譯時間類型 是判斷因素。 在精確的層面上,當具有編譯時間類型 C
和運行時間類型 R
的實例上以參數列表 A
調用名為 N
的方法時,其調用的處理方式如下:R
是 C
或者從 C
派生的類。
- 在系結時,多載解析會套用至
C
、N
和A
,以從C
所宣告和繼承的方法集合中選取特定方法M
。 這在第§12.8.10.2 中說明。 - 然後在運行時:
- 如果
M
非虛擬方法,M
則會叫用 。 - 否則,
M
是虛擬方法,並且會叫用相對於R
的M
最終衍生的實作。
- 如果
對於類別所宣告或繼承的每個虛擬方法,該類別都有該方法的 最衍生實作。 與類別R
相關的虛擬方法M
的最衍生實作是根據以下內容確定的:
- 如果
R
包含用以引入的虛擬宣告M
,那麼這是相較於R
中M
的最衍生實作。 - 否則,如果
R
包含對M
的覆寫,則這是相對於R
的M
最衍生實作。 - 否則,
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
),會決定要叫用的實際方法實作。end 範例
因為允許方法隱藏繼承的方法,所以類別可以包含數個具有相同簽章的虛擬方法。 這不會產生歧義問題,因為除了最衍生的方法之外,其他全部都被隱藏。
範例:在下列程式代碼中
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(); } }
C
和D
類別包含兩個具有相同簽章的虛擬方法:所A
引進的和 所C
引進的虛擬方法。 所C
引進的方法會隱藏繼承自A
的方法。 因此,D
的覆寫宣告會覆寫由C
引進的方法,而D
無法覆寫由A
引進的方法。 此範例會產生輸出:B.F B.F D.F D.F
請注意,您可以透過一個在該方法不被隱藏的較少衍生類型來存取
D
的實例,以叫用隱藏的虛擬方法。end 範例
15.6.5 覆寫方法
當實例方法宣告包含override
修飾詞時,方法會稱為覆寫方法。 覆寫方法會覆寫具有相同簽章的繼承虛擬方法。 雖然虛擬方法宣告 引進 了新的方法,但覆寫方法宣告 會藉由提供該方法的新實作來特製化 現有的繼承虛擬方法。
覆寫宣告所覆寫的方法稱為覆寫基底方法。針對在類別M
中宣告的覆寫方法C
,覆寫的基底方法取決於檢查每個基類的C
,開始於直接基類C
,並繼續針對每個後續的直接基類,一直到有一個在指定的基類類型中可存取的方法,其簽章在替換類型參數後與M
相同。 為了尋找覆寫的基底方法,如果一個方法是 public
,或是 protected
,或是 protected internal
,或者如果它是 internal
或 private protected
並且與 C
在同一個程式中宣告,則該方法被視為可存取。
除非覆寫宣告的下列所有條件都成立,否則將發生編譯階段錯誤:
- 覆寫的基底方法可以如上所述找到。
- 只有一個這類覆寫的基底方法。 只有在基類類型是建構型別時,此限制才會生效,其中型別自變數的替代會讓兩個方法的簽章相同。
- 被覆寫的基底方法是一個虛擬方法、抽象方法或覆寫方法。 換句話說,覆寫的基底方法不可以是靜態或非虛擬。
- 覆寫的基底方法不是密封方法。
- 覆寫基底方法的傳回型別與 override 方法之間有識別轉換。
- 覆寫宣告與被覆寫的基底方法具有相同的宣告可見性。 換句話說,覆寫宣告無法變更虛擬方法的存取範圍。 不過,如果覆寫的基底方法是受保護的內部,並且它是在不同於包含覆寫宣告之組件的元件中宣告,則覆寫宣告的宣告存取性應為受保護。
- 覆寫宣告未指定任何 type_parameter_constraints_clause。 相反地,限制會繼承自被覆蓋的基礎方法。 覆寫方法中類型參數的條件約束可能會由繼承條件約束中的類型自變數取代。 這可能會導致明確指定時無效的條件約束,例如實值型別或密封型別。
範例:下列示範覆寫規則如何適用於泛型類別:
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> }
範例結束
覆寫宣告可以使用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()
,則它會遞歸地調用在B
中宣告的PrintFields
方法,而不是在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
修飾詞,因此無法覆寫A
中的F
方法。 而是F
中的B
方法會隱藏A
中的方法,且會回報警告,因為在宣告中未包含新的修飾詞。end 範例
範例:在下列程式代碼中
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
方法。 由於新的F
在B
中具有私用存取權,因此其範圍只包含B
類別主體,而且不會延伸至C
。 因此,允許在C
中宣告F
來覆寫從A
繼承的F
。end 範例
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
提供兩個覆寫方法:一個方法具有sealed
修飾詞,而另一個方法則沒有。B
的修飾詞使用sealed
可防止C
進一步覆寫F
。end 範例
15.6.7 抽象方法
當實例方法宣告包含 abstract
修飾詞時,該方法會稱為 抽象方法。 雖然抽象方法也是隱含的虛擬方法,但它不能有 修飾詞 virtual
。
抽象方法宣告引進了新的虛擬方法,但不提供該方法的實作。 非抽象衍生類別需要藉由覆寫該方法來提供自己的實作。 因為抽象方法沒有提供實際實作,因此抽象方法的方法主體只會包含分號。
抽象方法宣告只有在抽象類中才允許 ({15.2.2.2.2)。
範例:在下列程式代碼中
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
。Ellipse
和Box
類別是具體的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"); }
類別會宣告虛擬方法,類別
A
B
會使用抽象方法覆寫此方法,而 類別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
關鍵詞之前時,它才會被識別為內容關鍵詞(§6.4.4)。 局部方法無法明確地實作介面方法。
部分方法宣告有兩種:如果方法宣告的主體是分號,則宣告會稱為 定義部分方法宣告。 如果區塊內容不是僅有一個分號,那麼該宣告被稱為實作部分方法宣告。 在類型宣告的各個部分,只能有一個定義具有指定簽章的部分方法宣告,而且最多只能有一個使用指定簽章實作部分方法宣告。 如果已指定實作部分方法宣告,則對應的定義部分方法宣告應存在,而且宣告應符合下列指定:
- 宣告應具有相同修飾詞(雖然不一定以相同順序)、方法名稱、類型參數數目和參數數目。
- 宣告中的對應參數應具有相同修飾詞(雖然不一定以相同順序)和相同的類型,或識別可轉換型別(類型參數名稱中的模數差異)。
- 宣告中的對應型別參數應具有相同的條件約束(類型參數名稱中的模數差異)。
實作部分方法宣告可以出現在與對應定義部分方法宣告相同的部分。
只有定義部分方法會參與多載解析。 因此,不論是否指定實作宣告,調用表達式都可以解析為部分方法的調用。 由於部分方法一律會傳 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; } }
end 範例
擴充方法是一般靜態方法。 此外,如果其所屬的靜態類別在作用域內,可以使用實例方法呼叫語法(§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.15.1)。 否則,非異步方法的有效傳回型別是其傳回型別,而具有傳回型別之異步方法的有效傳回型 «TaskType»<T>
別(15.15.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; }
傳回
F
值方法會導致編譯時間錯誤,因為控制可以流離方法主體的結尾。G
和H
方法是正確的,因為所有可能的執行路徑都會在指定傳回值的 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'
| 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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
有兩種類型的 property_declaration:
- 第一個宣告非 ref 值屬性。 其值的類型是 type。 這類屬性可以是可讀取和/或可寫入的。
- 第二個宣告具 ref 類型值的屬性。 其值是variable_reference(§9.5),可能是
readonly
一個類型為type的變數。 這種屬性只能讀取。
屬性宣告可能包括一組屬性(第22條),以及任何一種允許的宣告存取權限(第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條)、abstract
(第15.6.7條,第15.7.6條),以及extern
(第15.6.8條)的修飾符。
屬性宣告與方法宣告(§15.6)在有效修飾詞組合的規則上相同。
member_name(§15.6.1)指定屬性的名稱。 除非 屬性是明確的介面成員實作, 否則member_name 只是 標識符。 針對顯式的介面成員實作(§18.6.2),member_name包含一個interface_type,後面接著“.
”和一個識別符號。
屬性 的類型 至少可以和屬性本身一樣可存取(~7.5.5)。
property_body可能由語句主體部分或表達式主體部分組成。 在語句主體中,accessor_declarations應以“{
”和“}
”標記括住,以宣告屬性的存取子(§15.7.3)。 存取子會指定與讀取和寫入 屬性相關聯的可執行語句。
在property_body 中,由=>
後接表達式和分號組成的表達式主體與語句主體{ get { return 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 的值,否則無法將屬性作為
in
、out
或ref
引數傳遞,因為它必須返回變數引用(§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'
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
accessor_declarations包含get_accessor_declaration、set_accessor_declaration或兩者。 每個存取子宣告都包含選擇性屬性、選擇性 accessor_modifier、標記 get
或 set
,後面接著 accessor_body。
對於以 ref 為值的屬性,ref_get_accessor_declaration 包含可選的屬性、可選的 accessor_modifier、標記 get
,後面接著 ref_accessor_body。
accessor_modifier的使用受下列限制所控管:
- 在介面或明確介面成員實作中,不得使用accessor_modifier。
- 對於沒有
override
修飾詞的屬性或索引器,只有在屬性或索引器同時具有 get 和 set 存取子時,才允許accessor_modifier,且只能應用在其中一個存取子上。 - 對於包含
override
修飾詞的屬性或索引器,存取子應符合被覆寫存取子的 accessor_modifier(如果有的話)。 -
accessor_modifier 應宣告的存取權限必須比屬性或索引器本身所宣告的存取權限更嚴格。 精確:
- 如果屬性或索引器的宣告存取層級是
public
,那麼由accessor_modifier所宣告的存取層級可以是private protected
、protected internal
、internal
、protected
或private
。 - 如果屬性或索引器具有
protected internal
的宣告可存取性,則由 accessor_modifier 所宣告的可存取性可以是private protected
、protected private
、internal
、protected
或private
。 - 如果屬性或索引器具有
internal
或protected
的宣告存取範圍,則accessor_modifier宣告的存取性應該是private protected
或private
。 - 如果屬性或索引器的宣告可見性為
private protected
,則由 accessor_modifier 宣告的可見性應該是private
。 - 如果屬性或索引器的宣告存取權限是
private
,則不能使用 任何的accessor_modifier。
- 如果屬性或索引器的宣告存取層級是
針對 abstract
和 extern
非 ref 值屬性,任何指定的存取子的 accessor_body 僅是分號。 非抽象且非 extern 的屬性,但不是索引器,可能會把所有指定存取子的 accessor_body 設為分號,此時,它是自動實作的屬性(§15.7.4)。 自動實作的屬性至少應該有 get 存取子。 對於任何其他非抽象、非外部屬性的存取子,accessor_body為:
- 區塊,指定要在叫用對應存取子時執行的語句;或
- 表達式本體,由
=>
組成,後面接著 表達式 和分號,表示在叫用對應存取子時要執行的單一表達式。
針對 abstract
和 extern
參考型別屬性,ref_accessor_body 僅為分號。 對於任何其他非抽象、非 extern 屬性的存取子,ref_accessor_body 可能是:
- 區塊,指定要在叫用 get 存取子時執行的語句;或
- 表達式主體的組成部分,包括
=>
,接著是ref
、variable_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.21),或作為++
或–-
的運算元時(§12.8.16,§12.9.6),會使用提供新值的參數來調用 set 存取器(§12.21.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 存取子會傳回儲存在私有caption
欄位中的string
。 set 存取子會檢查新值是否與目前的值不同,如果是,則會儲存新的值並重新繪出控件。 屬性通常會遵循上述模式:get 存取子只會傳回儲存在private
欄位中的值,而 set 存取子會修改該private
欄位,然後執行更新物件狀態所需的任何其他動作。 給定上述類別,以下是Caption
屬性的使用範例:Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
在這裡,會藉由將值指派給 屬性來叫用 set 存取子,而 get 存取子則是藉由在表達式中參考 屬性來叫用。
end 範例
屬性的 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 {...} } }
B
中的P
屬性會對於讀取和寫入,隱藏A
中的P
屬性。 因此,在語句中B b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
指定給
b.P
的賦值會導致報告編譯時錯誤,因為B
中的P
只讀屬性隱藏了A
中的P
只寫屬性。 請注意,不過,可以使用型別轉換來存取隱藏的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
欄位x
和y
來儲存其位置。 位置會公開為X
和Y
屬性,並且作為一個Point
類型的Location
屬性。 如果在未來的版本中,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; }
若
x
和y
是public 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
包含三個屬性、In
Out
和Error
,分別代表標準輸入、輸出和錯誤裝置。 藉由將這些成員公開為屬性,類別Console
可能會延遲其初始化,直到實際使用為止。 例如,第一次參考Out
屬性時,如 中所示Console.Out.WriteLine("hello, world");
建立輸出裝置的底層
TextWriter
。 不過,如果應用程式沒有參考In
和Error
屬性,則不會為這些裝置建立任何物件。範例結束
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; } } }
end 範例
範例:在下列情况中
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.21.2),則 set 存取子應存在且可供存取。
- 如果使用方式是作為複合指派的目標(§12.21.4),或作為
++
或--
運算子的目標(§12.8.16, §12.9.6),則必須存在且可存取 get 存取子和 set 存取子。
範例:在下列範例中,屬性
A.Text
會隱藏B.Text
屬性,即使在只呼叫 set 存取子的內容中也一樣。 相反地,類別M
無法存取屬性B.Count
,因此會改用可存取的屬性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 虛擬、密封、覆寫和抽象存取子
注意:這個子句同時適用於屬性(\15.7)和索引器(\15.9)。 子句是依照屬性撰寫的,當要將索引器代入屬性時,請查閱§15.9.2中提供的屬性與索引器之間的差異清單。 結束註釋
虛擬屬性宣告會指定屬性的存取子是虛擬的。 修飾 virtual
會套用至屬性的所有非私有存取子。 當虛擬屬性的存取子具有private
accessor_modifier時,私用存取子會隱含地不是虛擬。
抽象屬性宣告會指定屬性的存取子是虛擬的,但不會提供存取子的實際實作。 非抽象的衍生類別需要透過覆寫屬性來為存取子提供自己的實作。 因為抽象屬性宣告的存取子沒有提供實際實作,因此其 accessor_body 只包含分號。 抽象屬性不得有private
存取器。
包含 abstract
和 override
修飾詞的屬性宣告會指定屬性為抽象屬性,並覆寫基底屬性。 這類屬性的存取子也是抽象的。
抽象屬性宣告只有在抽象類中才允許 ({15.2.2.2.2)。 繼承的虛擬屬性的存取子可以在衍生類別中,透過包含指定override
指示詞的屬性宣告來覆寫。 這稱為 覆寫屬性宣告。 覆寫屬性宣告不會宣告新的屬性。 相反地,它只會專門化現有虛擬屬性的存取器的實作。
覆寫宣告和被覆寫的基底屬性必須具有相同的宣告可見性。 換句話說,覆寫宣告不得變更基底屬性的存取範圍。 不過,如果覆寫的基底屬性在內部受到保護,而且它宣告在與包含覆寫宣告的元件不同的元件中,則覆寫宣告的宣告存取範圍應受到保護。 如果繼承的屬性只有單一存取子(亦即,如果繼承的屬性是唯讀或唯寫的),則覆寫屬性應該只包含該存取子。 如果繼承的屬性包含這兩個存取子(亦即,如果繼承的屬性為讀寫),則覆寫屬性可以包含單一存取子或兩個存取子。 在覆寫和繼承的屬性類型之間應有識別轉換。
覆寫屬性宣告可以包含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; } }
在這裡,
X
、Y
和Z
的宣告是覆寫屬性宣告。 每個屬性宣告的存取修飾詞、類型和名稱都完全匹配對應繼承屬性的內容。X
的 get 存取子和Y
的 set 存取子使用 base 關鍵詞來存取繼承的存取子。 宣告Z
會覆蓋這兩個抽象存取子,因此,在B
中沒有任何突出的abstract
函式成員,並且允許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'
| 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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
事件宣告可能包含一組屬性(§22)以及任何一種允許的宣告存取權限(§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.6)、abstract
(§15.6.7、§15.8.5)和extern
(§15.6.8)修飾詞。
事件宣告與方法宣告(§15.6)的有效修飾詞組合遵循相同的規則。
事件宣告的類型應該是delegate_type(§8.2.8),而且delegate_type的存取性至少應該和事件本身一樣高(§7.5.5)。
事件宣告可以包含 event_accessor_declaration。 不過,如果是非 extern、非抽象事件,編譯器會自動提供它們(§15.8.2);而針對 extern
事件,存取子由外部提供。
省略event_accessor_declaration的事件宣告定義了一個或多個事件,即每個variable_declarator對應一個事件。 所有的屬性和修飾詞皆適用於由某個 event_declaration 所宣告的所有成員。
這是一個編譯時期錯誤,因為event_declaration同時包含abstract
和event_accessor_declaration。
當事件宣告包含 extern
修飾詞時,事件會稱為 外部事件。 因為外部事件宣告沒有提供實際的實作,如果包含extern
修飾詞和event_accessor_declaration,這是一個錯誤。
具有 abstract
或 external
修飾詞的事件宣告中的 variable_declarator 包含 variable_initializer 時,這是編譯時期錯誤。
事件可以作為+=
和-=
運算子的左操作數。 這些運算符分別用來附加事件處理程式,或從事件中移除事件處理程式,以及事件存取修飾詞控制允許這類作業的內容。
由宣告該事件之型別以外的程式代碼所允許的唯一作業是 +=
和 -=
。 因此,雖然這類程式代碼可以新增和移除事件的處理程式,但它無法直接取得或修改事件處理程式的基礎清單。
在 x += y
或 x –= y
的操作中,當 x
是一個事件時,這個操作的結果具有 void
類型(§12.21.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
事件。end 範例
15.8.2 類似欄位的事件
在包含事件宣告的類別或結構程序文字中,可以使用某些事件,例如字段。 若要以此方式使用,事件不得為抽象或外部,不得明確含有 event_accessor_declaration。 這類事件可用於任何允許欄位的上下文。 欄位包含委派 (§20),此委派是指已新增至事件的事件處理程序清單。 如果沒有新增事件處理程式,欄位會包含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
欄位使用。 如範例所示,欄位可以檢查、修改及用於委派呼叫表達式。 類別Button
中的OnClick
方法「引發」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_declaration 和 remove_accessor_declaration 所組成。 每個存取子宣告由標記「add」或「remove」組成,後面接著一個區塊。 與add_accessor_declaration相關聯的區塊會指定要在加入事件處理程式時執行的語句,而與 remove_accessor_declaration相關聯的區塊會指定要在移除事件處理程式時執行的語句。
每個 add_accessor_declaration 和 remove_accessor_declaration 都對應到一個有單一值參數(事件類型)和 void
傳回型別的方法。 事件存取子的隱含參數名為 value
。 當事件指派中使用事件時,會使用適當的事件存取子。 具體來說,如果指派運算符是 +=
,則會使用 add 存取子,如果指派運算符是 –=
,則會使用 remove 存取子。 在任何一種情況下,賦值運算子的右操作數會作為事件存取子的參數使用。 add_accessor_declaration或remove_accessor_declaration的區塊應符合void
方法在第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。
包含 abstract
和 override
修飾詞的事件宣告會指定事件是抽象的,並覆寫基底事件。 這類事件的存取器也是抽象的。
抽象事件宣告只有在抽象類中才允許 ({15.2.2.2.2)。
可以藉由包含指定 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'
| 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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
有兩種 indexer_declaration:
- 第一個宣告非引用值索引器。 其值具有類型 型別。 這種索引器可以是可讀取和/或可寫入的。
- 第二個宣告 ref 值索引器。 其值是一個variable_reference(第9.5節),可能是
readonly
某個type類型的變數。 這種索引器只能讀取。
indexer_declaration可能包含一組屬性(§22)和任何一種允許的宣告存取性的種類(§15.3.6),new
(§15.3.5)、virtual
(§15.6.4)、override
(§15.6.5)、sealed
(§15.6.6)、abstract
(§15.6.7)和extern
(§15.6.8)修飾詞。
索引器宣告與方法宣告({15.6)在有效的修飾詞組合上受限於相同的規則,但有一個例外狀況是 static
索引器宣告不允許修飾詞。
索引器宣告的類型會指定宣告所引進之索引器的項目類型。
注意:由於索引器被設計用於類似陣列元素的上下文,因此針對陣列定義的名詞元素類型也會與索引器搭配使用。 註釋結束
除非索引器是顯式的介面成員實作,否則 類型 後接著關鍵字 this
。 對於明確的介面成員實作,類型 後面接著 介面類型、“.
”和關鍵字 this
。 不同於其他成員,索引器沒有使用者定義的名稱。
parameter_list會指定索引器的參數。 索引器的參數清單會對應至方法 (~15.6.2),但至少應指定一個參數,不允許 this
ref
、 和 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 值並且因此傳回參考(§9.7),否則不可能將索引器元素作為
in
、out
或ref
自變數傳遞。 註解
索引器的參數列表 定義了索引器的簽名(§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.3)。
- 屬性可以是靜態成員,而索引器一律是實例成員。
- 屬性的 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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
注意:前綴邏輯否定(§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 只包含分號。 對於所有其他運算符, operator_body 為區塊主體或表達式主體。
下列規則適用於所有運算符宣告:
- 運算符宣告應同時包含
public
和static
修飾詞。 - 運算子的 parameter(s) 不得有 以外的
in
修飾詞。 - 運算符的簽章({15.10.2、 \15.10.3、 \15.10.4)與相同類別中宣告之所有其他運算符的簽章不同。
- 運算符宣告中參考的所有型別,至少可以和運算符本身一樣可存取({7.5.5)。
- 同一個修飾詞在運算符宣告中出現多次是錯誤的。
每個運算符類別都會施加額外的限制,如下列子集所述。
和其他成員一樣,在基類中宣告的運算符是由衍生類別繼承。 因為運算符宣告一律需要宣告運算子參與運算符簽章的類別或結構,所以在衍生類別中宣告的運算符無法隱藏基類中宣告的運算符。 因此,在運算符宣告中,new
修飾詞永遠不需要,因此也不允許使用。
如需一元運算符和二元運算子的其他資訊,請參閱 <12.4>。
如需轉換運算子的其他資訊,請參閱 §10.5。
15.10.2 一元運算符
下列規則適用於一元運算符宣告,其中 T
表示包含運算符宣告的類別或結構實例類型:
- 一元
+
、-
、!
(僅限邏輯否定)或~
運算符應採用類型T
或T?
的單一參數,而且可以傳回任何類型。 - 一元
++
或--
運算符應採用類型T
為 或T?
的單一參數,並傳回衍生自它的相同類型或型別。 - 一元
true
或false
運算子應該接受類型T
或T?
的單一參數,並回傳型別bool
。
一元運算子的簽章包含運算元 Token (+
、-
!
~
++
、、--
、 true
或 false
) 和單一參數的類型。 傳回型別不是一元運算符簽章的一部分,也不是參數的名稱。
一元運算子 true
和 false
需要成對宣告。 如果類別宣告其中一個運算符而不宣告另一個運算符,就會發生編譯時期錯誤。
true
和 false
運算子會在第 12.24 節中進一步說明。
範例:下列範例顯示整數向量類別之 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.6)。 不同於C++,這個方法不應該直接修改其操作數的值,因為這會違反後置遞增運算符的標準語意(^12.8.16)。
範例結束
15.10.3 二元運算符
下列規則適用於二元運算符宣告,其中 T
表示包含運算符宣告的類別或結構實例類型:
- 二進位非移位運算符應採用兩個參數,其中至少一個參數應具有 類型
T
或T?
,而且可以傳回任何類型。 - 二進位
<<
或>>
運算符(•12.11)應採用兩個參數,其中第一個參數的類型T
或T?
,第二個參數的類型int
或int?
,而且可以傳回任何類型。
二元運算子的簽章包含運算子標記 (+
、-
*
/
%
&
|
^
、<<
>>
==
!=
>
<
>=
或 <=
兩個參數的類型。 傳回型別和參數的名稱不是二元運算符簽章的一部分。
某些二進位運算子需要配對宣告。 對於配對之任一運算符的每個宣告,應該有配對之其他運算符的相符宣告。 如果識別轉換存在於其傳回型別與其對應的參數類型之間,則兩個運算符宣告相符。 下列運算子需要配對宣告:
- 運算子
==
和運算子!=
- 運算子
>
和運算子<
- 運算子
>=
和運算子<=
15.10.4 轉換運算元
轉換運算符宣告引進 使用者定義轉換(§10.5),這增強了預先定義的隱含和明確轉換。
包含 implicit
關鍵詞的轉換運算符宣告引入使用者定義的隱式轉換。 隱含轉換可能發生在各種情況下,包括函數成員呼叫、類型轉換和賦值。 這會在§10.2中進一步說明。
包含 explicit
關鍵詞的轉換運算符宣告引入使用者定義的顯式轉換。 明確轉換可能發生在轉換表達式中,並在 •10.3 中進一步說明。
轉換運算子會從轉換運算子的參數類型所表示的來源類型轉換成目標類型,以轉換運算符的傳回型別表示。
對於給定的來源類型 S
和目標類型 T
,如果 S
或 T
是可為 Null 的值類型,則讓 S₀
和 T₀
參考其基礎類型;否則,S₀
和 T₀
分別等於 S
和 T
。 只有在下列所有條件都成立時,才允許類別或結構宣告從來源類型 S
轉換成目標類型 T
。
S₀
和T₀
是不同的類型。S₀
或T₀
是包含運算符宣告的類別或結構實例類型。S₀
和T₀
都不是interface_type。排除使用者定義的轉換,從
S
到T
或從T
到S
的轉換不存在。
基於這些規則的目的,與或 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 }
允許前兩個運算符宣告,因為
T
和int
string
分別被視為沒有關聯性的唯一型別。 不過,第三個運算符是錯誤,因為C<T>
是的D<T>
基類。end 範例
根據第二個規則,轉換運算子應該將運算元轉換為或從運算元宣告的類別或結構類型。
範例:類別或結構類型
C
可以定義從C
到int
和從int
到C
的轉換,但不能從int
轉換成bool
。 結束範例
無法直接重新定義預先定義的轉換。 因此,不允許轉換運算符從object
轉換為其他類型或從其他類型轉換為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
存在,則會忽略從S
到T
的所有使用者定義轉換(隱含或明確)。 - 如果預先定義的明確轉換 (~10.3) 從類型
S
到類型T
存在,則會忽略從S
到T
的任何使用者定義明確轉換。 此外:-
S
如果 或T
是介面類型,則會忽略從S
到T
的使用者定義隱含轉換。 - 否則,仍會考慮從
S
到T
的使用者定義隱含轉換。
-
對於除 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
確實實作了指定的interface_type時,這樣的轉換才會成功。
轉換運算子的簽章包含來源類型和目標類型。 (這是傳回類型參與簽章的唯一成員形式。轉換運算子的隱含或明確分類不是運算子簽章的一部分。 因此,類別或結構無法同時宣告具有相同來源和目標類型的隱含和明確轉換運算元。
注意:一般而言,使用者定義隱含轉換的設計應該永遠不會擲回例外狀況,且永遠不會遺失資訊。 如果使用者定義轉換可能會引發例外狀況(例如,因為來源自變數超出範圍)或資訊遺失(例如捨棄高階位),則該轉換應該定義為明確的轉換。 尾註
範例:在下列程式代碼中
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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
constructor_declaration可能包含一組屬性(§22)、任何一種允許的宣告的存取性(§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 實例變數初始化表達式
當實例建構函式沒有建構函式初始化表達式,或是具有形式為 base(...)
的建構函式初始化表達式時,該建構函式會隱含地執行在其類別中宣告之實例欄位的variable_initializer所指定的初始化。 這相當於在進入建構函式時立即執行,並在直接基類建構函式隱式調用之前的指派序列。 變數初始化表達式會以出現在類別宣告 ({15.5.6) 中的文字順序執行。
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; } }
包含數個變數初始化,並且也包含這兩種形式的建構函式初始化(
base
和this
)。 此範例會對應至以下所示的程式代碼,其中每個批注都表示自動插入的語句(自動插入建構函式調用所使用的語法無效,而只是用來說明機制)。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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
static_constructor_declaration 可能包含一組屬性(§22)和一個修飾詞(§15.6.8)。
static_constructor_declaration的標識碼應命名宣告靜態建構函式的類別。 如果指定任何其他名稱,就會發生編譯時期錯誤。
當靜態建構函式宣告包含 extern
修飾詞時,靜態建構函式會稱為 外部靜態建構函式。 由於外部靜態建構函式宣告沒有提供實際的實作,因此其 static_constructor_body 是由分號所組成。 針對所有其他靜態建構函式宣告, static_constructor_body 是由任一項所組成
- 區塊,指定要執行以初始化 類別的語句;或
- 表達式主體由
=>
和 表達式 組成,並以分號作結,表示要執行的單一表達式以初始化類別。
static_constructor_body是區塊或表達式主體,對應於具有void
傳回型別的靜態方法的method_body(§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
的初始化器會執行A
的static
建構函式,因為參考了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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
finalizer_declaration 可能包含一組 屬性 (§22)。
終結器宣告器的標識符應與終結器宣告所在的類別同名。 如果指定任何其他名稱,就會發生編譯時期錯誤。
當完成項宣告包含extern
修飾詞時,完成項會稱為外部完成項。 因為外部完成項宣告沒有提供實際的實作,因此其 finalizer_body 是由分號所組成。 對於所有其他完成項, finalizer_body 包含任一項
- 程式區塊,指定要執行的語句,以對類別實例進行最終處理。
- 或者是表達式體,它由
=>
後跟 表達式 和分號組成,表示需執行的單一表達式,以完成類別實例的建立。
終結器不會被繼承。 因此,類別除了可以在該類別中宣告的終結器以外,沒有其他終結器。
注意:因為完成項不需要參數,所以不能多載,因此類別最多可以有一個完成項。 結尾註解
終結器會自動調用,且無法被顯式調用。 當任何程式代碼都無法使用該實例時,實例就有資格進行最終處理。 實例的終結器執行可能會在該實例符合終結條件後的任何時間發生(§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
因為繼承鏈中的終結器會依序被呼叫,從最衍生到最不衍生。
範例結束
完成項是藉由在System.Object
上覆寫Finalize
虛擬方法來實作。 不允許 C# 程式覆寫此方法,或直接呼叫此方法及其覆寫版本。
範例:例如,程式
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
包含兩個錯誤。
範例結束
編譯器應該表現得如同這個方法及其覆寫完全不存在一樣。
範例:因此,此程式:
class A { void Finalize() {} // Permitted }
有效且顯示的方法會隱藏
System.Object
的Finalize
方法。結束範例
如需了解從終結器拋出例外狀況時的行為,請參閱 §21.4。
15.14 反覆運算器
15.14.1 一般
使用反覆運算器區塊(第 13.3 節)實作的函式成員(§12.6)稱為反覆運算器。
只要對應函式成員的傳回型別是其中一個枚舉器介面(~15.14.2)或其中一個可列舉的介面(~15.14.3),反覆運算器區塊就可以當做函式成員的主體使用。 它可能會以method_body、operator_body或accessor_body發生,而事件、實例建構函式、靜態建構函式和完成項不得實作為反覆運算器。
使用迭代器區塊實作函式成員時,如果函式成員的參數列指定任何 in
、out
、ref
參數,或指定型別為 ref struct
的參數,這會導致編譯時期錯誤。
15.14.2 列舉器介面
枚舉器界面指的是非泛型界面System.Collections.IEnumerator
以及所有泛型界面System.Collections.Generic.IEnumerator<T>
的具現化。 為了簡潔起見,在此子命令和其同層級中,這些介面會分別參考為 IEnumerator
和 IEnumerator<T>
。
15.14.3 可列舉介面
可列舉的介面是非泛型介面System.Collections.IEnumerable
和泛型介面System.Collections.Generic.IEnumerable<T>
的所有具現化。 為了簡潔起見,在此子命令和其同層級中,這些介面會分別參考為 IEnumerable
和 IEnumerable<T>
。
15.14.4 產出類型
反覆運算器會產生一連串的值,這些值全都相同。 此類型稱為反覆運算器的產值類型。
- 傳回
IEnumerator
或IEnumerable
之反覆運算器的 yield 類型為object
。 - 傳回
IEnumerator<T>
或IEnumerable<T>
之反覆運算器的 yield 類型為T
。
15.14.5 列舉器物件
15.14.5.1 一般
當傳回列舉值介面類型的函式成員是使用反覆運算器區塊實作時,叫用函式成員不會立即在反覆運算器區塊中執行程序代碼。 相反地,會建立並傳回列舉器物件。 這個物件會封裝反覆運算器區塊中指定的程序代碼,並在叫用列舉值物件的 MoveNext
方法時,於反覆運算器區塊中執行程序代碼。 列舉值物件具有下列特性:
- 它會實作
IEnumerator
和IEnumerator<T>
,其中T
是反覆運算器的 yield 類型。 - 它會實作
System.IDisposable
。 - 它會使用傳遞至函式成員的自變數值複本和實例值複本來初始化。
- 它有四個潛在狀態, 在之前、 執行中、 暫停和 之後,且一開始處於 之前 狀態。
列舉值物件通常是編譯程式產生的列舉值類別的實例,可將程式代碼封裝在反覆運算器區塊中,並實作列舉值介面,但可能會有其他實作方法。 如果列舉類別是由編譯器所產生,該類別將會直接或間接地嵌套於包含函式成員的類別中,具有私用可存取性,而且其名稱會保留給編譯器使用(§6.4.3)。
列舉值物件可能會實作比上述指定的介面更多。
接下來的子句描述了由列舉值物件所提供的MoveNext
、Current
、和Dispose
成員在IEnumerator
和IEnumerator<T>
介面實作中的必要行為。
列舉值物件不支援 IEnumerator.Reset
方法。 調用此方法會導致拋出System.NotSupportedException
。
15.14.5.2 MoveNext 方法
MoveNext
列舉值物件的 方法會封裝反覆運算器區塊的程序代碼。 叫用 方法會在 MoveNext
反覆運算器區塊中執行程序代碼,並適當地設定 Current
列舉值對象的屬性。 所 MoveNext
執行的精確動作取決於叫用時的 MoveNext
列舉值物件狀態:
- 如果列舉值物件的狀態是 之前,則叫用
MoveNext
:- 將狀態變更為 執行中。
- 將反覆運算器區塊的參數 (包括
this
) 初始化為列舉值物件初始化時所儲存的自變數值和實例值。 - 從頭開始執行反覆運算器區塊,直到執行中斷為止(如下所述)。
- 如果列舉值物件的狀態正在 執行,則叫用的結果
MoveNext
未指定。 - 如果列舉值物件 的狀態已暫止,則叫用MoveNext:
- 將狀態變更為 執行中。
- 將所有局部變數和參數的值(包括
this
)還原至上次暫停反覆運算器區塊執行時所儲存的值。注意:這些變數所參考之任何對象的內容,在先前呼叫
MoveNext
之後可能已變更。 附註 - 立即在導致執行暫停的 yield return 語句後繼續執行迭代器區塊,並持續執行直至被中斷執行為止(如下所述)。
- 如果列舉物件的狀態為 after,呼叫
MoveNext
會傳回 false。
MoveNext
執行反覆運算器區塊時,執行可以透過四種方式中斷:透過 yield return
語句、透過 yield break
語句、遇到反覆運算器區塊的結尾,以及擲回並傳播出反覆運算器區塊的例外狀況。
-
yield return
語句在遇到時(§9.4.4.20):- 語句中指定的表達式會被評估,隱含轉換成產生類型,並指派給列舉器物件的
Current
屬性。 - 迭代器主體的執行已暫停。 所有局部變數和參數(包括
this
)的值會被儲存,這個yield return
語句的位置也同樣會被記錄。yield return
如果語句位於一或多個try
區塊內,則相關聯的 finally 區塊此時不會被執行。 - 列舉值物件的狀態會變更為 暫止。
- 方法
MoveNext
會true
傳回其呼叫端,指出反覆專案已成功前進到下一個值。
- 語句中指定的表達式會被評估,隱含轉換成產生類型,並指派給列舉器物件的
- 在遇到
yield break
語句時(§9.4.4.20):-
yield break
如果語句位於一或多個try
區塊內,則會執行相關聯的finally
區塊。 - 列舉值物件的狀態會變更為 之後。
- 方法
MoveNext
會false
傳回其呼叫端,指出反覆專案已完成。
-
- 遇到迭代器程式區塊的結尾時:
- 列舉值物件的狀態會變更為 之後。
- 方法
MoveNext
會false
傳回其呼叫端,指出反覆專案已完成。
- 當例外被擲出並從疊代器區塊向外傳播時:
- 反覆運算器主體中的適當
finally
區塊將會由例外狀況傳播執行。 - 列舉值物件的狀態會變更為 之後。
- 例外狀況傳播會繼續傳至
MoveNext
方法的呼叫端。
- 反覆運算器主體中的適當
15.14.5.3 目前屬性
列舉值對象的 Current
屬性會受到 yield return
反覆運算器區塊中的 語句影響。
當列舉值對象處於 暫止 狀態時,的值 Current
是先前呼叫 MoveNext
所設定的值。 當列舉物件處於開始、運行或結束的狀態時,存取Current
的結果是未定義的。
針對具有非object
型別的 yield 反覆運算器,透過列舉值物件的Current
實作存取的結果,等同於透過列舉值物件的IEnumerable
Current
實作存取IEnumerator<T>
後,將結果轉換為object
。
15.14.5.4 Dispose 方法
方法 Dispose
用於清理反覆運算,方法是將列舉器物件帶入後的狀態。
- 如果列舉值物件的狀態是之前,呼叫
Dispose
會將狀態變更為之後。 - 如果列舉值物件的狀態正在 執行,則叫用的結果
Dispose
未指定。 - 如果列舉值物件 的狀態已暫止, 則叫用
Dispose
:- 將狀態變更為 執行中。
- 執行任何 finally 區塊,就像上次執行的
yield return
語句和yield break
語句一樣。 如果這導致例外狀況被擲回並從反覆運算器主體傳播出來,則列舉值物件的狀態會設定為after,且該例外狀況將傳播至Dispose
方法的呼叫者。 - 將狀態變更為 之後。
- 如果列舉值物件的狀態在 之後,叫
Dispose
用不會有任何影響。
15.14.6 可列舉物件
15.14.6.1 一般
使用反覆運算器區塊實作傳回可列舉介面類型的函式成員時,叫用函式成員不會立即在反覆運算器區塊中執行程序代碼。 相反地,會建立可列舉的物件並傳回。 可列舉物件 GetEnumerator
的方法會傳回列舉值物件,該列舉值物件會封裝反覆運算器區塊中指定的程序代碼,並在叫用列舉值物件的 MoveNext
方法時,在反覆運算器區塊中執行程序代碼。 可列舉的物件具有下列特性:
- 它會實作
IEnumerable
和IEnumerable<T>
,其中T
是反覆運算器的 yield 類型。 - 它會初始化為參數值(如果有的話)和傳遞至函式成員的實例值的複本。
可列舉物件通常是編譯程式產生的可列舉類別的實例,可將程式代碼封裝在反覆運算器區塊中,並實作可列舉的介面,但可能採用其他實作方法。 如果編譯程式產生可列舉類別,則該類別會直接或間接地巢狀於包含函式成員的類別中,其具有私人輔助功能,而且其名稱會保留供編譯程式使用(~6.4.3)。
可列舉的物件可能會實作比上述指定的介面更多的介面。
注意:例如,可列舉的物件也可以實作
IEnumerator
和IEnumerator<T>
,使其既作為可列舉物件,也作為列舉器。 一般而言,這類實作會在第一次呼叫GetEnumerator
時,傳回其自身的實例(以節省分配)。 如果有後續的GetEnumerator
調用,它會返回一個新的類別實例,通常屬於同一個類別,因此對不同列舉器實例的呼叫不會互相影響。 即使先前的列舉程式已經遍歷到序列的結尾,它也無法傳回相同的實例,因為所有日後對已耗盡列舉程式的呼叫都必須擲回例外狀況。 註釋結尾
15.14.6.2 GetEnumerator 方法
可列舉物件提供 IEnumerable
和 IEnumerable<T>
介面方法的GetEnumerator
實作。 這兩 GetEnumerator
種方法會共用一個通用實作,這個實作會取得並傳回可用的列舉值物件。 列舉物件會以初始化可列舉物件時所保存的參數值和實例值來初始化;但除此之外,列舉物件的功能如§15.14.5中所述。
15.15 異步函式
15.15.1 一般
具有async
修飾符的方法(§15.6)或匿名函式(§12.19)稱為async 函式。 一般而言,「async」是用來描述具有 async
修飾詞的任何函式。
異步函式的參數列表中指定任何 in
、out
或 ref
參數,或任何 ref struct
型別的參數都會導致編譯時期錯誤。
異步方法的return_type應為 void
或工作類型。 對於產生結果值的異步方法,工作類型應該是泛型。 對於不會產生結果值的異步方法,工作類型不得為泛型。 這類類型會分別在此規格 «TaskType»<T>
中稱為 和 «TaskType»
。 標準程式庫的System.Threading.Tasks.Task
類型和從System.Threading.Tasks.Task<TResult>
構建的類型都屬於工作類型,而類別、結構或介面類型則是透過屬性System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
與工作生成器類型相關聯的。 這類類型在此規格 «TaskBuilderType»<T>
中稱為 和 «TaskBuilderType»
。 工作類型最多可以有一個類型參數,而且不能巢狀於泛型類型中。
傳回任務類型的異步方法稱為返回任務。
工作類型在確切定義中可能會有所不同,但從語言的觀點來看,工作類型處於其中一個不完整、成功或錯誤的狀態。
故障的工作會記錄相關的例外狀況。
成功的«TaskType»<T>
記錄T
類型的結果。 工作類型是可等候的,因此工作可以是 await 表達式的運算元(§12.9.8)。
範例:工作類型
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.15.2)。 工作產生器類型應該完全符合其對應工作類型的宣告存取範圍。
注意: 如果宣告
internal
工作類型,則對應的產生器類型也必須宣告internal
並在相同的元件中定義。 如果任務類型巢狀於另一個類型內,則任務建構器類型也必須巢狀於該相同類型中。 註腳結束
異步函式可以藉由其主體中的 await 表達式 §12.9.8 暫停評估。 暫停的 await 表達式可以稍後通過 繼續委派 恢復評估。 繼續委派的類型為 System.Action
,而當它被叫用時,異步函式調用的評估將會從離開時的等候表達式處繼續。
非同步函式調用的目前呼叫端,如果該調用從未被暫停,則是原始呼叫端;否則是恢復委派的最近呼叫端。
15.15.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
建立關聯。- 建置者應在
Start()
中 或在Start()
返回後調用stateMachine.MoveNext()
,以推進狀態機器。
- 建置者應在
- 當
Start()
傳回後,async
方法會叫用builder.Task
,使工作從非同步方法中返回。 - 每次呼叫
stateMachine.MoveNext()
都會推進狀態機器。 - 如果狀態機器順利完成,將調用
builder.SetResult()
,並使用方法的返回值(如果有)。 - 否則,
e
如果在狀態機器中擲回例外狀況,builder.SetException(e)
則會呼叫 。 - 如果狀態機器到達
await expr
表示式,則會調用expr.GetAwaiter()
。 - 如果 awaiter 實作
ICriticalNotifyCompletion
且IsCompleted
為 false,則狀態機器會叫用builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
。-
AwaitUnsafeOnCompleted()
應在等待者完成時,使用含有呼叫stateMachine.MoveNext()
的Action
來呼叫awaiter.UnsafeOnCompleted(action)
。
-
- 否則,狀態機器會叫用
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.15.3 工作傳回異步函式的評估
叫用工作傳回異步函式會導致產生傳回之工作類型的實例。 這稱為 異步函式的傳回工作 。 工作一開始處於 不完整 的狀態。
接著會評估異步函式主體,直到暫停為止(藉由到達 await 運算式)或終止,此時控件會傳回給呼叫端,以及傳回工作。
當異步函式的主體終止時,返回的工作會被移出未完成的狀態:
- 如果函式主體因為到達 return 陳述式或主體結尾而終止,則在返回任務中會記錄任何結果值,並且該任務會進入 成功 狀態。
- 如果函式主體因為未攔截
OperationCanceledException
而終止,則例外狀況會記錄在處於已 取消 狀態的返回任務中。 - 如果函式主體因任何其他未捕獲的例外狀況(§13.10.6)而終止,則會在進入 故障 狀態的傳回工作中記錄例外狀況。
15.15.4 評估非同步 void 回傳函式
如果異步函式的傳回類型是 void
,則評估與上述方法不同:因為不會傳回任何工作,因此函式會改為將完成和例外狀況傳達給目前線程的同步處理內容。 同步上下文的確切定義依實現而定,但它表示當前線程運行的位置。 在開始評估、成功完成或導致擲回未捕捉的例外狀況時,會通知回傳void
的非同步函式的同步處理內容。
zh-TW: 這可讓上下文追蹤有多少個 void
傳回的異步函式正在其下執行,並決定如何處理它們引發的例外。