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 ';'?
;
class_declaration包含一組選擇性的屬性 (~22),後面接著一組選擇性的 class_modifiers (15.2.2),後面接著選擇性修飾詞 (\15.2.7),後面接著關鍵詞和名稱類別的標識符。 後面接著選擇性的type_parameter_list (~15.2.3),後面接著選擇性的class_base規格 (15.2.4)。 後面接著選擇性的type_parameter_list (~15.2.3),後面接著選擇性的class_base規格 (15.2.2.2.7),後面接著選擇性的class_base規格 (15.2.4)。 後面接著選擇性的type_parameter_list (~15.3),後面接著選擇性的type_parameter_list (~15.2.3),後面接著選擇性partial
修飾詞 (\15.2.7),後面接著關鍵詞和名稱類別的標識符。 後面接著選擇性的type_parameter_list (~15.2.3),後面接著選擇性修飾詞 (\15.2.7),後面接著關鍵詞和class
。 後面接著選擇性的type_parameter_list (~15.2.3),後面接著選擇性修飾詞 (\15.2,後面接著一組選擇性的 type_parameter_constraints_clauses ({15.2.5),後面接著class_body ({15.2.6),選擇性地後面接著分號。
除非類別宣告也提供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
因此允許(但不需要)為非抽象成員。end 範例
如果某個類別的部分類型宣告 (abstract
) 包含 修飾詞,則類別是抽象的。 否則,類別為非抽象。
15.2.2.3 密封類別
修飾 sealed
詞是用來防止衍生自 類別。 如果密封類別指定為另一個類別的基類,就會發生編譯時期錯誤。
密封類別不能也是抽象類。
注意:
sealed
修飾詞主要用於防止非預期的衍生,但也啟用特定的運行時間優化。 特別是,由於已知密封類別永遠不會有任何衍生類別,因此可以將密封類別實例上的虛擬函式成員調用轉換成非虛擬調用。 end note
如果類別的一個或多個部分類型宣告 ({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)。
注意:所有常數和巢狀類型都會分類為靜態成員。 end note
- 靜態類別不得具有、
protected
或private protected
宣告的輔助功能成員protected internal
。
違反上述任何限制的編譯時間錯誤。
靜態類別沒有實例建構函式。 無法在靜態類別中宣告實例建構函式,而且靜態類別未提供任何預設實例建構函式(~15.11.5)。
靜態類別的成員不會自動為靜態,而且成員宣告應明確包含 static
修飾詞(常數和巢狀類型除外)。 當類別巢狀於靜態外部類別內時,除非巢狀類別明確包含 static
修飾詞,否則巢狀類別不是靜態類別。
如果某個類別的部分類型宣告 (static
) 包含 修飾詞,則類別是靜態的。 否則,類別不是靜態的。
15.2.2.4.2 參考靜態類別類型
如果可以參考靜態類別,則允許namespace_or_type_name (~7.8)
-
namespace_or_type_name是
T
格式為、 或 namespace_or_type_name的T.I
。 -
typeof(T)
如果允許參考靜態類別,則primary_expression (~12.8)
-
primary_expression是
E
格式的 member_access (E.I
) 中的 。
在任何其他內容中,參考靜態類別是編譯時期錯誤。
注意:例如,將靜態類別當做基類、成員的構成型別(~15.3.7)、泛型型別自變數或類型參數條件約束的錯誤。 同樣地,靜態類別不能用於數位型別、新運算式、轉換運算式、is 運算式、as expression、
sizeof
運算式或預設值運算式。 end note
15.2.3 類型參數
類型參數是一個簡單的標識符,表示提供用來建立建構型別之類型自變數的佔位元元。 藉由 constrast,類型自變數 (~8.4.2) 是建立建構型別時替代類型參數的類型。
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter定義於 •8.5 中。
類別宣告中的每個類型參數都會在該類別的宣告空間 ({7.3) 中定義名稱。 因此,它不能與該類別的另一個類型參數或在該類別中宣告的成員具有相同的名稱。 類型參數的名稱不能與類型本身相同。
如果兩個部分泛型型別宣告(在相同的程式中)具有相同的完整名稱(包括類型參數數目的generic_dimension_specifier({12.8.18),則會產生相同的未系結泛型型別。 兩個這類部分類型宣告應依序為每個類型參數指定相同的名稱。
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時,它會指定所宣告類別的直接基類。 如果非部分類別宣告沒有object
。 當部分類別宣告包含基類規格時,該基類規格應該參考與包含基類規格之該部分所有其他部分相同的類型。 如果部分類別中沒有任何部分包含基類規格,則基類為 object
。 類別繼承其直接基類的成員,如 •15.3.4 中所述。
範例:在下列程式代碼中
class A {} class B : A {}
類別
A
據說是 的B
直接基類,據說B
衍生自A
。 由於A
不會明確指定直接基類,因此其直接基類是隱含的object
。end 範例
針對建構類別類型,包括泛型型別宣告中宣告的巢狀型別({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> {}
end 範例
類別類型的直接基類應該至少可以和類別類型本身一樣可存取(~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
。end 範例
類別不相依於其內巢狀的類別。
範例:在下列程式代碼中
class A { class B : A {} }
B
A
相依於 (因為A
是其直接基類和其立即封入類別),但不A
相依於 (因為 不是因為不相依B
於(因為B
不是因為不相依),也不是的A
封入類別)。 因此,此範例是有效的。end 範例
無法衍生自密封類別。
範例:在下列程式代碼中
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
類別
B
發生錯誤,因為它嘗試衍生自密封類別A
。end 範例
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
。end 範例
一般而言,每個元件都會提供在該元件上宣告之介面的實作;不過,這不是必要條件。 元件可以提供在不同元件上宣告之介面的實作。
範例:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
end 範例
類別宣告中指定的基底介面可以建構介面類型 ({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> {}
end 範例
介面實作會在 \18.6 中進一步討論。
15.2.5 類型參數條件約束
泛型型別和方法宣告可以選擇性地藉由包含 type_parameter_constraints_clause來指定類型參數條件約束。
type_parameter_constraints_clauses
: type_parameter_constraints_clause
| type_parameter_constraints_clauses 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()
。
主要條件約束可以是類別類型、unmanaged
類別類型和參考型別條件約束可以包含 nullable_type_annotation。
次要條件約束可以是interface_type或type_parameter,選擇性地後面接著nullable_type_annotation。 nullable_type_annotatione* 的存在表示類型自變數是對應至滿足條件約束之不可為 Null 參考型別的可為 Null 參考型別。
參考型別條件約束會指定用於型別參數的類型自變數應該是參考型別。 所有類別類型、介面類型、委派類型、數位類型和類型參數已知為參考類型(如下所述)都滿足此條件約束。
類別類型、參考型別條件約束和次要條件約束可以包含可為 Null 的類型批注。 類型參數上存在或不存在此批注,表示類型自變數的可為 Null 預期:
- 如果條件約束不包含可為 Null 的類型批注,則類型自變數必須是不可為 Null 的參考型別。 如果類型自變數是可為 Null 的參考型別,編譯程式可能會發出警告。
- 如果條件約束包含可為 Null 的類型批注,則不可為 Null 的參考型別和可為 Null 的參考型別都滿足條件約束。
類型自變數的 Null 屬性與類型參數的 Null 屬性不相符。 如果類型參數的 Null 性不符合類型自變數的 Null 屬性,編譯程式可能會發出警告。
注意:若要指定類型自變數是可為 Null 的參考型別,請勿將可為 Null 的類型批注新增為條件約束(使用
T : class
或T : BaseClass
),但在整個泛型宣告中使用T?
來指出類型自變數的對應可為 Null 參考型別。 end note
可為 Null 的類型批注 ?
無法在不受限制的類型自變數上使用。
如果類型 T
自變數是可為 Null 的參考型 C?
別,則的實體 T?
會解譯為 C?
,而不是 C??
。
範例:下列範例顯示類型自變數的可為 Null 性如何影響其類型參數之宣告的 Null 性:
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 型別。end 範例
非 Null 條件約束會指定用於類型參數的類型自變數應該是不可為 Null 的實值型別或不可為 Null 的參考型別。 允許類型自變數不是不可為 Null 的實值型別或不可為 Null 的參考型別,但編譯器可能會產生診斷警告。
實值型別條件約束指定用於類型參數的類型自變數應該是不可為 Null 的實值型別。 具有實值型別條件約束的所有不可為 Null 結構類型、列舉型別和型別參數都滿足此條件約束。 請注意,雖然分類為實值型別,但可為 Null 的實值型別 (~8.3.12) 不符合實值型別條件約束。 具有實值型別條件約束的類型參數不應該有constructor_constraint,雖然它可能用作另一個具有constructor_constraint之類型參數的類型自變數。
注意:此
System.Nullable<T>
類型會指定 的不可為 Null 的實值型別條件約束T
。 因此,以遞歸方式建構的窗體T??
類型,並Nullable<Nullable<T>>
禁止使用。 end note
因為 unmanaged
不是因為 不爲關鍵詞,在 primary_constraint Unmanaged 條件約束一律與class_type語法模棱兩可。 基於相容性考慮,如果名稱的名稱查閱 (unmanaged
) 成功,則會將其class_type
視為 。 否則會將其視為 Unmanaged 條件約束。
Unmanaged 類型條件約束會指定用於類型參數的類型自變數必須是不可為 Null 的 Unmanaged 類型(~8.8)。
指標類型永遠不允許為類型自變數,而且即使為 Unmanaged 類型,也無法滿足任何類型條件約束。
如果條件約束是類別類型、介面類型或類型參數,該類型會指定用於該類型參數之每個類型自變數都應該支援的最小「基底類型」。 每當使用建構的型別或泛型方法時,就會在編譯階段針對型別參數的條件約束檢查類型自變數。 提供的型別自變數應符合 \8.4.5 中所述的條件。
class_type條件約束應滿足下列規則:
- 此類型應該是類別類型。
- 類型不得為
sealed
。 - 類型不得為下列其中一種類型:
System.Array
或System.ValueType
。 - 類型不得為
object
。 - 指定型別參數的一個條件約束最多可以是類別類型。
指定為 interface_type 條件約束的類型應符合下列規則:
- 此類型應該是介面類型。
- 指定的 子句中不得多次
where
指定類型。
不論是哪一種情況,條件約束都可能牽涉到關聯型別或方法宣告的任何型別參數作為建構型別的一部分,而且可能涉及所宣告的類型。
任何指定為型別參數條件約束的類別或介面類型,至少都必須以宣告的泛型型別或方法一樣可存取 (~7.5.5)。
指定為 type_parameter 條件約束的類型應符合下列規則:
- 此類型應該是類型參數。
- 指定的 子句中不得多次
where
指定類型。
此外,類型參數的相依性圖表中不得有迴圈,其中相依性是所定義的可轉移關聯:
- 如果類型參數作為類型參數
T
的條件約束,則S
。T
- 如果類型參數
S
相依於類型參數T
,而T
相依於類型參數,則S
類型參數U
。
鑒於此關聯性,類型參數依賴本身(直接或間接)是編譯時間錯誤。
任何條件約束都應該在相依型別參數之間保持一致。 如果類型參數 S
相依於類型參數 T
,則:
-
T
不應有實值型別條件約束。 否則,會有效密封,T
因此S
會強制為與T
相同的類型,而不需要兩個類型參數。 - 如果
S
具有實值型別條件約束,則T
不應該有 class_type 條件約束。 - 如果 具有class_type條件約束
S
,且具有A
條件約束T
,則應該有識別轉換或隱含參考轉換,B
或從A
隱含參考轉換至 。B
B
A
- 如果
S
也相依於型別參數U
,且U
具有class_type條件約束,且A
具有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
的編譯時間錯誤。
範例:以下是條件約束的範例:
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 { ... }
end 範例
型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
。
型T
定義如下:
讓我們 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
)。 如果集合沒有包含的類型,則的有效基類T
為object
。 一致性規則可確保最包含的類型存在。
如果類型參數是方法類型參數,其條件約束繼承自基底方法,則有效的基類會在類型替代之後計算。
這些規則可確保有效的基類一律是 class_type。
型T
定義如下:
-
如果沒有
T
secondary_constraints,則其有效介面集是空的。 - 如果
T
具有 interface_type 條件約束,但沒有 type_parameter 條件約束,則其有效介面集是其 interface_type 條件約束的動態清除集合。 - 如果沒有
T
interface_type條件約束,但具有type_parameter條件約束,則其有效介面集是其type_parameter條件約束之有效介面集的聯集。 - 如果
T
同時具有interface_type條件約束和type_parameter條件約束,則其有效介面集是其interface_type條件約束和其type_parameter條件約束之有效介面集合的動態清除集合的聯集。
如果類型參數具有參考型別條件約束或其有效的基類不是 或 object
,則System.ValueType
別。 如果已知為參考型別且具有不可為 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> { ... }
正確,因為包含條件約束的元件(前兩個)會分別針對同一組類型參數指定相同的主要、次要和建構函式條件約束集。
end 範例
15.2.6 類別主體
類別 的class_body 會定義該類別的成員。
class_body
: '{' class_member_declaration* '}'
;
15.2.7 部分宣告
在多個元件中定義類別、結構或介面類型時,會使用 修飾 partial
詞。 修飾partial
詞是內容關鍵詞(~6.4.4class
struct
或 interface
之前,只有特殊意義。
部分型別宣告的每個部分都應該包含 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; }
end 範例
在 {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),而且這些宣告會結合來定義單一類型。
注意:因為類型宣告的完整名稱會編碼類型參數的數目,所以只要兩個不同的類型參數數目不同,就可能會共用相同的名稱。 end note
常數、欄位、屬性或事件的名稱應該與相同類別中宣告的所有其他成員名稱不同。
方法的名稱應該與相同類別中宣告的所有其他非方法名稱不同。 此外,方法的簽章 ({7.6) 應該與相同類別中宣告之所有其他方法的簽章不同,而相同類別中宣告的兩個方法不得具有與 、
in
和out
完全ref
不同的簽章。實例建構函式的簽章應該與相同類別中宣告之所有其他實例建構函式的簽章不同,而相同類別中宣告的兩個建構函式則不一定只有和
ref
不同的out
簽章。索引器的簽章應該與相同類別中宣告之所有其他索引器的簽章不同。
運算子的簽章應該與相同類別中宣告之所有其他運算符的簽章不同。
類別的繼承成員 ({15.3.4) 不是類別宣告空間的一部分。
注意:因此,允許衍生類別宣告名稱或簽章為繼承成員的成員(實際上隱藏繼承的成員)。 end note
在多個元件中宣告之型別的成員集合(\15.2.7)是每個元件中宣告的成員聯集。 類型宣告之所有部分的主體會共用相同的宣告空間({7.3),而每個成員的範圍({7.7)則延伸到所有元件的主體。 任何成員的輔助功能網域一律包含封入類型的所有部分;一個元件中宣告的私人成員可從另一個元件自由存取。 除非該成員是具有 partial
修飾詞的類型,否則在型別的多個部分中宣告相同的成員是編譯時期錯誤。
範例:
partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int z; } }
end 範例
欄位初始化順序在 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
end 範例
15.3.3 建構型別的成員
建構型別的非繼承成員是藉由 替代成員宣告中的每個type_parameter ,取得建構型別的對應 type_argument 。 替代程式是以類型宣告的語意意義為基礎,而不只是文字替代。
範例:指定泛型類別宣告
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[,][]
end 範例
在實例函式成員內,的型別是包含宣告的實例類型 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 } }
end 範例
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中每個對應的型別參數。 接著,這些成員會藉由替代成員宣告中的每個type_parameter來轉換,而base_class_specification的對應type_argument。
範例:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }
在上述程式代碼中,建構的型
D<int>
別具有非繼承成員公用int
G(string s)
,方法是取代類型參數int
的型別自變數T
。D<int>
也具有類別宣告B
的繼承成員。 這個繼承的成員是由先藉由在基類規格B<int[]>
中取代D<int>
int
來判斷的T
基類類型B<T[]>
。 然後,做為的B
int[]
型別自變數,會取代U
在中public U F(long index)
產生繼承的成員public int[] F(long index)
。end 範例
15.3.5 新修飾詞
class_member_declaration可以宣告與繼承成員具有相同名稱或簽章的成員。 發生這種情況時,會說衍生類別成員會 隱藏 基類成員。 如需成員隱藏繼承成員時的精確規格,請參閱 •7.7.2.3 。
如果可存取,則繼承的成員M
會被視為可用,而且沒有其他繼承的可存取成員 N 已隱藏 M
。M
隱含隱藏繼承的成員不會被視為錯誤,但編譯程式應該發出警告,除非衍生類別成員的宣告包含 new
修飾詞,以明確指出衍生成員是要隱藏基底成員。 如果巢狀類型的一或多個部分宣告 ({15.2.7) 包含 new
修飾詞,如果巢狀類型隱藏可用的繼承成員,則不會發出任何警告。
new
如果修飾詞包含在不會隱藏可用繼承成員的宣告中,則會發出該效果的警告。
15.3.6 Access 修飾詞
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 靜態和實例成員
類別的成員是靜態成員或實例成員。
注意:一般而言,將靜態成員視為屬於類別和實例成員屬於物件(類別的實例)會很有用。 end note
當欄位、方法、屬性、事件、運算元或建構函式宣告包含 static
修飾詞時,它會宣告靜態成員。 此外,常數或類型宣告會隱含宣告靜態成員。 靜態成員具有下列特性:
- 當靜態成員
M
在窗體的 member_accessE.M
參考時,E
應表示具有成員M
的類型。 這是表示 實例的E
編譯時間錯誤。 - 非泛型類別中的靜態字段會確切識別一個儲存位置。 無論建立非泛型類別的實例數目為何,只有一份靜態字段複本。 不論封閉式建構型別的實例數目為何,每個相異的封閉式建構型別 (~8.4.3) 都有自己的靜態字段集。
- 靜態函式成員(方法、屬性、事件、運算符或建構函式)不會在特定實例上運作,而且在這類函式成員中參考這個是編譯時間錯誤。
當欄位、方法、屬性、事件、索引器、建構函式或完成項宣告不包含靜態修飾詞時,它會宣告實例成員。 (實例成員有時稱為非靜態成員。實例成員具有下列特性:
- 在窗體
M
的 member_access (~12.8.7) 中參考實例成員E.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中,實例成員應透過實例存取,靜態成員應透過類型存取。end 範例
15.3.9 巢狀類型
15.3.9.1 一般
類別或結構內宣告的類型稱為 巢狀類型。 在編譯單位或命名空間內宣告的類型稱為 非巢狀類型。
範例:在下列範例中:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
類別
B
是巢狀類型,因為它是在 類別A
內宣告,而 類別A
是非巢狀類型,因為它是在編譯單位內宣告。end 範例
15.3.9.2 完整名稱
巢狀類型宣告的完整名稱 (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
。end 範例
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
在巢狀類型內無法用來參考包含型別的實例成員。 如果巢狀類型需要存取其包含型別的實例成員,則可以藉由為包含型別的實例提供 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
傳遞給 的建構函式,以提供實例成員的後續存取C
權。end 範例
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
會呼叫 中F
定義的靜態方法C
,並F
具有私用宣告的輔助功能。end 範例
巢狀類型也可以存取在其包含類型之基底類型中定義的受保護成員。
範例:在下列程式代碼中
class Base { protected void F() => Console.WriteLine("Base.F"); } class Derived: Base { public class Nested { public void G() { Derived d = new Derived(); d.F(); // ok } } } class Test { static void Main() { Derived.Nested n = new Derived.Nested(); n.G(); } }
巢狀類別
Derived.Nested
會透過的 實例F
呼叫 ,Derived
存取 在基類中Base
定義的受保護方法Derived
。end 範例
泛型類別中的 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 } }
end 範例
雖然程式設計樣式不正確,但巢狀類型中的類型參數可以隱藏在外部類型中宣告的成員或類型參數。
範例:
class Outer<T> { class Inner<T> // Valid, hides Outer's T { public T t; // Refers to Inner's T } }
end 範例
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# 實作中的保留成員名稱細節保持一致。
end note
完成項的宣告({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 保留給事件的成員名稱
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 保留給索引器的成員名稱
T get_Item(L);
void set_Item(L, T value);
即使索引器是只讀或唯讀的,還是保留這兩個簽章。
此外,成員名稱 Item
已保留。
15.3.10.5 保留給完成項的成員名稱
針對包含完成項的類別 ({15.13),保留下列簽章:
void Finalize();
15.3.10.6 保留給運算符的方法名稱
下列方法名稱是保留的。 雖然許多運算符在此規格中都有對應的運算符,但有些運算符會保留供未來的版本使用,有些則保留給與其他語言的 Interop。
方法名稱 | C# 運算子 |
---|---|
op_Addition |
+ (二進位) |
op_AdditionAssignment |
(保留) |
op_AddressOf |
(保留) |
op_Assign |
(保留) |
op_BitwiseAnd |
& (二進位) |
op_BitwiseAndAssignment |
(保留) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(保留) |
op_CheckedAddition |
(保留供日後使用) |
op_CheckedDecrement |
(保留供日後使用) |
op_CheckedDivision |
(保留供日後使用) |
op_CheckedExplicit |
(保留供日後使用) |
op_CheckedIncrement |
(保留供日後使用) |
op_CheckedMultiply |
(保留供日後使用) |
op_CheckedSubtraction |
(保留供日後使用) |
op_CheckedUnaryNegation |
(保留供日後使用) |
op_Comma |
(保留) |
op_Decrement |
-- (前置詞和後置詞) |
op_Division |
/ |
op_DivisionAssignment |
(保留) |
op_Equality |
== |
op_ExclusiveOr |
^ |
op_ExclusiveOrAssignment |
(保留) |
op_Explicit |
明確 (縮小) 強制 |
op_False |
false |
op_GreaterThan |
> |
op_GreaterThanOrEqual |
>= |
op_Implicit |
隱含(擴大) 強制 |
op_Increment |
++ (前置詞和後置詞) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(保留) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(保留) |
op_LogicalNot |
! |
op_LogicalOr |
(保留) |
op_MemberSelection |
(保留) |
op_Modulus |
% |
op_ModulusAssignment |
(保留) |
op_MultiplicationAssignment |
(保留) |
op_Multiply |
* (二進位) |
op_OnesComplement |
~ |
op_PointerDereference |
(保留) |
op_PointerToMemberSelection |
(保留) |
op_RightShift |
>> |
op_RightShiftAssignment |
(保留) |
op_SignedRightShift |
(保留) |
op_Subtraction |
- (二進位) |
op_SubtractionAssignment |
(保留) |
op_True |
true |
op_UnaryNegation |
- (一元) |
op_UnaryPlus |
+ (一元) |
op_UnsignedRightShift |
(保留供日後使用) |
op_UnsignedRightShiftAssignment |
(保留) |
15.4 常數
常數是代表常數值的類別成員:可在編譯時期計算的值。 constant_declaration引進指定類型的一或多個常數。
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
constant_declaration可能包含一組屬性(~22)、修飾詞(\15.3.5),以及任何一種允許的宣告輔助功能類型(\15.3.6)。new
屬性和修飾詞會套用至constant_declaration所宣告的所有成員。 即使常數被視為靜態成員, constant_declaration 不需要也不允許 static
修飾詞。 同一個修飾詞在常數宣告中出現多次是錯誤的。
constant_declaration的類型會指定宣告所引進的成員類型。 此類型後面接著constant_declarators (13.6.3),每個清單都會引進新的成員。 constant_declarator是由一個標識符所組成,該標識符會命名成員,後面接著 “” 標記,後面接著提供=
(\12.23)。
常數宣告中指定的類型應該是 sbyte
、byte
、short
、ushort
、int
uint
long
ulong
char
float
double
decimal
bool
enum_typestring
或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
成員宣告、屬性和其他常數宣告。 end note
注意:如12.23 中所述,constant_expression是可在編譯時期完整評估的表達式。 由於建立非 null 值的唯一
new
string
可能值。null
end note
需要常數值的符號名稱,但是當常數宣告中不允許該值的類型,或當constant_expression無法在編譯時期計算值時,可能會改用只讀字段 ({15.5.3)。
注意:和
const
的版本設定語readonly
意不同 (\15.5.3.3.3)。 end note
宣告多個常數的常數宣告相當於具有相同屬性、修飾詞和型別之單一常數的多個宣告。
範例:
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; }
end 範例
只要相依性不是循環性質,常數就允許相依於相同程式中的其他常數。
範例:在下列程式代碼中
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
。end 範例
常數宣告可能相依於其他程式的常數,但這類相依性只能在單一方向進行。
範例:參考上述範例,如果在
A
個別程式中宣告 和B
,就有可能A.X
相依B.Z
於 ,但B.Z
無法同時相依於A.Y
。 end 範例
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)、四個存取修飾詞的有效組合(new
),以及修飾詞(~15.5.2)。static
此外,field_declaration可能包含修飾詞(~15.5.3)或readonly
修飾詞(~15.5.4),但不包括兩者。volatile
屬性和修飾詞會套用至field_declaration宣告的所有成員。 同一 個修飾詞在field_declaration中出現多次是錯誤的。
field_declaration的類型會指定宣告所引進的成員類型。 此類型後面接著一份variable_declarator清單,每個清單都會引進新的成員。 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; }
end 範例
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
反而具有完全相同的效果。end 範例
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
為常數,則在編譯時X
會取得的值Program2
,而且在重新編譯之前Program1
,不會受到 中的Program2
變更影響。end 範例
15.5.4 揮發性欄位
當field_declaration包含volatile
修飾詞時,該宣告所引進的字段是揮發性字段。 對於非揮發性欄位,重新排序指令的優化技術可能會導致多線程程式產生非預期且無法預測的結果,這些程式會存取字段,而不需要同步處理,例如lock_statement所提供的程式(~13.13)。 這些優化可由編譯程式、運行時間系統或硬體來執行。 對於揮發性欄位,這類重新排序優化會受到限制:
- 動態欄位的讀取稱為 volatile 讀取。 揮發性讀取具有「取得語意」;也就是說,它保證會在指令序列中發生記憶體的任何參考之前發生。
- 動態欄位的寫入稱為動態寫入。 揮發性寫入具有「發行語意」;也就是說,在指令序列中寫入指令之前,保證會在任何記憶體參考之後發生。
這些限制可確保所有的執行緒將會觀察任何其他執行緒執行的 volatile 寫入會按照其執行的順序進行。 不需要一個符合規範的實作,才能提供從所有執行線程看到之揮發性寫入的單一總順序。 揮發性欄位的類型應該是下列其中一項:
- reference_type。
- 已知為參考型別的type_parameter (~15.2.5)。
- 型
byte
別 、sbyte
、short
、ushort
int
uint
char
float
bool
、、System.IntPtr
或 。System.UIntPtr
- 具有、、、、、
byte
sbyte
或short
enum_base 類型的ushort
enum_type。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
,則允許存放區在存放result
區之後finished
可以從欄位result
讀取值 0。 宣告finished
為volatile
欄位可防止任何這類不一致。end 範例
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
都會自動初始化為預設值。end 範例
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
初始化表示式時,的值已經1
是 ,因此b
會初始化為2
。end 範例
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 中所
A
執行。end 範例
15.5.6.3 實例欄位初始化
類別的實例欄位變數初始化表達式會對應至該類別的任何一個實例建構函式專案(~15.11.3)專案時,立即執行的指派序列。 在部分類別中,「文字順序」的意義是由 \15.5.6.1 指定。 變數初始化表達式會以出現在類別宣告中的文字順序執行({15.5.6.1)。 類別實例建立和初始化程式會在 •15.11 中進一步說明。
實例欄位的變數初始化表達式無法參考所建立的實例。 因此,它是變數初始化表達式中參考this
的編譯時期錯誤,因為變數初始化表達式是透過simple_name參考任何實例成員的編譯時期錯誤。
範例:在下列程式代碼中
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
的
y
變數初始化表達式會導致編譯時期錯誤,因為它參考正在建立的實例成員。end 範例
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 自動具有指定的語意。 end note
一個method_declaration可能包含一組屬性(~22)和其中一種允許的宣告輔助功能(\15.3.6)、new
(\15.3.5)、 static
(\15.6.3)、(\15.6.3virtual
\15 .6.4)、 override
(~15.6.5)、 sealed
(~15.6.6)、(15.6.7extern
~15.6.8)和async
(~15.15)修飾詞。
如果下列所有專案都成立,宣告就具有有效的修飾詞組合:
- 宣告包含有效存取修飾詞的組合({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 為 ,則方法會傳回 no-no-value 且不會傳回值;
void
- 否則,方法會 依值 傳回,並傳回值。
傳 回值或 returns-no-value 方法宣告的 return_type會指定方法所傳回的結果類型,如果有的話。 只有 returns-no-value 方法可以包含 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)。
傳 回值或 returns-no-value 方法的 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 s ({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
修飾詞、類型、標識符和選擇性default_argument。 每個 fixed_parameter 都會宣告具有指定名稱之指定型別的參數。
this
修飾詞會將 方法指定為擴充方法,而且只能在非泛型非巢狀靜態類別中靜態方法的第一個參數上使用。 如果參數是限制為 struct
的型別或型別參數struct
,this
修飾詞可能會與 或 ref
修飾詞結合,但不能in
與 out
修飾詞結合。 擴充方法會在 •15.6.10 中進一步說明。 具有default_argument的fixed_parameter稱為選擇性參數,而不含default_argument的fixed_parameter則是必要參數。 必要的參數不得出現在parameter_list的選擇性參數之後。
具有 ref
、 out
或 this
修飾詞的參數不能有 default_argument。 輸入參數可能有 default_argument。
default_argument中的運算式應該是下列其中一項:
- constant_expression
- 格式
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
修飾詞不是 。 end note
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
不過,當參數是in
參數時,自變數可能是隱含轉換 (~10.2) 從該自變數表達式到對應參數類型的表達式。
在宣告為反覆運算器(~15.14)或異步函式的函式上,不允許參考參數(~15.15)。
在採用多個參考參數的方法中,多個名稱可以代表相同的儲存位置。
15.6.2.3.2 輸入參數
使用 in
修飾詞宣告的參數是 輸入參數。 對應至輸入參數的自變數是方法調用點的現有變數,或是方法調用中實作所建立的變數(~12.6.2.3)。 如需明確指派規則,請參閱 \9.2.8。
這是修改輸入參數值的編譯時期錯誤。
注意:輸入參數的主要用途是提高效率。 當方法參數的類型是大型結構時(就記憶體需求而言),在呼叫 方法時,避免複製自變數的整個值會很有用。 輸入參數可讓方法參考記憶體中現有的值,同時提供保護以防止這些值不必要的變更。 end note
15.6.2.3.3 參考參數
使用 ref
修飾詞宣告的參數是 參考參數。 如需明確指派規則,請參閱 \9.2.6。
範例:範例
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }
產生下列輸出
i = 2, j = 1
針對中的呼叫
Swap
,Main
表示x
和i
表示y
。j
因此,調用的效果是交換 和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); } }
中的調用
F
會針對和G
傳遞 對s
的a
b
參考。 因此,針對該呼叫,名稱s
、a
與b
都參考相同的儲存位置,而三個指派都會修改實例欄位s
。end 範例
struct
如果是型別,在實例方法、實例存取子(~12.2.1)或具有建構函式初始化表達式的實例建構函式中,this
關鍵詞的行為與結構類型的參考參數完全相同(~12.8.14)。
15.6.2.3.4 輸出參數
使用 out
修飾詞宣告的參數是 輸出參數。 如需明確指派規則,請參閱 \9.2.7。
宣告為部分方法的方法 (~15.6.9) 不應該有輸出參數。
注意:輸出參數通常用於產生多個傳回值的方法。 end note
範例:
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
之前取消指派,而且在呼叫之後會被視為絕對指派。end 範例
15.6.2.4 參數陣列
使用 params
修飾詞宣告的參數是參數數位。 如果參數清單包含參數陣列,它應該是清單中的最後一個參數,而且應該為單一維度陣列類型。
範例:型
string[]
別 和string[][]
可以當做參數數位的類型使用,但類型string[,]
不能。 end 範例
注意:無法將 修飾詞與
params
修飾詞 、in
或out
結合ref
。 end note
參數陣語允許在方法調用的兩種方式之一中指定自變數:
- 為參數陣列指定的自變數可以是可隱含轉換成參數陣列類型的單一表達式(~10.2)。 在此情況下,參數陣列的行為與實值參數類似。
- 或者,調用可以指定參數數位的零個或多個自變數,其中每個自變數都是可隱含轉換成參數陣元素類型的表達式。 在此情況下,調用會建立參數數位類型的實例,其長度會對應至自變數數目、使用指定的自變數值初始化數位實例的專案,並使用新建立的數位實例做為實際自變數。
除了在調用中允許變數數目的自變數之外,參數數位完全相當於相同類型的值參數 (~15.6.2.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[]
個專案,並將該陣列實例當做 value 參數傳遞。 同樣地,的第三個調用F
會建立零元素int[]
,並將該實例當做值參數傳遞。 第二個和第三個調用完全相當於寫入:F(new int[] {10, 20, 30, 40}); F(new int[] {});
end 範例
執行多載解析時,具有參數數位的方法可能適用,可以是以一般形式或展開形式(~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[])
在此範例中,具有參數數位之方法的兩種可能展開形式已經包含在類別中,做為一般方法。 因此,當執行多載解析時,不會考慮這些展開的窗體,因此第一個和第三個方法調用會選取一般方法。 當類別宣告具有參數陣語的方法時,也並不罕見地包含某些擴充形式做為一般方法。 如此一來,就可以避免叫用具有參數數位之方法展開形式時所發生的陣列實例配置。
end 範例
陣列是參考型別,因此針對參數數位傳遞的值可以是
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[]
參考)。end 範例
15.6.3 靜態和實例方法
當方法宣告包含 static
修飾詞時,該方法會稱為靜態方法。 當沒有任何 static
修飾詞存在時,方法會說為實例方法。
靜態方法不會在特定實例上運作,而且是在靜態方法中參考 this
的編譯時期錯誤。
實例方法會在類別的指定實例上運作,而且該實例可以存取為 this
(~12.8.14)。
靜態和實例成員之間的差異會在 \15.3.8 中進一步討論。
15.6.4 虛擬方法
當實例方法宣告包含虛擬修飾詞時,該方法會稱為虛擬方法。 當沒有任何虛擬修飾詞存在時,方法會說為 非虛擬方法。
非虛擬方法的實作不一致:不論在宣告方法的類別實例或衍生類別的實例上叫用方法,實作都相同。 相反地,虛擬方法的實作可由衍生類別取代。 取代繼承之虛擬方法實作的程序稱為覆寫該方法 (~15.6.5)。
在虛擬方法調用中, 執行該調用的實例運行時間類型 會決定要叫用的實際方法實作。 在非虛擬方法調用中, 實例的編譯時間類型 是判斷因素。 確切地說,當具有編譯時間類型和運行時間類型N
之實例上的自變數清單A
叫用名為 C
的方法時, R
R
C
會以下列方式處理調用:C
- 在系結時,多載解析會套用至 、 和 ,從中宣告並繼承
C
的方法集合中選取特定方法N
。A
M
C
這在 •12.8.10.2 中說明。 - 然後在執行時間:
- 如果
M
非虛擬方法,M
則會叫用 。 - 否則,
M
是虛擬方法,而且會叫用 與 相關的M
最衍生實R
作。
- 如果
對於類別所宣告或繼承的每個虛擬方法,該類別都有 方法的衍生 實作。 與類別M
相關的虛擬方法R
最衍生實作取決於如下:
- 如果
R
包含的M
虛擬宣告,則這是 與相關的M
最衍生實R
作。 - 否則,如果
R
包含的M
覆寫,則這是 與相關的M
最衍生實R
作。 - 否則,的衍生實
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> }
end 範例
覆寫宣告可以使用base_access存取覆寫的基底方法({12.8.15)。
範例:在下列程式代碼中
class A { int x; public virtual void PrintFields() => Console.WriteLine($"x = {x}"); } class B : A { int y; public override void PrintFields() { base.PrintFields(); Console.WriteLine($"y = {y}"); } }
中的
base.PrintFields()
B
調用會叫用 中A
宣告的 PrintFields 方法。 base_access會停用虛擬調用機制,並只將基底方法視為非virtual
方法。 如果 已B
寫入((A)this).PrintFields()
中的呼叫,它會遞歸地叫PrintFields
用 中宣告的方法,而不是 在B
中A
宣告的方法,因為PrintFields
是虛擬且運行時間類型((A)this)
為B
。end 範例
只有包含 override
修飾詞,方法才能覆寫另一個方法。 在其他所有情況下,具有相同簽章與繼承方法的方法只會隱藏繼承的方法。
範例:在下列程式代碼中
class A { public virtual void F() {} } class B : A { public virtual void F() {} // Warning, hiding inherited F() }
中的
F
B
方法不包含override
修飾詞,因此不會覆寫F
中的A
方法。 相反地F
,中的B
方法會隱藏 中的A
方法,並回報警告,因為宣告不包含新的修飾詞。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
。 因此,允許 在中F
宣告C
覆寫F
繼承自A
的 。end 範例
15.6.6 Sealed 方法
當實例方法宣告包含 sealed
修飾詞時,該方法會稱為 密封方法。 密封方法會覆寫具有相同簽章的繼承虛擬方法。 密封方法也應標示為 override
修飾詞。
sealed
使用修飾詞可防止衍生類別進一步覆寫 方法。
範例:範例
class A { public virtual void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public sealed override void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class C : B { public override void G() => Console.WriteLine("C.G"); }
類別
B
提供兩個F
覆寫方法:具有sealed
修飾詞的方法,以及G
沒有的方法。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
方法並提供實際的實作。end 範例
參考抽象方法時,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()
編譯時間錯誤,因為它參考了抽象方法。end 範例
允許抽象方法宣告覆寫虛擬方法。 這可讓抽象類強制在衍生類別中重新實作 方法,並使方法的原始實作無法使用。
範例:在下列程式代碼中
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
會覆寫抽象方法以提供自己的實作。end 範例
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); }
end 範例
15.6.9 部分方法
當方法宣告包含 partial
修飾詞時,該方法會說為 部分方法。 部分方法只能宣告為部分類型的成員(~15.2.7),而且受限於一些限制。
部分方法可以在類型宣告的某個部分中定義,並在另一個部分實作。 實作是選擇性的;如果沒有元件實作 partial 方法,則部分方法宣告和所有呼叫都會從元件組合所產生的類型宣告中移除。
部分方法不得定義存取修飾詞;它們是隱含私用的。 其傳回型別應為 void
,且其參數不得為輸出參數。 只有在關鍵詞正前方出現標識碼部分時,才會在方法宣告中辨識為內容關鍵詞 (void
)。 部分方法無法明確實作介面方法。
部分方法宣告有兩種:如果方法宣告的主體是分號,則宣告會稱為 定義部分方法宣告。 如果主體不是分號,則宣告據說是 實作部分方法宣告。 在類型宣告的各個部分,可能只有一個定義具有指定簽章的部分方法宣告,而且可能只有一個實作具有指定簽章的部分方法宣告。 如果已指定實作部分方法宣告,則對應的定義部分方法宣告應存在,而且宣告應符合下列指定:
- 宣告應具有相同修飾詞(雖然不一定以相同順序)、方法名稱、類型參數數目和參數數目。
- 宣告中的對應參數應具有相同修飾詞(雖然不一定以相同順序)和相同的類型,或識別可轉換型別(類型參數名稱中的模數差異)。
- 宣告中的對應型別參數應具有相同的條件約束(類型參數名稱中的模數差異)。
實作部分方法宣告可以出現在與對應定義部分方法宣告相同的部分。
只有定義部分方法會參與多載解析。 因此,不論是否指定實作宣告,調用表達式都可以解析為部分方法的調用。 由於部分方法一律會傳 void
回 ,因此這類調用表達式一律會是 expression 語句。 此外,由於部分方法是隱含的 private
,因此這類語句一律會在宣告部分方法之類型宣告的其中一個部分內發生。
注意:比對定義及實作部分方法宣告的定義不需要參數名稱相符。 當使用具名自變數時,這會產生令人驚訝的行為,儘管定義良好。。 例如,假設在一個檔案中定義部分方法宣告
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) {} }
無效,因為調用使用實作中的自變數名稱,而不是定義部分方法宣告。
end note
如果部分類型宣告中沒有任何部分包含指定部分方法的實作宣告,則叫用它的任何表達式語句只會從合併的類型宣告中移除。 因此,調用表達式,包括任何子表達式,在運行時間沒有作用。 部分方法本身也會被移除,而且不會是合併類型宣告的成員。
如果指定部分方法的實作宣告存在,則會保留部分方法的調用。 部分方法會產生類似實作部分方法宣告的方法宣告,但下列情況除外:
partial
不包含修飾詞。產生的方法宣告中的屬性是定義和以未指定順序實作部分方法宣告的結合屬性。 不會移除重複專案。
所產生方法宣告之參數的屬性是定義之對應參數的結合屬性,並以未指定的順序實作部分方法宣告。 不會移除重複專案。
如果針對部分方法 M
提供定義宣告,但未提供實作宣告,則適用下列限制:
從 建立委派
M
時發生編譯時間錯誤(~12.8.17.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)); } } }
end 範例
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 語句都應該指定類型為有效傳回型別的表達式,而且具有呼叫端內容 (~9.7.2) 的 ref 安全內容。
針對傳回值和傳回 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 語句的區塊。end 範例
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 值屬性。 其值具有類型 類型。 這類屬性可以是可讀取和/或可寫入的。
- 第二個宣告 ref 值屬性。 其值是類型變數variable_reference (
readonly
),可能是 。 這種屬性只能讀取。
property_declaration可能包含一組屬性 (~22extern
屬性宣告與方法宣告 ({15.6) 與有效修飾詞組合的規則相同。
member_name (~15.6.1) 會指定屬性的名稱。 除非 屬性是明確的介面成員實作, 否則member_name 只是 標識符。 針對明確的介面成員實作(~18.6.2),member_name包含interface_type後面接著 “” 和.
。
屬性 的類型 至少可以和屬性本身一樣可存取(~7.5.5)。
property_body可能包含語句主體或表達式主體。 在語句主體中,}
宣告 屬性的存取子 ({15.7.3)。 存取子會指定與讀取和寫入 屬性相關聯的可執行語句。
在property_body表達式主體中=>
,由 後面接著表達式和分號所組成的運算式E
主體與語句主體{ get { return E; } }
完全相等,因此只能用來指定只讀屬性,其中 get 存取子的結果是由單一表達式所指定。
property_initializer只能針對自動實作的屬性指定 (~15.7.4),並導致使用表示式所指定值初始化這類屬性的基礎字段。
ref_property_body可能包含語句主體或表達式主體。 在語句主體中, get_accessor_declaration 宣告 屬性的 get 存取子 (~15.7.3)。 存取子會指定與讀取 屬性相關聯的可執行語句。
在ref_property_bodyV
{ get { return ref V; } }
注意:即使存取屬性的語法與字段的語法相同,屬性也不會分類為變數。 因此,除非屬性是 ref 值,否則無法將屬性傳遞為
in
、out
或ref
自變數,因此會傳回變數參考 (~9.7)。 end note
當屬性宣告包含 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 中提供的屬性和索引器之間的差異清單。 end note
屬性 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應宣告比屬性或索引器本身所宣告的輔助功能更嚴格限制的輔助功能。 精確:
- 如果屬性或索引器具有 的宣告輔助功能,則 accessor_modifier 所宣告的輔助功能可以是 、、
public
、private protected
或protected internal
。internal
protected
private
- 如果屬性或索引器具有 的宣告輔助功能,則 accessor_modifier 所宣告的輔助功能可以是 、、
protected internal
、private protected
或protected private
。internal
protected
private
- 如果屬性或索引器具有 或的宣告存取範圍
internal
,則accessor_modifierprotected
宣告的輔助功能應該是 或private protected
。private
- 如果屬性或索引器具有的宣告存取範圍
private protected
,則由 accessor_modifier 宣告的輔助功能應該是private
。 - 如果屬性或索引器具有的宣告存取範圍
private
,則無法使用 任何accessor_modifier 。
- 如果屬性或索引器具有 的宣告輔助功能,則 accessor_modifier 所宣告的輔助功能可以是 、、
針對 abstract
和 extern
非 ref 值屬性,每個指定之存取子的任何 accessor_body 都只是分號。 非抽象的非 extern 屬性,但不是索引器,也可能具有指定之所有存取子的accessor_body分號,在此情況下,它是自動實作的屬性({15.7.4)。 自動實作的屬性至少應該有 get 存取子。 對於任何其他非抽象、非 extern 屬性的存取子, accessor_body 為:
- 區塊,指定要在叫用對應存取子時執行的語句;或
- 表達式主體,後面
=>
接著 表達式 和分號,表示叫用對應存取子時要執行的單一表達式。
針對 abstract
和 extern
ref 值屬性, ref_accessor_body 只是分號。 對於任何其他非抽象、非 extern 屬性的存取子, ref_accessor_body 為:
- 區塊,指定要在叫用 get 存取子時執行的語句;或
- 表達式主體,後面
=>
接著ref
、 variable_reference 和分號。 叫用 get 存取子時,會評估變數參考。
非 ref 值屬性的 get 存取子會對應至具有屬性型別傳回值的無參數方法。 除了指派的目標以外,當表達式中參考這類屬性時,會叫用 get 存取子來計算屬性的值(~12.2.2)。
非 ref-valued 屬性之 get 存取子的主體應符合 \15.6.11 中所述之傳值方法的規則。 特別是, return
get 存取子主體中的所有語句都應該指定可隱含轉換成屬性類型的表達式。 此外,無法連線 get 存取子的端點。
ref 值屬性的 get 存取子會對應至無參數方法,其中傳回值為 屬性類型的變數variable_reference 。 在表達式中參考這類屬性時,會叫用 get 存取子來計算 屬性的variable_reference 值。 該 變數參考,就像任何其他一樣,接著會用來讀取或針對非只讀 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 } }
end 範例
ref-valued 屬性之 get 存取子的主體,應符合 \15.6.11 中所述之 ref 值方法的規則。
set 存取子會對應至具有屬性類型和 void
傳回型別之單一值參數的方法。 set 存取子的隱含參數一律命名為 value
。 當屬性被參考為工作分派的目標時(~12.21),或做為 或 ++
的操作數 –-
(\12.8.16, \12.9.6),則會使用提供新值的自變數來叫用 set 存取子。。 set 存取子的主體應符合 \15.6.11void
之方法的規則。 特別是,不允許 set 存取子主體中的 return 語句指定表達式。 由於 set 存取子隱含具有名為 value
的參數,所以 set 存取子中的局部變數或常數宣告具有該名稱的編譯時間錯誤。
根據 get 和 set 存取子是否存在,屬性分類如下:
- 包含 get 存取子和 set 存取子的屬性據說是讀寫屬性。
- 只有 get 存取子的屬性據說是唯讀屬性。 這是唯讀屬性成為指派目標的編譯時間錯誤。
- 只有 set 存取子的屬性會稱為 唯寫屬性。 除了指派的目標以外,在表達式中參考唯寫屬性是編譯時期錯誤。
注意:前置和後置
++
運算符和--
複合指派運算符無法套用至唯寫屬性,因為這些運算符在寫入新運算符之前先讀取其操作數的舊值。 end note
範例:在下列程式代碼中
public class Button : Control { private string caption; public string Caption { get => caption; set { if (caption != value) { caption = value; Repaint(); } } } public override void Paint(Graphics g, Rectangle r) { // Painting code goes here } }
控件
Button
會宣告公用Caption
屬性。 Caption 屬性的 get 存取子會string
傳回儲存在私人caption
欄位中的 。 set 存取子會檢查新值是否與目前的值不同,如果是,則會儲存新的值並重新繪出控件。 屬性通常會遵循上述模式:get 存取子只會傳回儲存在private
欄位中的值,而 set 存取子會修改該private
欄位,然後執行更新物件狀態所需的任何其他動作。Button
鑒於上述類別,以下是使用 屬性的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; } }
不會宣告單一讀寫屬性。 相反地,它會宣告兩個具有相同名稱的屬性、一個唯讀屬性和一個唯讀屬性。 由於在相同類別中宣告的兩個成員不能有相同的名稱,因此此範例會導致發生編譯時間錯誤。
end 範例
當衍生類別以與繼承屬性相同的名稱宣告屬性時,衍生屬性會隱藏繼承的屬性,同時讀取和寫入。
範例:在下列程式代碼中
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
P
中的B
屬性會在P
讀取和寫入時隱藏 屬性A
。 因此,在語句中B b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
指派會導致
b.P
報告編譯時間錯誤,因為 中的P
只讀B
屬性會隱藏 中的P
唯A
寫屬性。 不過請注意,轉換可用來存取隱藏P
屬性。end 範例
不同於公用欄位,屬性會提供物件內部狀態與其公用介面之間的分隔。
範例:請考慮下列程序代碼,其使用
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
屬性,以及 做為Location
類型的Point
屬性。 如果在未來的 版本中Label
,將位置儲存為Point
內部會變得更加方便,則可以進行變更,而不會影響 類別的公用介面:class Label { private Point location; private string caption; public Label(int x, int y, string caption) { this.location = new Point(x, y); this.caption = caption; } public int X => location.X; public int Y => location.Y; public Point Location => location; public string Caption => caption; }
若
x
和y
是public readonly
欄位,則不可能對Label
類別進行這樣的變更。end 範例
注意:透過屬性公開狀態不一定比直接公開欄位更有效率。 特別是當屬性為非虛擬且只包含少量程式代碼時,執行環境可能會以存取子的實際程式代碼取代對存取子的呼叫。 此程式稱為 內嵌,可讓屬性存取與欄位存取一樣有效率,但會保留屬性增加的彈性。 end note
範例:由於叫用 get 存取子在概念上相當於讀取欄位的值,所以 get 存取子的程式設計樣式會被視為有可觀察副作用的不良程式設計樣式。 在範例中
class Counter { private int next; public int Next => next++; }
屬性的值
Next
取決於先前存取屬性的次數。 因此,存取 屬性會產生可觀察的副作用,而 屬性應該改為實作為方法。get 存取子的「無副作用」慣例並不表示只應該撰寫 get 存取子來傳回儲存在字段中的值。 事實上,get 存取子通常會藉由存取多個字段或叫用方法來計算屬性的值。 不過,正確設計的 get 存取子不會執行任何導致物件狀態可觀察變更的動作。
end 範例
屬性可用來延遲資源的初始化,直到第一次參考它為止。
範例:
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
屬性,則不會為這些裝置建立任何物件。end 範例
15.7.4 自動實作屬性
自動實作的屬性(或簡短的 auto 屬性),是非抽象、非 extern、非 ref 值屬性,且只有 分號accessor_bodys。 自動屬性應該有 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; } }
只讀欄位的工作分派是有效的,因為它們發生在建構函式內。
end 範例
雖然支援欄位是隱藏的,但該欄位可能透過自動實作屬性的 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; } } }
end 範例
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 存取子的內容中也一樣。 相反地,類別無法存取 屬性B.Count
,因此會改用可存取M
的屬性A.Count
。class A { public string Text { get => "hello"; set { } } public int Count { get => 5; set { } } } class B : A { private string text = "goodbye"; private int count = 0; public new string Text { get => text; protected set => text = value; } protected new int Count { get => count; set => count = value; } } class M { static void Main() { B b = new B(); b.Count = 12; // Calls A.Count set accessor int i = b.Count; // Calls A.Count get accessor b.Text = "howdy"; // Error, B.Text set accessor not accessible string s = b.Text; // Calls B.Text get accessor } }
end 範例
選取特定的 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 } }
end 範例
15.7.6 虛擬、密封、覆寫和抽象存取子
注意:這個子句同時適用於屬性(\15.7)和索引器(\15.9)。 子句是以屬性來撰寫,當索引器取代屬性/屬性的索引器/索引器時,請參閱在 \15.9.2 中提供的屬性和索引器之間的差異清單。 end note
虛擬屬性宣告會指定屬性的存取子是虛擬的。 修飾 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 存取子和 set 存取子Y
,其使用base關鍵詞來存取繼承的存取子。 的Z
宣告會覆寫這兩個抽象存取子,因此,中abstract
沒有任何未完成B
的函式成員,而且B
可以成為非抽象類。end 範例
當屬性宣告為覆寫時,覆寫程式代碼可以存取任何覆寫存取子。 此外,屬性或索引器本身和存取子的宣告存取範圍,應符合被覆寫成員和存取子的存取子。
範例:
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 } }
end 範例
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)。
event_declaration可能包含一組屬性 (\22) 和任何一種允許的宣告輔助功能類型 (\15.3.6static
\15.6.3、\15.8.4)、(virtual
、•15.8.5)、 override
(~15.6.5、15.8.5)、(~15.6.6abstract
~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
的編譯時間錯誤。
當事件宣告包含 extern
修飾詞時,事件會稱為 外部事件。 因為外部事件宣告沒有提供實際的實作,所以 extern
它包含 修飾詞和 event_accessor_declaration是錯誤的。
使用 或 修飾abstract
詞包含variable_initializer的事件宣告external
variable_declarator,這是編譯時期錯誤。
事件可以做為 和 +=
運算子的-=
左操作數。 這些運算符分別用來附加事件處理程式,或從事件中移除事件處理程式,以及事件存取修飾詞控制允許這類作業的內容。
由宣告該事件之型別以外的程式代碼所允許的唯一作業是 +=
和 -=
。 因此,雖然這類程式代碼可以新增和移除事件的處理程式,但它無法直接取得或修改事件處理程式的基礎清單。
在窗體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
欄位使用。 如範例所示,欄位可以檢查、修改及用於委派調用表達式。 類別OnClick
「raises」Button
事件中的Click
方法。 引發事件的概念完全等同於叫用該事件所代表的委派項目,因此,就引發事件而言,並沒有任何特殊的語言建構。 請注意,委派調用前面會先進行檢查,以確保委派為非 Null,而且會在本機複本上進行檢查,以確保線程安全。在 類別的
Button
宣告之外,Click
成員只能在 和+=
運算子的–=
左側使用,就像 在 中一樣b.Click += new EventHandler(...);
會將委派附加至事件的調用清單
Click
,以及Click –= new EventHandler(...);
會從事件的調用清單中
Click
移除委派。end 範例
編譯類似欄位的事件時,編譯器將自動建立儲存機制以保存委託,並為該事件建立可存取子以新增或移除事件處理常式到委託欄位。 新增和移除作業是安全線程,而且可以在靜態事件之包含物件的包含物件上保留鎖定時完成 (~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
+=
的 參考–=
會導致叫用 add 和 remove 存取子。 的所有其他參考Ev
都會編譯為參考隱藏字段__Ev
(~12.8.7)。 名稱 「__Ev
是任意的;隱藏的欄位可能具有任何名稱或完全沒有名稱。end note
15.8.3 事件存取子
注意:事件宣告通常會省略 event_accessor_declaration,如上述範例所示
Button
。 例如,如果無法接受每個事件的一個字段的儲存成本,可能會包含它們。 在這種情況下,類別可以包含 event_accessor_declaration,並使用私人機制來儲存事件處理程式清單。 end note
事件的 event_accessor_declarations 會指定與新增和移除事件處理程式相關聯的可執行語句。
存取子宣告是由add_accessor_declaration和remove_accessor_declaration所組成。 每個存取子宣告都包含標記新增或移除,後面接著區塊。 與add_accessor_declaration相關聯的區塊會指定要在加入事件處理程式時執行的語句,而與 remove_accessor_declaration相關聯的區塊會指定要在移除事件處理程式時執行的語句。
每個 add_accessor_declaration 和 remove_accessor_declaration 都會對應至具有事件類型之單一值參數和 void
傳回型別的方法。 事件存取子的隱含參數名為 value
。 當事件指派中使用事件時,會使用適當的事件存取子。 具體來說,如果指派運算符是 +=
,則會使用 add 存取子,如果指派運算符是 –=
,則會使用 remove 存取子。 在任一情況下,指派運算符的右操作數會當做事件存取子的自變數使用。 add_accessor_declaration或remove_accessor_declaration區塊void
特別是, 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 委派值與索引鍵產生關聯並不需要任何成本,因此未處理的事件不會耗用任何記憶體。end 範例
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 值索引器。 其值具有類型 類型。 這種索引器可以是可讀取和/或可寫入的。
- 第二個宣告 ref 值索引器。 其值是類型變數variable_reference (
readonly
),可能是 。 這種索引器只能讀取。
indexer_declaration可能包含一組屬性 (\22) 和任何一種允許的宣告輔助功能類型 (\15.3.6new
\15.3.5)、(\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
索引器宣告不允許修飾詞。
索引器宣告的類型會指定宣告所引進之索引器的項目類型。
注意:當索引器設計成用於類似數位元素的內容時,針對數位定義的字詞 元素類型 也會與索引器搭配使用。 end note
除非索引器是明確的介面成員實作,否則 類型 後面接著 關鍵詞 this
。 對於明確的介面成員實作, 類型 後面接著 interface_type、“.
”和 關鍵詞 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_bodyV
{ get { return ref V; } }
注意:即使存取索引器項目的語法與陣列元素的語法相同,索引器元素也不會分類為變數。 因此,除非索引器是 ref 值,否則不可能將索引器項目傳遞為
in
、out
或ref
自變數,因此會傳回參考 (~9.7)。 end note
索引 器parameter_list 會定義索引器簽章 ({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
和傳統 「sieve」 演演算法來計算介於 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; } } }
end 範例
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_declarations 宣告:
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),而以相同語彙標記 (!
) 表示,則不同。 後者不是可多載運算符。
end note
多載運算符有三種類別:一元運算符(•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)。
end 範例
15.10.3 二元運算符
下列規則適用於二元運算符宣告,其中 T
表示包含運算符宣告的類別或結構實例類型:
- 二進位非移位運算符應採用兩個參數,其中至少一個參數應具有 類型
T
或T?
,而且可以傳回任何類型。 - 二進位
<<
或>>
運算符 (^12.11) 應採用兩個參數,其中第一個參數應具有 typeT
或 T?,而第二個參數應具有 類型int
或int?
,而且可以傳回任何類型。
二元運算子的簽章包含運算子標記 (+
、-
*
/
%
&
|
^
、<<
>>
==
!=
>
<
>=
或 <=
兩個參數的類型。 傳回型別和參數的名稱不是二元運算符簽章的一部分。
某些二進位運算子需要配對宣告。 對於配對之任一運算符的每個宣告,應該有配對之其他運算符的相符宣告。 如果識別轉換存在於其傳回型別與其對應的參數類型之間,則兩個運算符宣告相符。 下列運算子需要配對宣告:
- 運算子
==
和運算子!=
- 運算子
>
和運算子<
- 運算子
>=
和運算子<=
15.10.4 轉換運算元
轉換運算符宣告引進 使用者定義轉換 ({10.5),可增強預先定義的隱含和明確轉換。
包含 關鍵詞的 implicit
轉換運算符宣告引進用戶定義隱含轉換。 隱含轉換可能發生在各種情況下,包括函式成員調用、轉換表達式和指派。 這會在 •10.2 中進一步說明。
包含 關鍵詞的 explicit
轉換運算符宣告引進使用者定義明確轉換。 明確轉換可能發生在轉換表達式中,並在 •10.3 中進一步說明。
轉換運算子會從轉換運算子的參數類型所表示的來源類型轉換成目標類型,以轉換運算符的傳回型別表示。
對於指定的來源類型和S
目標類型,如果 T
或 S
為可為 Null 的實值型T
別,則讓 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
。 end 範例
無法直接重新定義預先定義的轉換。 因此,不允許轉換運算符從 或 object
轉換成 ,因為隱含和明確轉換已存在於 和 所有其他類型之間 object
。 同樣地,轉換的來源和目標類型都不能是另一種轉換的基底類型,因為轉換會已經存在。 不過,在泛型型別上宣告運算符,對於特定型別自變數而言,可以指定已經存在的轉換做為預先定義的轉換。
範例:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
當 type
object
指定為 的T
型別自變數時,第二個運算符會宣告已經存在的轉換(因此,從任何類型到類型物件也有明確的轉換)。end 範例
如果預先定義的轉換存在於兩種類型之間,則會忽略這些類型之間的任何使用者定義的轉換。 具體而言:
- 如果預先定義的隱含轉換 (~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 }
end 範例
不允許使用者定義轉換從 或 轉換成 interface_type。 特別是,這項限制可確保在轉換成interface_type時,不會發生任何使用者定義的轉換,而且object
。
轉換運算子的簽章包含來源類型和目標類型。 (這是傳回類型參與簽章的唯一成員形式。轉換運算子的隱含或明確分類不是運算子簽章的一部分。 因此,類別或結構無法同時宣告具有相同來源和目標類型的隱含和明確轉換運算元。
注意:一般而言,使用者定義隱含轉換的設計應該永遠不會擲回例外狀況,且永遠不會遺失資訊。 如果使用者定義轉換可能會引發例外狀況(例如,因為來源自變數超出範圍)或資訊遺失(例如捨棄高階位),則該轉換應該定義為明確的轉換。 end note
範例:在下列程式代碼中
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
子集。end 範例
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),建構函式的parameter_list中沒有任何參數應包含 this
修飾詞。 參數清單會定義實例建構函式的簽章 ({7.6),並控管進程,其中多載解析 ({12.6.4) 會選取調用中的特定實例建構函式。
實例建構函式parameter_list中所參考的每個型別,至少可以和建構函式本身一樣可存取(~7.5.5)。
選擇性constructor_initializer會指定要叫用的另一個實例建構函式,再執行這個實例建構函式constructor_body中指定的語句。 這會在 •15.11.2 中進一步說明。
當建構函式宣告包含extern
修飾詞時,建構函式會稱為外部建構函式。 由於外部建構函式宣告沒有提供實際實作,因此其 constructor_body 包含分號。 對於所有其他建 構函式,constructor_body 是由任一個建構函式所組成
- 區塊,指定要初始化 類別新實例的語句;或
- 表達式主體,後面
=>
接著 表達式 和分號,並表示單一表達式,以初始化 類別的新實例。
區塊或表達式主體constructor_body對應至void
(~15.6.11)。
實例建構函式不會繼承。 因此,類別除了類別中實際宣告的實例建構函式之外,沒有實例建構函式,但例外狀況是,如果類別不包含任何實例建構函式宣告,則會自動提供預設實例建構函式({15.11.5)。
實例建構函式會由 object_creation_expression s (12.8.17.2) 和 constructor_initializer來叫用。
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() {...}
end note
實例建構函式宣告parameter_list所提供的參數範圍包括該宣告的建構函式初始化表達式。 因此,允許建構函式初始化表達式存取建構函式的參數。
範例:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
end 範例
實例建構函式初始化表達式無法存取所建立的實例。 因此,在建構函式初始化表達式的自變數表達式中參考這個是編譯時期錯誤,因為自變數表達式會透過 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; } }
end 範例
15.11.5 預設建構函式
如果類別不包含實例建構函式宣告,則會自動提供預設實例建構函式。 該預設建構函式只會叫用直接基類的建構函式,就像它有表單 base()
的建構函式初始化表達式一樣。 如果類別是抽象的,則預設建構函式的宣告輔助功能會受到保護。 否則,預設建構函式的宣告輔助功能是公用的。
注意:因此,預設建構函式一律為窗體
protected C(): base() {}
或
public C(): base() {}
其中
C
是類別的名稱。end note
如果多載解析無法判斷基類建構函式初始化表達式的唯一最佳候選專案,則會發生編譯時期錯誤。
範例:在下列程式代碼中
class Message { object sender; string text; }
提供預設建構函式,因為類別不包含實例建構函式宣告。 因此,此範例完全相當於
class Message { object sender; string text; public Message() : base() {} }
end 範例
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可能包含一組屬性(~22extern
static_constructor_declaration的標識碼應命名宣告靜態建構函式的類別。 如果指定任何其他名稱,就會發生編譯時期錯誤。
當靜態建構函式宣告包含 extern
修飾詞時,靜態建構函式會稱為 外部靜態建構函式。 由於外部靜態建構函式宣告沒有提供實際的實作,因此其 static_constructor_body 是由分號所組成。 針對所有其他靜態建構函式宣告, static_constructor_body 是由任一項所組成
- 區塊,指定要執行以初始化 類別的語句;或
- 表達式主體,後面
=>
接著 表達式 和分號,並表示要執行的單一表達式,以初始化 類別。
區塊或表達式主體static_constructor_body對應至傳回型別為 (~15.6.11) 的靜態方法void
method_body。
靜態建構函式不會繼承,而且無法直接呼叫。
封閉類別的靜態建構函式最多會在指定的應用程式域中執行一次。 靜態建構函式的執行是由應用程式域內發生下列第一個事件所觸發:
- 建立類別的實例。
- 會參考 類別的任何靜態成員。
如果類別包含 Main
開始執行的方法 (~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
所觸發。end 範例
建構迴圈相依性,允許以預設值狀態觀察具有變數初始化表達式的靜態字段。
範例:範例
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。end 範例
由於靜態建構函式只針對每個封閉的建構類別類型執行一次,所以對無法透過條件約束 (~15.2.5) 在編譯階段無法檢查的類型參數強制執行運行時間檢查是一個便利的地方。
範例:下列類型會使用靜態建構函式來強制類型自變數為列舉:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
end 範例
15.13 完成項
注意:在此規格的舊版中,現在所謂的「完成項」稱為「解構函式」。 經驗表明,「解構函式」一詞會造成混淆,而且往往導致不正確的預期,特別是程式設計人員知道C++。 在C++中,解構函式是以具決定性的方式呼叫,而在 C# 中,完成項則不是。 若要從 C# 取得判斷行為,應該使用
Dispose
。 end note
「完成項」是實作將類別執行個體完成所需之動作的成員。 完成項是使用 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)。
finalizer_declarator的標識碼應命名宣告完成項的類別。 如果指定任何其他名稱,就會發生編譯時期錯誤。
當完成項宣告包含extern
修飾詞時,完成項會稱為外部完成項。 因為外部完成項宣告沒有提供實際的實作,因此其 finalizer_body 是由分號所組成。 對於所有其他完成項, finalizer_body 包含任一項
- 區塊,指定要執行的語句,以完成 類別的實例。
- 或表達式主體,其包含
=>
後面接著 表達式 和分號,並表示要執行的單一表達式,以便完成 類別的實例。
區塊或表達式主體finalizer_body對應至傳回型別為 (void
) 之實例方法的method_body。
完成項不會繼承。 因此,類別除了可以在該類別中宣告的完成項以外,沒有完成項。
注意:因為完成項不需要參數,所以不能多載,因此類別最多可以有一個完成項。 end note
自動叫用完成項,而且無法明確叫用。 當任何程式代碼都無法使用該實例時,實例就有資格進行最終處理。 實例的完成項執行可能會在實例變成有資格進行最終處理之後,隨時發生 (~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(); } }
is
B's finalizer A's finalizer
因為繼承鏈結中的完成項會依序呼叫,從大部分衍生到最少衍生。
end 範例
完成項是藉由覆寫 上的Finalize
虛擬方法System.Object
來實作。 不允許 C# 程式覆寫此方法,或直接呼叫它(或覆寫它)。
範例:例如,程式
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
包含兩個錯誤。
end 範例
編譯器應該表現得如同這個方法及其覆寫完全不存在一樣。
範例:因此,此程式:
class A { void Finalize() {} // Permitted }
有效且顯示的方法會隱藏
System.Object
的Finalize
方法。end 範例
如需從完成項擲回例外狀況時的行為討論,請參閱 <21.4。
15.14 反覆運算器
15.14.1 一般
使用反覆運算器區塊實作的函式成員 (~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 Yield 類型
反覆運算器會產生一連串的值,這些值全都相同。 此類型稱為反覆運算器的yield類型。
- 傳回
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
之後可能已變更。 end note - 繼續執行反覆運算器區塊后,緊接在 yield return 語句之後,導致暫停執行,並繼續執行,直到執行中斷為止(如下所述)。
- 如果列舉值物件的狀態在 之後,叫
MoveNext
用會傳回 false。
當執行反覆運算器區塊時 MoveNext
,可以透過四種方式中斷執行:透過 yield return
語句、 yield break
透過語句、遇到反覆運算器區塊的結尾,以及擲回並傳播出反覆運算器區塊的例外狀況。
-
yield return
遇到 語句時 (•9.4.4.20):- 會評估 語句中指定的表達式、隱含轉換成 yield 型別,並指派給
Current
列舉值對象的屬性。 - 反覆運算器主體的執行暫停。 所有局部變數和參數的值都會
this
儲存,如同這個yield return
語句的位置一樣。yield return
如果語句位於一或多個try
區塊內,則目前不會執行相關聯的 finally 區塊。 - 列舉值物件的狀態會變更為 暫止。
- 方法
MoveNext
會true
傳回其呼叫端,指出反覆專案已成功前進到下一個值。
- 會評估 語句中指定的表達式、隱含轉換成 yield 型別,並指派給
-
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
:- 將狀態變更為 執行中。
- 執行任何最後區塊,就像上次執行的
yield return
語句是語句一yield break
樣。 如果這會導致擲回並傳播出反覆運算器主體的例外狀況,列舉值物件的狀態會在 之後設定為 ,並將例外狀況傳播至方法的Dispose
呼叫端。 - 將狀態變更為 之後。
- 如果列舉值物件的狀態在 之後,叫
Dispose
用不會有任何影響。
15.14.6 可列舉物件
15.14.6.1 一般
使用反覆運算器區塊實作傳回可列舉介面類型的函式成員時,叫用函式成員不會立即在反覆運算器區塊中執行程序代碼。 相反地,會建立並傳回可列舉的物件。 可列舉物件 GetEnumerator
的方法會傳回列舉值物件,該列舉值物件會封裝反覆運算器區塊中指定的程序代碼,並在叫用列舉值物件的 MoveNext
方法時,在反覆運算器區塊中執行程序代碼。 可列舉的物件具有下列特性:
- 它會實作
IEnumerable
和IEnumerable<T>
,其中T
是反覆運算器的 yield 類型。 - 它會使用傳遞至函式成員的自變數值複本和實例值複本來初始化。
可列舉物件通常是編譯程式產生的可列舉類別的實例,可將程式代碼封裝在反覆運算器區塊中,並實作可列舉的介面,但可能採用其他實作方法。 如果編譯程式產生可列舉類別,則該類別會直接或間接地巢狀於包含函式成員的類別中,其具有私人輔助功能,而且其名稱會保留供編譯程式使用(~6.4.3)。
可列舉的物件可能會實作比上述指定的介面更多的介面。
注意:例如,可列舉的物件也可以實
IEnumerator
作 和IEnumerator<T>
,讓它同時做為可列舉和列舉值。 一般而言,這類實作會從第一次呼叫GetEnumerator
傳回自己的實例(以儲存配置)。 後續的調用GetEnumerator
,如果有的話,會傳回新的類別實例,通常是同一個類別,因此對不同列舉值實例的呼叫不會影響彼此。 即使先前的列舉值已經列舉超過序列結尾,它也無法傳回相同的實例,因為所有未來呼叫已耗盡的列舉值都必須擲回例外狀況。 end note
15.14.6.2 GetEnumerator 方法
可列舉物件提供和 GetEnumerator
介面方法的IEnumerable
IEnumerable<T>
實作。 這兩 GetEnumerator
種方法會共用一個通用實作,這個實作會取得並傳回可用的列舉值物件。 列舉值物件會使用初始化可列舉物件時所儲存的自變數值和實例值初始化,否則列舉值物件函式如 \15.14.5 中所述。
15.15 異步函式
15.15.1 一般
具有 修飾詞的方法 (~15.6) 或匿名函式 (~12.19) 稱為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() { ... } }
end 範例
工作產生器類型是對應至特定工作類型的類別或結構類型(~15.15.2)。 工作產生器類型應該完全符合其對應工作類型的宣告存取範圍。
注意: 如果宣告
internal
工作類型,則對應的產生器類型也必須宣告internal
並在相同的元件中定義。 如果工作類型是巢狀於另一個類型內,工作浮點類型也必須巢狀於該相同類型中。 end note
異步函式能夠藉由 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
- 建置者應該在
stateMachine.MoveNext()
或 之後Start()
呼叫Start()
,以推進狀態機器。
- 建置者應該在
- 傳回之後
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()
應該使用awaiter.UnsafeOnCompleted(action)
Action
等候程式完成時呼叫stateMachine.MoveNext()
的 。
-
- 否則,狀態機器會叫用
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
。-
AwaitOnCompleted()
應該使用awaiter.OnCompleted(action)
Action
等候程式完成時呼叫stateMachine.MoveNext()
的 。
-
-
SetStateMachine(IAsyncStateMachine)
編譯程式產生的IAsyncStateMachine
實作可能會呼叫,以識別與狀態機器實例相關聯的產生器實例,特別是針對狀態機器實作為實作實作為實值型別的情況。- 如果產生器呼叫
stateMachine.SetStateMachine(stateMachine)
,則會stateMachine
在與builder.SetStateMachine(stateMachine)
相關聯的產生器實例上呼叫stateMachine
。
- 如果產生器呼叫
注意:針對
SetResult(T result)
和«TaskType»<T> Task { get; }
,參數和自變數必須分別可轉換成 的識別T
。 這可讓工作類型產生器支援 Tuple 之類的類型,其中兩個不同類型是可轉換的身分識別。 end note
15.15.3 工作傳回異步函式的評估
叫用工作傳回異步函式會導致產生傳回之工作類型的實例。 這稱為 異步函式的傳回工作 。 工作一開始處於 不完整 的狀態。
接著會評估異步函式主體,直到暫停為止(藉由到達 await 運算式)或終止,此時控件會傳回給呼叫端,以及傳回工作。
異步函式的主體終止時,傳回工作會移出不完整的狀態:
- 如果函式主體因為到達 return 語句或主體結尾而終止,則會在傳回工作中記錄任何結果值,而該工作會放入 成功 狀態。
- 如果函式主體因為未攔截
OperationCanceledException
而終止,則會在已 取消 狀態的傳回工作中記錄例外狀況。 - 如果函式主體因任何其他未攔截的例外狀況 (~13.10.6) 而終止,則會在進入 錯誤 狀態的傳回工作中記錄例外狀況。
15.15.4 評估 void 傳回異步函式
如果異步函式的傳回類型是 void
,則評估與上述方法不同:因為不會傳回任何工作,因此函式會改為將完成和例外狀況傳達給目前線程的同步處理內容。 同步處理內容的確切定義與實作相關,但表示法是目前線程正在執行的位置。 評估傳回異步函式時,會通知 void
同步處理內容、順利完成,或導致擲回未攔截的例外狀況。
這可讓內容追蹤 void
有多少傳回異步函式正在其下執行,並決定如何傳播出它們的例外狀況。