16 結構
16.1 一般
結構與類別類似,因為它們代表可包含數據成員和函式成員的數據結構。 不過,不同於類別,結構是實值型別,不需要堆積配置。 型別的 struct
變數直接包含 的數據 struct
,而類別類型的變數則包含數據的參考,後者稱為物件。
注意:結構特別適用於具有值語意的小型數據結構。 複數、座標系統中的點或字典中的索引鍵/值組都是結構的良好範例。 這些數據結構的索引鍵是,它們很少有數據成員,它們不需要使用繼承或參考語意,而是可以使用指派複製值而非參考的值語意來方便實作。 end note
如 •8.3.5 中所述,C# 所提供的簡單型別,例如 int
、 double
和 bool
,實際上都是所有結構類型。
16.2 結構宣告
16.2.1 一般
struct_declaration是宣告新結構的type_declaration (•14.7) :
struct_declaration
: attributes? struct_modifier* 'ref'? 'partial'? 'struct'
identifier type_parameter_list? struct_interfaces?
type_parameter_constraints_clause* struct_body ';'?
;
struct_declaration包含一組選擇性的屬性 (22),後面接著一組選擇性的 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_clauses。
提供 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 (•23.2) 僅適用於不安全的程式代碼 (~23)。
同一個修飾詞在結構宣告中出現多次是編譯時期錯誤。
readonly
除了 之外,結構宣告的修飾詞與類別宣告的修飾詞意義相同({15.2.2)。
readonly
修飾詞表示struct_declaration宣告實例不可變的類型。
唯讀結構具有下列條件約束:
當只讀結構實例傳遞至方法時,其 this
會被視為輸入自變數/參數,不允許寫入任何實例字段的存取權(除了建構函式除外)。
16.2.3 Ref 修飾詞
ref
修飾詞表示struct_declaration宣告在執行堆疊上配置實例的類型。 這些類型稱為 ref 結構 類型。 ref
修飾詞會宣告實例可能包含類似 ref 的欄位,且不得從其安全內容複製 (~16.4.12)。 判斷 ref 結構安全內容的規則描述於 \16.4.12 中。
如果在下列任一內容中使用 ref 結構類型,則為編譯時期錯誤:
- 做為陣列的項目類型。
- 做為類別之欄位的宣告型別,或是沒有
ref
修飾詞的結構。 - 已將 Boxed 設為
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
參數不能用於這些內容中。 end note
這些條件約束可確保 類型的 ref struct
變數不會參考不再有效的堆疊記憶體,或參考不再有效的變數。
16.2.4 部分修飾詞
修飾 partial
詞表示這個 struct_declaration 是部分類型宣告。 多個在封入命名空間或類型宣告內具有相同名稱的部分結構宣告會結合成一個結構宣告,並遵循在 \15.2.7 中指定的規則。
16.2.5 結構介面
結構宣告可能包含 struct_interfaces 規格,在此情況下,結構據說會直接實作指定的介面類型。 針對建構的結構類型,包括泛型型別宣告內宣告的巢狀類型({15.3.9.7),每個實作的介面類型都是藉由替代 所指定介面中的每個type_parameter 來取得,這是建構型別的對應 type_argument 。
struct_interfaces
: ':' interface_type_list
;
在部分結構宣告({15.2.7)的多個部分上,會進一步 討論介面的處理方式。{15.2.4.3。
介面實作會在 \18.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 (•23.8.2) 僅適用於不安全的程式代碼 (~23)。
注意:除了 finalizer_declaration 以外的各種class_member_declaration,也會struct_member_declarations。 end note
除了在 \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)。
- 不允許結構宣告完成項。
16.4.2 值語意
結構是實值型別(~8.3),據說具有值語意。 另一方面,類別是參考型別(~8.2),據說具有參考語意。
結構類型的變數直接包含結構的數據,而類別類型的變數則包含包含數據的 對象參考。 當結構B
包含 類型的A
實例字段,而且A
是結構類型時,它是相依B
於的編譯時期錯誤A
,或從B
建構的類型。 A struct X
如果 X
包含類型的Y
實例欄位,則直接相依於結構Y
。 鑒於此定義,結構相依的完整結構集是直接相依於關聯性的可轉移性關閉。
範例:
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
相依於彼此。end 範例
使用類別時,兩個變數可以參考相同的物件,因此一個變數上的作業可能會影響另一個變數所參考的物件。 使用 結構時,變數各有自己的數據複本(除了參考參數的情況除外),而且無法讓一個上的作業影響另一個。 此外,除非明確可為 Null (~8.3.12),否則結構類型的值不可能是 null
。
注意:如果結構包含參考類型的字段,則其他作業可以改變參考之對象的內容。 不過,欄位本身的值,也就是它所參考的物件,無法透過不同結構值的突變來變更。 end note
範例:假設有下列專案
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
會參考相同的物件。end 範例
16.4.3 繼承
所有結構型別都會隱含繼承自 類別 ,接著會繼承自 類別 System.ValueType
object
。 結構宣告可以指定實作介面的清單,但結構宣告不可能指定基類。
結構類型絕不是抽象的,而且一律會隱含密封。 因此,結構 abstract
宣告中不允許 和 sealed
修飾詞。
由於結構不支援繼承,所以結構成員的宣告存取範圍不可以是 protected
、 private protected
或 protected internal
。
結構中的函式成員不可以是抽象或虛擬的,而且 override
只允許修飾詞覆寫繼承自 System.ValueType
的方法。
16.4.4 工作分派
結構類型的變數指派會 建立所指派值的複本 。 這與類別類型的變數指派不同,該變數會複製參考,但不會複製參考所識別的物件。
類似於指派,當結構當做值參數傳遞或當做函式成員的結果傳回時,就會建立結構的複本。 結構可以使用傳址參數,以傳址方式傳遞至函式成員。
當結構的屬性或索引器是指派的目標時,與屬性或索引器存取相關聯的實例表達式應分類為變數。 如果實例表達式分類為值,就會發生編譯時期錯誤。 這會在 •12.21.2 中進一步詳細說明。
16.4.5 預設值
如 \9.3 中所述,在建立變數時,會自動初始化數種變數的預設值。 對於類別型別和其他參考型別的變數,這個預設值為 null
。 不過,由於結構是不能 null
的實值型別,所以結構的預設值是將所有實值型別欄位設定為預設值,並將所有參考型別字段設定為 null
所產生的值。
範例:參考
Point
上述宣告的結構,此範例Point[] a = new Point[100];
將
Point
與y
欄位設定x
為零所產生的值,初始化陣列中的每個 。end 範例
結構的預設值會對應至結構的預設建構函式所傳回的值(~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
,而結構應該準備好處理此狀態。end note
16.4.6 Boxing 和 unboxing
類別類型的值可以轉換成類型 object
,或轉換成類別所實作的介面類型,只要在編譯階段將參考視為另一種類型即可。 同樣地,型 object
別的值或介面類型的值可以轉換成類別類型,而不需變更參考(但在此案例中,需要運行時間類型檢查)。
由於結構不是參考型別,因此這些作業會針對結構類型以不同的方式實作。 當結構類型的值轉換成特定參考型別時(如 \10.2.9 中所定義),就會進行Boxing作業。 同樣地,當特定參考型別的值(如 \10.3.7 中所定義)轉換為結構類型時,就會進行 Unboxing 作業。 與類別類型上相同作業的主要差異在於 Boxing 和 unboxing 會將 結構值複製到 Boxed 實例或箱外。
注意:因此,在 Boxing 或 unboxing 作業之後,對 Unboxed 所做的變更不會反映在 Boxed
struct
struct
中。 end note
如需 Boxing 和 unboxing 的進一步詳細數據,請參閱 \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()
用不會發生拳擊。end 範例
同樣地,當成員實作於實作實值型別時,在限制型別參數上存取成員時,永遠不會隱含地發生Boxing。 例如,假設介面 ICounter
包含方法 Increment
,可用來修改值。 如果使用 ICounter
做為條件約束,則會使用呼叫的變數Increment
參考呼叫 方法的實Increment
作,絕不會呼叫 Boxed 複本。
範例:
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
,它會修改 Boxed 複本x
中的值。 因此,程序的輸出為:0 1 1
end 範例
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 }
發生錯誤,因為實例欄位宣告包含變數初始化表達式。
end 範例
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); } }
語句都會使用 和
y
初始化為零來建立 。Point
x
end 範例
不允許結構實例建構函式包含窗體 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.21.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 } }
end example]
16.4.10 靜態建構函式
結構靜態建構函式會遵循與類別相同的大部分規則。 結構類型的靜態建構函式執行是由應用程式域內發生下列第一個事件所觸發:
- 參考結構類型的靜態成員。
- 呼叫結構類型的明確宣告建構函式。
注意:建立結構類型的預設值 (~16.4.5) 不會觸發靜態建構函式。 (其中一個範例是陣列中專案的初始值。 end note
16.4.11 自動實作屬性
自動實作的屬性 (~15.7.4) 會使用隱藏的支援欄位,這些欄位只能存取屬性存取子。
注意:此存取限制表示包含自動實作屬性之結構中的建構函式通常需要明確建構函式初始化表達式,否則它們不需要一個,以滿足叫用任何函式成員或建構函式傳回之前明確指派的所有字段需求。 end note
16.4.12 安全內容條件約束
16.4.12.1 一般
在編譯階段,每個表達式都會與內容相關聯,其中該實例及其所有字段都可以安全地存取,以及其安全內容。 安全內容是內容,會括住表達式,值可以安全逸出。
編譯時間類型不是 ref 結構的任何運算式,都有呼叫端內容的安全內容。
default
任何類型的表達式都有呼叫端內容的安全內容。
對於任何編譯時間類型為 ref 結構的非預設表達式,其具有下列各節所定義的安全內容。
安全內容記錄值可複製到其中的內容。 指定從具有安全內容的表達式E1
指派給具有安全內容的S2
表達式E2
,如果是S2
比 S1
更廣泛的內容,就會發生錯誤。S1
有三個不同的安全內容值,與針對參考變數定義的 ref-safe-context 值相同({9.7.2): declaration-block、 function-member 和 caller-context。 表達式的安全內容會限制其用法,如下所示:
- 對於 return 語句
return e1
,的安全內容e1
應該是呼叫端內容。 - 對於指派
e1 = e2
,的安全內容e2
至少應與的安全內容e1
一樣寬。
對於方法調用,如果型別有 ref
或 自變數ref struct
(包括接收者,除非類型為 readonly
),且具有安全內容S1
,則沒有自變數(包括接收者)可能會有比 更窄的安全內容S1
out
。
16.4.12.2 參數安全內容
ref 結構類型的參數,包括 this
實例方法的參數,具有呼叫端內容的安全內容。
16.4.12.3 局部變數安全內容
ref 結構類型的局部變數具有安全內容,如下所示:
- 如果變數是迴圈的
foreach
反覆專案變數,則變數的安全內容與迴圈表達式的安全內容foreach
相同。 - 否則,如果變數的宣告具有初始化表達式,則變數的安全內容與該初始化表達式的安全內容相同。
- 否則,變數在宣告點未初始化,且具有呼叫端內容的安全內容。
16.4.12.4 欄位安全內容
欄位 e.F
的參考,其中的類型 F
是 ref 結構型別,具有與 安全內容相同的安全內容 e
。
16.4.12.5 運算符
使用者定義運算子的應用程式會被視為方法調用(^16.4.12.6)。
對於產生值,例如 e1 + e2
或 c ? e1 : e2
的運算符,結果的安全內容是運算符之安全內容中最窄的內容。 因此,對於產生值的一元運算符,例如 +e
,結果的安全內容是操作數的安全內容。
注意:條件運算符的第一個操作數是
bool
,因此其安全內容是呼叫端內容。 接下來,產生的安全內容是第二和第三個操作數最窄的安全內容。 end note
16.4.12.6 方法與屬性調用
由方法調用 e1.M(e2, ...)
或屬性調用 e.P
所產生的值,具有下列內容中最小的安全內容:
- caller-context。
- 所有自變數表達式的安全內容(包括接收者)。
上述規則會將屬性調用 (或 get
set
) 視為基礎方法的方法調用。
16.4.12.7 stackalloc
stackalloc 運算式的結果具有 function-member 的安全內容。
16.4.12.8 建構函式調用
new
叫用建構函式的表達式會遵守與考慮傳回所建構型別的方法調用相同的規則。
此外,如果有任何初始化表達式存在,則安全內容是所有物件初始化表達式運算式之所有自變數和操作數的最小安全內容。
注意:這些規則依賴
Span<T>
下列格式的建構函式:public Span<T>(ref T p)
這類建構函式會讓的
Span<T>
實例與字段不ref
區分。 本檔所述的安全規則取決於ref
字段在 C# 或 .NET 中不是有效的建構。 end note