16.1 一般
結構與類別類似,因為它們代表可包含數據成員和函式成員的數據結構。 不過,不同於類別,結構是實值型別,不需要堆積配置。 型別為struct的變數直接包含struct的數據,而類別型別的變數則包含對數據的參考,後者稱為物件。
注意:結構特別適用於具有值語意的小型數據結構。 複數、座標系統中的點或字典中的索引鍵/值組都是結構的良好範例。 這些資料結構的關鍵在於,它們通常只有很少的數據成員,不需要使用繼承或引用語意,而是可以方便地使用值語意,讓指派是複製值而不是引用。 結束註解
如§8.3.5中所述,C#提供的簡單類型,例如int、double和bool,實際上都是結構類型。
16.2 結構宣告
16.2.1 一般
結構聲明 是一個 類型宣告(§14.7),用於宣告一個新的結構。
struct_declaration
: attributes? struct_modifier* 'ref'? 'partial'? 'struct'
identifier type_parameter_list? struct_interfaces?
type_parameter_constraints_clause* struct_body ';'?
;
struct_declaration包含一組選擇性屬性 (§23) ,後面接著一組選擇性struct_modifiers (§16.2.2) ,後面接著選擇性ref修飾詞 (§16.2.3) ,後面接著選擇性部分修飾詞 (§15.2.7) ,後面接著struct關鍵字和命名結構的標識符,後面接著選擇性 type_parameter_list 規格 (§15.2.3),後面接著選擇性struct_interfaces規格 (§16.2.5),後面接著選擇性的type_parameter_constraints子句規格 (§15.2.5),後面接著struct_body (§16.2.6),後面接著選擇性的分號。
除非結構宣告也提供 type_parameter_list,否則結構宣告不得提供 type_parameter_constraints_clause。
提供 type_parameter_list 的結構宣告是泛型結構宣告。 此外,泛型類別宣告或泛型結構宣告內巢狀的任何結構本身都是泛型結構宣告,因為應該提供包含型別的型別自變數來建立建構型別 ({8.4)。
包含ref關鍵詞的結構宣告不應有struct_interfaces部分。
16.2.2 結構修飾詞
struct_declaration可以選擇性地包含一連串struct_modifier。
struct_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'readonly'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§24.2) 僅適用於不安全代碼 (§24)。
同一個修飾詞在結構宣告中出現多次是編譯時期錯誤。
readonly除了 之外,結構宣告的修飾詞與類別宣告的修飾詞意義相同({15.2.2)。
readonly修飾詞表示struct_declaration宣告實例不可變的類型。
唯讀結構具有下列條件約束:
- 每個實體欄位也應該宣告為
readonly。 - 它不得宣告任何類似欄位的事件(§15.8.2)。
當只讀結構實例傳遞至方法時,其 this 會被視為輸入參數,即不允許寫入任何實例欄位(除了建構函式除外)。
16.2.3 Ref 修飾詞
ref修飾詞表示struct_declaration宣告在執行堆疊上配置實例的類型。 這些類型稱為 ref 結構 類型。
ref修飾詞會宣告實例可能包含類似 ref 的欄位,且不得從其安全內容複製(~16.4.15)。 判斷 ref 結構安全內容的規則描述於 \16.4.15 中。
如果在下列任一內容中使用 ref 結構類型,則為編譯時期錯誤:
- 作為陣列的元素類型。
- 做為類別或結構中沒有
ref修飾詞之欄位的宣告型別。 - 被限制在
System.ValueType或System.Object。 - 作為類型參數。
- 作為 Tuple 元素的型別。
- 異步方法。
- 迭代器。
- 不會從
ref struct型別轉換成 型object別或型別System.ValueType。 -
ref struct類型不得宣告為實作任何介面。 - 在
object或System.ValueType中宣告但未在ref struct型別中覆寫的實例方法,不得以該ref struct型別的接收者呼叫。 - 某
ref struct類型的實例方法不應該被方法群組轉換為委派類型所捕獲。 - lambda 運算式或區域函式不得捕捉 ref 結構。
注意:不應該
ref struct宣告async實例方法,也不會在yield return實例方法中使用 或yield break語句,因為隱含this參數不能用於這些內容中。 結束註解
這些條件約束可確保 類型的 ref struct 變數不會參考不再有效的堆疊記憶體,或參考不再有效的變數。
16.2.4 部分修飾詞
修飾詞 partial 表示這個 struct_declaration 是部分類型宣告。 多個在封入命名空間或類型宣告內具有相同名稱的部分結構宣告會結合成一個結構宣告,並遵循在 \15.2.7 中指定的規則。
16.2.5 結構介面
結構宣告可能包含 struct_interfaces 規格,在此情況下,結構據說會直接實作指定的介面類型。 針對建構的結構類型,包括在泛型型別宣告中宣告的巢狀類型(§15.3.9.7),每個實作的介面類型都是通過在所指定的介面中用建構型別的對應type_argument 替代每個type_parameter 來取得。
struct_interfaces
: ':' interface_type_list
;
在多個部分的部分結構宣告(§15.2.7)中介面的處理方式,會在§15.2.4.3中進一步討論。
第 19.6 節進一步討論介面實作。
16.2.6 結構主體
結構 struct_body 會定義結構的成員。
struct_body
: '{' struct_member_declaration* '}'
;
16.3 結構成員
結構的成員包含其 struct_member_declaration所引進的成員,以及繼承自 類型 System.ValueType的成員。
struct_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| static_constructor_declaration
| type_declaration
| fixed_size_buffer_declaration // unsafe code support
;
fixed_size_buffer_declaration (§24.8.2) 僅適用於不安全代碼 (§24)。
注意:各種 class_member_declaration,除了 finalizer_declaration 以外,也都是 struct_member_declaration。 結束註解
除了在 \16.4 中指出的差異之外,在 \15.3 到 \15.12 中提供的類別成員描述也適用於結構成員。
16.4 類別和結構差異
16.4.1 一般
結構與類別在幾個重要方面上有所不同:
- 結構是實值型別(~16.4.2)。
- 所有結構類型都會隱含繼承自 類別
System.ValueType(~16.4.3)。 - 結構類型的變數指派會 建立所指派值的複本 (~16.4.4)。
- 結構的預設值是藉由將所有字段設定為預設值 (\16.4.5) 所產生的值。
- Boxing 和 unboxing 作業可用來在結構類型與特定參考型別之間轉換(§16.4.6)。
- 在結構成員中的意義
this不同(~16.4.7)。 - 不允許結構實例字段宣告包含變數初始化表達式 ({16.4.8)。
- 不允許結構宣告無參數實例建構函式(~16.4.9)。
- 結構體不得宣告終結器。
- 事件宣告、屬性宣告、屬性存取子、索引器宣告和方法宣告可以有修飾詞
readonly,而這些相同成員類型在類別中通常不允許這樣做。
16.4.2 值語意
結構是實值型別(~8.3),據說具有值語意。 另一方面,類別是參考型別(~8.2),據說具有參考語意。
結構類型的變數直接包含結構的數據,而類別類型的變數則包含包含數據的 對象參考。 當結構B包含類型A的實例字段,而A是結構類型時,如果A依賴於B或來自B建構的類型,則會產生編譯時期錯誤。 如果 X 包含類型為 的實例欄位,則結構 YX。 鑒於此定義,結構所依賴的完整結構集是「直接依賴於」關係的傳遞閉包。
範例:
struct Node { int data; Node next; // error, Node directly depends on itself }是錯誤,因為
Node包含其本身類型的實例欄位。 另一個範例struct A { B b; } struct B { C c; } struct C { A a; }是錯誤,因為每個類型
A、B和C相依於彼此。結束 範例
使用類別時,兩個變數可以參考相同的物件,因此一個變數上的作業可能會影響另一個變數所參考的物件。 使用結構體時,變數各自擁有自己的數據副本(除了以引用參數的情況外),因此在一個變數上的操作不會影響到另一個變數。 此外,除非明確可為 Null (~8.3.12),否則結構類型的值不可能是 null。
注意:如果結構包含參考類型的字段,則其他作業可以改變參考之對象的內容。 不過,欄位本身的值,也就是它所參考的物件,無法透過不同結構值的突變來變更。 結束註解
範例:給定下列
struct Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } class A { static void Main() { Point a = new Point(10, 10); Point b = a; a.x = 100; Console.WriteLine(b.x); } }輸出為
10。 指派a給b時會建立值的複本,因此b不會受到指派給a.x的影響。 若Point被宣告為類別,輸出將為100,因為a和b将參考相同的物件。結束 範例
16.4.3 繼承
所有結構型別會隱含地繼承自類別 System.ValueType,而類別 System.ValueType 則會繼承自類別 。 結構宣告可以指定實作介面的清單,但結構宣告不可能指定基類。
結構體永遠不是抽象的,並且總是隱含封閉的。 因此,abstract 和 sealed 修飾詞在結構宣告中不被允許。
由於結構不支援繼承,所以結構成員的宣告存取範圍不可以是 protected、 private protected或 protected internal。
結構中的函式成員不可以是抽象或虛擬的,而且 override 只允許修飾詞覆寫繼承自 System.ValueType的方法。
16.4.4 工作分派
結構類型的變數指派會 建立所指派值的複本 。 這與類別類型的變數指派不同,該變數會複製參考,但不會複製參考所識別的物件。
類似於指派,當結構當做值參數傳遞或當做函式成員的結果傳回時,就會建立結構的複本。 結構可以透過傳址參數,以參考方式傳遞至函式成員。
當結構的屬性或索引器是指派的目標時,與屬性或索引器存取相關聯的實例表達式應分類為變數。 如果實例表達式分類為值,就會發生編譯時期錯誤。 這在 §12.23.2 中進行了更詳細的描述。
16.4.5 預設值
如 \9.3 中所述,在建立變數時,會自動初始化數種變數的預設值。 對於類別型別和其他參考型別的變數,這個預設值為 null。 不過,由於結構是不能 null的實值型別,所以結構的預設值是將所有實值型別欄位設定為預設值,並將所有參考型別字段設定為 null所產生的值。
範例:參考
Point上述宣告的結構,此範例Point[] a = new Point[100];將陣列中的每個
Point初始化為將x和y欄位設定為零所產生的值。結束 範例
結構的預設值會對應至結構的預設建構函式所傳回的值(~8.3.3)。 不同於類別,結構不允許宣告無參數實例建構函式。 相反地,每個結構都會隱含地具有無參數實例建構函式,一律會傳回所有欄位設定為其預設值所產生的值。
注意:結構的設計應考慮預設初始化狀態為有效的狀態。 在範例中
struct KeyValuePair { string key; string value; public KeyValuePair(string key, string value) { if (key == null || value == null) { throw new ArgumentException(); } this.key = key; this.value = value; } }用戶定義的實例建構函式只有在被明確呼叫時,才會防止
null的值造成問題。 在變數受限於預設值初始化的情況下KeyValuePair,key和value欄位將會是null,而結構應該準備好處理此狀態。結束註解
16.4.6 裝箱和拆箱
類別類型的值可以轉換成類型 object ,或轉換成類別所實作的介面類型,只要在編譯階段將參考視為另一種類型即可。 同樣地,類型 object 的值或介面類型的值可以轉換回類別類型,而不需變更參考(但在此案例中,需要執行時類型檢查)。
由於結構不是參考型別,因此這些作業會針對結構類型以不同的方式實作。 當結構類型的值轉換成特定參考型別時(如 §10.2.9 中所定義),就會進行裝箱作業。 同樣地,當特定參考型別的值(如第10.3.7節中所定義)轉換為結構類型時,就會進行拆箱操作。 與類別類型相同的作業相比,主要差異在於boxing和unboxing會將結構值複製至Boxed實例或從Boxed實例取出。
注意:因此,在進行 boxing 或 unboxing 操作後,對未封箱的
struct所做的更改不會反映在已封箱的struct中。 結束註解
如需了解封箱和拆箱的更多詳情,請參閱 §10.2.9 和 §10.3.7。
16.4.7 此的意義
結構中 的意義this與 類別中 的意義this不同,如 •12.8.14 中所述。 當結構類型覆寫繼承自 System.ValueType 的虛擬方法(例如 Equals、 GetHashCode或 ToString),透過結構類型的實例叫用虛擬方法時,不會造成 Boxing 發生。 即使結構當做類型參數使用,而且會透過類型參數類型的實例進行調用,也是如此。
範例:
struct Counter { int value; public override string ToString() { value++; return value.ToString(); } } class Program { static void Test<T>() where T : new() { T x = new T(); Console.WriteLine(x.ToString()); Console.WriteLine(x.ToString()); Console.WriteLine(x.ToString()); } static void Main() => Test<Counter>(); }程式輸出為:
1 2 3雖然
ToString有副作用是不良的風格,但此範例顯示呼叫x.ToString()的三次操作中沒有發生封箱。結束 範例
同樣地,當成員實作於實作實值型別時,在限制型別參數上存取成員時,永遠不會隱含地發生Boxing。 例如,假設介面 ICounter 包含方法 Increment,可用來修改值。 如果使用 ICounter 作為限制條件,那麼會以Increment所呼叫的變數作為參考來呼叫Increment方法的實作,絕不會呼叫封裝的複本。
範例:
interface ICounter { void Increment(); } struct Counter : ICounter { int value; public override string ToString() => value.ToString(); void ICounter.Increment() => value++; } class Program { static void Test<T>() where T : ICounter, new() { T x = new T(); Console.WriteLine(x); x.Increment(); // Modify x Console.WriteLine(x); ((ICounter)x).Increment(); // Modify boxed copy of x Console.WriteLine(x); } static void Main() => Test<Counter>(); }第一次呼叫 ,修改
Increment變數x中的值。 這與第二次呼叫Increment不相等,因為它修改了x的封裝複本中的值。 因此,程序的輸出為:0 1 1結束 範例
16.4.8 欄位初始化
如 §16.4.5 中所述,結構的預設值是由將所有值類型欄位設定為預設值,以及所有參考類型欄位設為null所形成的結果。 因此,結構不允許實例字段宣告包含變數初始化表達式。 此限制僅適用於實例欄位。 允許結構體的靜態欄位包含變數初始化器。
範例:如下
struct Point { public int x = 1; // Error, initializer not permitted public int y = 1; // Error, initializer not permitted }發生錯誤,因為實例欄位宣告包含變數初始化表達式。
結束 範例
直接在具有struct_modifier的struct_declaration內宣告的readonly應具有field_modifierreadonly。
16.4.9 建構函式
不同於類別,結構不允許宣告無參數實例建構函式。 相反地,每個結構都會隱含地具有無參數實例建構函式,一律會傳回所有實值型別字段設為其預設值所產生的值,並將所有參考類型欄位 null 設為 (~8.3.3)。 結構可以宣告具有參數的實例建構函式。
範例:給定下列
struct Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } class A { static void Main() { Point p1 = new Point(); Point p2 = new Point(0, 0); } }語句都會建立一個
Point,其中x和y都初始化為零。結束 範例
不允許結構體實例建構子包含形式為base(argument_list)的建構子初始化,其中 argument_list 是可選的。
結構 this 實例建構函式的參數會對應至結構類型的輸出參數。 因此,this 應該在建構函式返回的每個位置,確定指派(§9.4)。 同樣地,在確定指派之前,即使是隱含的也無法在建構函式主體中讀取它。
如果結構實例建構函式指定建構函式初始化表達式,該初始化表達式會被視為在建構函式主體之前發生的明確指派。 因此,系統本身不需要初始化要求。
範例:請考慮下列實例建構函式實作:
struct Point { int x, y; public int X { set { x = value; } } public int Y { set { y = value; } } public Point(int x, int y) { X = x; // error, this is not yet definitely assigned Y = y; // error, this is not yet definitely assigned } }除非已明確指派建構結構的所有欄位,否則無法呼叫實例函式成員(包括屬性
X和Y的 set 存取子)。 不過請注意,如果Point類別不是結構,則會允許實例建構函式實作。 這有一個例外狀況,這牽涉到自動實作的屬性(~15.7.4)。 確定指派規則 (§12.23.2) 特別豁免指派給該結構類型的實例建構函式內結構類型的自動屬性:這類指派會被視為自動屬性隱藏支援欄位的明確指派。 因此,允許下列事項:struct Point { public int X { get; set; } public int Y { get; set; } public Point(int x, int y) { X = x; // allowed, definitely assigns backing field Y = y; // allowed, definitely assigns backing field } }結束範例]
16.4.10 靜態建構函式
結構靜態建構函式會遵循與類別相同的大部分規則。 結構類型的靜態建構函式執行是由應用程式域內發生下列第一個事件所觸發:
- 參考結構類型的靜態成員。
- 結構型別的明確宣告的建構函式被呼叫。
注意:建立結構類型的預設值 (~16.4.5) 不會觸發靜態建構函式。 (其中一個範例是陣列中元素的初始值。)end note
16.4.11 屬性
在struct_declaration中,實例屬性的property_declaration(§15.7.1)可能包含property_modifierreadonly。 不過,靜態屬性不得包含該修飾詞。
嘗試透過該結構中宣告的唯讀屬性修改實例結構變數的狀態,這是編譯時期錯誤。
自動實作屬性在編譯時期會出現錯誤,如果它同時具有 readonly 修飾詞和 set 存取子。
自動實作的屬性若在 readonly 結構中具有 set 存取子,則會導致編譯時錯誤。
在一個結構中宣告的自動實作屬性不需要有修飾詞 readonly,因為其存取子 readonly 被隱含地假設為只讀。
在屬性本身,以及它的readonly或get存取子上都有set修飾詞,是編譯時期的錯誤。
屬性若在其所有的存取子上都具有唯讀修飾詞,會導致編譯時錯誤。
注意:若要更正錯誤,請將 修飾詞從存取子移至屬性本身。 結束註解
針對屬性存取子表示式, s.P:
- 如果在
s.PM中的進程會建立 的暫存複本T時叫用 型別的 set 存取子s,則為編譯時期錯誤。 - 如果
s.P叫用 類型的Tget 存取子,則會遵循 \12.6.6.1 中的進程,包括視需要建立 的s暫存複本。
自動實作的屬性(§15.7.4)會使用隱藏的後備欄位,這些欄位只能由屬性存取子存取。
注意:此存取限制表示包含自動實作屬性之結構中的建構函式通常需要明確建構函式初始化表達式,否則它們不需要一個,以滿足叫用任何函式成員或建構函式傳回之前明確指派的所有字段需求。 結束註解
16.4.12 方法
一個 struct_declaration 中實例方法的 method_declaration(§15.6.1)可能會包含 method_modifierreadonly。 不過,靜態方法不得包含該修飾詞。
嘗試透過該結構中宣告的唯讀方法修改實例結構變數的狀態,這是編譯時期錯誤。
雖然只讀方法可能會呼叫同層級的非唯讀方法、屬性或索引器的 get 存取子,但這樣做會促使建立 this 的隱含複本,以防止潛在的副作用。
readonly 方法可能會呼叫唯讀的同層級屬性或索引器 set 存取子。 如果同層級成員的存取子不是明確或隱含唯讀的,就會發生編譯錯誤。
所有部分方法的 method_declaration 都應該有 readonly 修飾詞,或者所有都不應該有。
16.4.13 索引器
在struct_declaration的實例索引器中,indexer_declaration(§15.9)可能包含indexer_modifierreadonly。
嘗試透過該結構中宣告的唯讀索引器修改實例結構變數的狀態,這是編譯時期錯誤。
在索引器本身以及其 readonly 或是 get 存取子上使用set修飾詞為編譯時錯誤。
索引器在所有存取子上都有只讀修飾詞,這是編譯時期錯誤。
注意:若要更正錯誤,請將 修飾詞從存取子移至索引器本身。 結束註解
16.4.14 事件
實例的 event_declaration (~15.8.1), struct_declaration 中的非字段式事件可能包含 event_modifierreadonly。 不過,靜態事件不得包含該修飾詞。
16.4.15 安全內容條件約束
16.4.15.1 一般
在編譯階段,每個表達式都會與一個上下文相關聯,其中該實例及其所有欄位都可以安全地存取,這就是其安全環境。 安全環境是可以包容某個表達式的環境,在這個環境中,值可以安全地被傳遞。
編譯時期類型不是 ref 結構的任何運算式,都具有呼叫者內容的安全上下文。
任何類型的 default 表達式都取決於呼叫端內容的安全上下文。
對於任何編譯時間類型為 ref 結構的非預設表達式,其具有下列各節所定義的安全內容。
安全上下文記錄指出值可以被複製到哪些上下文中。 如果將具有安全內容E1的表達式S1指派給具有安全內容E2的表達式S2,而S2比S1具有更廣泛的內容,則這是錯誤的。
有三個不同的安全內容值,與針對參考變數定義的 ref-safe-context 值相同({9.7.2): declaration-block、 function-member 和 caller-context。 表達式的安全內容會限制其用法,如下所示:
- 對於 return 語句
return e1的安全上下文e1應該是呼叫者上下文。 - 指派
e1 = e2的安全情境e2至少應與安全情境e1一樣寬。
對於一個方法調用,如果有ref或out參數的ref struct類型(包括接收者,除非類型是readonly),並且具有安全上下文S1,那麼任何參數(包括接收者)都不可能有比S1更窄的安全上下文。
16.4.15.2 參數安全情境
ref 結構類型的參數,包含實例方法中的 this 參數,會在呼叫方上下文中具有安全上下文。
16.4.15.3 局部變數安全內容
ref 結構類型的局部變數具有安全內容,如下所示:
- 如果變數是迴圈的
foreach反覆專案變數,則變數的安全內容與迴圈表達式的安全內容foreach相同。 - 否則,如果變數的宣告具有初始化表達式,則變數的安全內容與該初始化表達式的安全內容相同。
- 否則,變數在宣告點未初始化,且具有呼叫端內容的安全內容。
16.4.15.4 欄位安全上下文
欄位 e.F 的參考,其中類型 F 是 ref 結構型別,具有與 e 相同的安全內容。
16.4.15.5 運算符
使用者定義運算子的應用程式會被視為方法調用(^16.4.15.6)。
對於產生值,例如 e1 + e2 或 c ? e1 : e2的運算符,結果的安全內容是運算符之安全內容中最窄的內容。 因此,對於產生值的一元運算符,例如 +e,結果的安全內容是操作數的安全內容。
注意:條件運算符的第一個操作數是
bool,因此其安全內容是呼叫端內容。 因此,所得的安全上下文是第二和第三個運算元中最狹窄的安全上下文。 結束註解
16.4.15.6 方法和屬性調用
由方法調用 e1.M(e2, ...) 或屬性調用 e.P 所產生的值,其安全上下文為以下上下文中最小者:
- 呼叫者-語境。
- 所有引數表達式的安全環境(包括接收者)。
上述規則會將屬性調用 (或 getset) 視為基礎方法的方法調用。
16.4.15.7 堆疊分配
stackalloc 運算式的結果具有 function-member 的安全內容。
16.4.15.8 建構函式調用
new 調用建構函式的表達式會遵守與被視為傳回所建構之型別的方法調用相同的規則。
此外,安全環境是所有物件初始化表達式中各自的參數和運算元的最小安全環境,若有任一初始化表達式存在,則遞迴計算之。
注意:這些規則依賴
Span<T>不具有以下形式的建構函式:public Span<T>(ref T p)這類建構函式會使
Span<T>實例與ref欄位無法區分。 本檔所述的安全規則取決於ref字段在 C# 或 .NET 中不是有效的建構。 結束註解