24.1 一般規定
需要不支援不安全程式碼的實作,才能診斷此子句中定義的語法規則的任何使用方式。
本條款的其餘部分,包括其所有子條款,均具有條件規範性。
附註: 如上述子句所定義的核心 C# 語言,與 C 和 C++ 顯著不同,因為它省略了指標作為資料類型。 相反地,C# 提供參考,以及建立記憶體回收器所管理物件的能力。 這種設計與其他功能相結合,使 C# 成為比 C 或 C++ 更安全的語言。 在核心 C# 語言中,根本不可能有未初始化的變數、「懸空」指標或將陣列索引到超出其邊界的表達式。 因此,經常困擾 C 和 C++ 程式的整個類別的錯誤都被消除了。
雖然 C 或 C++ 中的幾乎每個指標類型結構在 C# 中都有一個對應的引用類型,但在某些情況下,訪問指標類型成為必要。 例如,如果不存取指標,則可能無法或無法實作時間關鍵型演算法,以與底層作業系統介面、存取記憶體對應裝置或實作時間關鍵型演算法。 為了滿足此需求,C# 提供了編寫 不安全程式碼的能力。
在不安全的程式碼中,可以宣告和操作指標、執行指標和整數類型之間的轉換、取得變數的位址等等。 從某種意義上說,編寫不安全的程式碼很像在 C# 程式中編寫 C 程式碼。
從開發人員和用戶的角度來看,不安全的代碼實際上是一個“安全”的功能。 不安全的程式碼應明確標記修飾符
unsafe,因此開發人員不可能意外使用不安全的功能,並且執行引擎的工作原理是確保不安全的程式碼無法在不受信任的環境中執行。結尾註釋
24.2 不安全環境
C# 的不安全功能僅在不安全內容中可用。 不安全的內容是藉由在類型、成員或本機函式的宣告中包含 unsafe 修飾詞,或採用 unsafe_statement來引進:
- 類別、結構、介面或委派的宣告可能會包含
unsafe修飾詞,在此情況下,該類型宣告的整個文字範圍 (包括類別、結構或介面的主體) 會被視為不安全的內容。附註: 如果 type_declaration 是部分的,則只有該部分是不安全的前後關聯。 結尾註釋
- 欄位、方法、屬性、事件、索引子、運算子、實例建構函式、終結式、靜態建構函式或本機函式的宣告可能會包含
unsafe修飾詞,在此情況下,該成員宣告的整個文字範圍會被視為不安全的內容。 - unsafe_statement允許在區塊內使用不安全的上下文。 相關聯 區塊 的整個文字範圍會被視為不安全的環境定義。 在不安全內容中宣告的本機函式本身是不安全的。
相關的語法擴展如下所示,並在後續子句中顯示。
unsafe_modifier
: 'unsafe'
;
unsafe_statement
: 'unsafe' block
;
範例:在下列程式代碼中
public unsafe struct Node { public int Value; public Node* Left; public Node* Right; }
unsafe結構宣告中指定的修飾元會導致結構宣告的整個文字範圍變成不安全的內容。 因此,可以將 和Left欄位宣告Right為指標類型。 上面的例子也可以寫public struct Node { public int Value; public unsafe Node* Left; public unsafe Node* Right; }在這裡,
unsafe欄位宣告中的修飾符會導致這些宣告被視為不安全的內容。結束範例
除了建立不安全的內容,因此允許使用指標類型之外, unsafe 修飾詞對類型或成員沒有影響。
範例:在下列程式代碼中
public class A { public unsafe virtual void F() { char* p; ... } } public class B : A { public override void F() { base.F(); ... } }方法
F上的Aunsafe 修飾符只會讓 的文字範圍變成 不F安全的內容,其中可以使用語言的不安全功能。 在 in 的F重寫中,不需要重新指定B修飾符,當然unsafe,除非方法本身F需要存取不安全的B特性。當指標類型是方法簽章的一部分時,情況會略有不同
public unsafe class A { public virtual void F(char* p) {...} } public class B: A { public unsafe override void F(char* p) {...} }這裡,因為 的簽章包含指標類型,所以
F只能寫在不安全的上下文中。 不過,不安全的內容可以透過讓整個類別不安全 (如 中的情況A) 或在方法宣告中包含unsafe修飾符 (如 中的情況)B來引進不安全內容。結束範例
在部分類型宣告上使用修飾詞時 unsafe (§15.2.7) ,只有該特定部分會被視為不安全的內容。
24.3 指標類型
在不安全的內容中, 類型 (§8.1) 可以是 pointer_type ,也可以是 value_type、 reference_type或 type_parameter。 在不安全的內容中, pointer_type 也可以是陣列的元素類型 (§17)。 pointer_type也可以在不安全內容之外的表達式類型 (§12.8.18) 中使用, (因為這類用法並不不安全) 。
pointer_type寫成unmanaged_type (§8.8) 或關鍵字 void,後面接著記*號:
pointer_type
: value_type ('*')+
| 'void' ('*')+
;
在指標類型中 之前 * 指定的類型稱為指標類型的 所指稱類型 。 它代表指標類型的值所指向的變數類型。
pointer_type只能在不安全上下文的array_type中使用 (§24.2)。 non_array_type 是本身不是array_type的任何類型。
與參照 (參照類型的值) 不同,記憶體回收器不會追蹤指標,記憶體回收器不知道指標及其指向的資料。 因此,不允許指標指向引用或包含引用的結構,並且指標的參照類型應該是 unmanaged_type。 指標類型本身是非受控類型,因此指標類型可以作為另一個指標類型的參考類型。
混合指標和引用的直觀規則是,引用(對象)的所指對象允許包含指標,但指標的所指對象不允許包含引用。
範例:下表給出了指標類型的一些範例:
Example Description byte*指標 bytechar*指標 charint**指標對指標 intint*[]指向的指標的單維陣列 intvoid*未知類型的指標 結束範例
對於給定的實作,所有指標類型都應具有相同的大小和表示法。
附註: 不同於 C 和 C++,當在相同的宣告中宣告多個指標時,在 C# 中,則
*只會與基礎類型一起寫入,而不是作為每個指標名稱上的前置詞標點符號。 例如:int* pi, pj; // NOT as int *pi, *pj;結尾註釋
具有類型的 T* 指標的值表示類型 T為 的變數的位址。 指標間接運算子 * (§24.6.2) 可用來存取此變數。
範例:給定類型為
P的變數int*,運算式*P表示在 中包含的位址int找到的P變數。 結束範例
與物件引用一樣,指標可以是 null。 將間接運算子套用 null至值指標會導致實作定義的行為 (§24.6.2) 。 具有值 null 的指標以全位元零表示。
類型代表 void* 未知類型的指標。 因為所指類型未知,所以間接運算子無法套用至類型的 void*指標,也無法在這類指標上執行任何算術運算。 不過,類型的 void* 指標可以轉換至任何其他指標類型 (反之亦然) ,並與其他指標類型的值進行比較 (§24.6.8) 。
指標類型是個別的類型類別。 與參考類型和值類型不同,指標類型不會繼承自 object ,指標類型與 object之間不存在轉換。 特別是,指標不支援裝箱和取消裝箱 (§8.3.13) 。 不過,容許在不同指標類型之間,以及指標類型與整數類型之間進行轉換。 這在 §24.5 中進行了描述。
pointer_type 無法作為類型引數 (§8.4) 使用,而且類型推斷 (§12.6.3) 會在泛型方法呼叫上失敗,這些呼叫會將類型引數推斷為指標類型。
pointer_type 無法作為動態繫結作業的子運算式類型 (§12.3.3) 。
pointer_type 不能作為擴充方法中第一個參數的類型 (§15.6.10) 。
pointer_type 可用作易失性場的類型 (§15.5.4)。
類型的E*是具有動態擦除E的所指類型的指標類型。
具有指標類型的運算式無法用來在anonymous_object_creation_expression內的member_declarator中提供值 (§12.8.17.3)。
任何指標類型的預設值 (§9.3) 是 null。
附註: 雖然指標可以作為依照參數傳遞,但這樣做可能會導致未定義的行為,因為指標很可能設定為指向呼叫方法傳回時不再存在的局部變數,或它曾經指向的固定物件不再固定。 例如:
class Test { static int value = 20; unsafe static void F(out int* pi1, ref int* pi2) { int i = 10; pi1 = &i; // return address of local variable fixed (int* pj = &value) { // ... pi2 = pj; // return address that will soon not be fixed } } static void Main() { int i = 15; unsafe { int* px1; int* px2 = &i; F(out px1, ref px2); int v1 = *px1; // undefined int v2 = *px2; // undefined } } }結尾註釋
方法可以傳回某種類型的值,而該類型可以是指標。
範例:當給定指向 s 的
int連續序列、該序列的元素計數和其他值int的指標時,如果發生匹配,以下方法會傳回該序列中該值的位址;否則它會傳回null:unsafe static int* Find(int* pi, int size, int value) { for (int i = 0; i < size; ++i) { if (*pi == value) { return pi; } ++pi; } return null; }結束範例
在不安全的內容中,有數個建構可用於在指標上操作:
- 一元
*運算子可用來執行指標間接運算 (§24.6.2)。 - 運算
->子可用來透過指標存取結構的成員 (§24.6.3) 。 - 運算
[]子可用來索引指標 (§24.6.4)。 - 一元
&運算子可用於取得變數的位址(§24.6.5)。 - and
++--運算子可用來遞增和遞減指標 (§24.6.6)。 - 二進位和
+-運算子可用於執行指標算術 (§24.6.7)。 -
==、 、!=<、><=、 和>=運算子可用來比較指標 (§24.6.8) 。 - 運算
stackalloc子可用來從呼叫堆疊配置記憶體 (§24.9) 。 - 該
fixed語句可用於臨時修復變量,以便可以獲取其地址 (§24.7)。
24.4 固定變數和可移動變數
位址運算子 (§24.6.5) 和 fixed 陳述式 (§24.7) 將變數分為兩類: 固定變數s 和 可移動變數s。
固定變數位於不受記憶體回收器作業影響的儲存位置。 (固定變數的範例包括局部變數、值參數,以及取消參考指標所建立的變數。另一方面,可移動變數位於記憶體回收器會重新定位或處置的儲存位置。 (可移動變數的範例包括物件中的欄位和陣列的元素。
運算子 & (§24.6.5) 允許不受限制地取得固定變數的位址。 不過,由於可移動變數會受到記憶體回收器的重新定位或處置,因此可移動變數的位址只能使用 (fixed statement) 取得,且該位址僅在該fixed陳述式的持續時間內保持有效。
準確地說,固定變數是下列其中一項:
- 由參考局部變數、值參數或參數陣列的 simple_name (§12.8.4) 所產生的變數,除非變數是由匿名函式擷取 (§12.21.6.2) 。
- 由形式為 的member_access (
V.I) 產生的變數,其中V是struct_type的固定變數。 - 由 形式的pointer_indirection_expression (
*P) 、 形式的pointer_member_access (P->I) 或 形式的pointer_element_access (§24.6.4)P[E]所產生的變數。
所有其他變數都歸類為可移動變數。
靜態欄位被歸類為可移動變數。 此外,按參考參數被歸類為可移動變量,即使為參數給出的引數是固定變量。 最後,透過取消引用指標產生的變數一律被歸類為固定變數。
24.5 指標轉換
24.5.1 一般規定
在不安全的內容中,可用的隱含轉換集 (§10.2) 會擴充為包含下列隱含指標轉換:
- 從任何 pointer_type 到類型
void*。 - 從
null字面 (§6.4.5.7) 到任何 pointer_type。
此外,在不安全的內容中,可用的明確轉換集 (§10.3) 會擴充為包含下列明確指標轉換:
- 從任何 pointer_type 到任何其他 pointer_type。
- 從
sbyte、 、byteshort、ushort、intuintlong、 、 或ulong到任何pointer_type。 - 從任何pointer_type到
sbyte、byte、shortushortintuintlong或 。ulong
最後,在不安全的內容中,標準隱含轉換集 (§10.4.2) 包含下列指標轉換:
- 從任何 pointer_type 到類型
void*。 - 從
null字面意思到任何 pointer_type。
兩個指標類型之間的轉換永遠不會變更實際指標值。 換句話說,從一種指標類型轉換到另一種指標類型不會影響指標所提供的基礎位址。
當一個指標類型轉換成另一個指標類型時,如果產生的指標未正確對齊指向的類型,則如果取消參考結果,則行為會未定義。 一般來說,「正確對齊」這個概念是傳遞的:如果指向類型的 A 指標正確對齊到類型的 B指標,而該指標又針對指向類型的 C指標正確對齊,那麼指向類型的 A 指標對於指向類型的 C指標正確對齊。
範例:請考慮下列案例,其中具有一種類型的變數是透過指向不同類型的指標來存取的:
unsafe static void M() { char c = 'A'; char* pc = &c; void* pv = pc; int* pi = (int*)pv; // pretend a 16-bit char is a 32-bit int int i = *pi; // read 32-bit int; undefined *pi = 123456; // write 32-bit int; undefined }結束範例
當指標類型轉換為指標 byte時,結果會指向變數的最低定址 byte 。 結果的連續增量 (直到變數的大小) 會產生指向該變數剩餘位元組的指標。
範例:下列方法會將 a
double中的八個位元組中的每一個顯示為十六進位值:class Test { static void Main() { double d = 123.456e23; unsafe { byte* pb = (byte*)&d; for (int i = 0; i < sizeof(double); ++i) { Console.Write($" {*pb++:X2}"); } Console.WriteLine(); } } }當然,產生的輸出取決於字節序。 一種可能性是
" BA FF 51 A2 90 6C 24 45"。結束範例
指標和整數之間的對應是實作定義的。
附註: 不過,在具有線性位址空間的 32 位元及 64 位元 CPU 架構上,指標與整數類型或從整數類型轉換的行為通常與轉換或值分別與這些整數類型完全相同
uintulong。 結尾註釋
24.5.2 指標陣列
指標陣列可以在不安全的內容中使用 array_creation_expression (§12.8.17.4) 來建構。 指標陣列只允許套用至其他陣列類型的部分轉換:
- 從任何array_type到其實作介面的隱含參考轉換 (
System.Array) 也適用於指標陣列。 不過,任何嘗試透過或其實作的介面存取System.Array陣列元素都可能導致執行階段異常,因為指標類型無法轉換為object。 - 從單一維度陣列類型到其泛型基底介面的隱含和明確參考轉換 (
S[]、System.Collections.Generic.IList<T>) 永遠不會套用至指標陣列。 - 明確的參考轉換 (
System.Array) 及其實作至任何array_type的介面會套用至指標陣列。 - 從及其基底介面到單一維度陣列類型的
System.Collections.Generic.IList<S>明確參考轉換 (T[]) 永遠不會套用至指標陣列,因為指標類型無法作為類型引數使用,而且沒有從指標類型轉換成非指標類型。
這些限制表示 foreach 中所述陣列上的陳述式展開無法套用至指標陣列。 相反, foreach 格式為
foreach (V v in x)
embedded_statement
其中 的 x 類型是形式的陣列類型 T[,,...,], n 是維度數減去 1 和 T or V 是指標類型,使用巢狀 for 迴圈進行擴展,如下所示:
{
T[,,...,] a = x;
for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
{
for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
{
...
for (int in = a.GetLowerBound(n); in <= a.GetUpperBound(n); in++)
{
V v = (V)a[i0,i1,...,in];
*embedded_statement*
}
}
}
}
變數 a、 i0、 i1...
in對程式的x或任何其他原始碼不可見或無法存取。 變數 v 在內嵌語句中是唯讀的。 如果沒有從(元素類型)到 T的明確轉換 (V),則會產生錯誤,並且不會採取進一步的步驟。 如果具有值 ,則x會在執行階段擲回 anull。System.NullReferenceException
附註: 雖然不允許指標類型作為類型引數,但指標陣列可以作為類型引數使用。 結尾註釋
24.6 運算式中的指標
24.6.1 一般規定
在不安全的內容中,運算式可能會產生指標類型的結果,但在不安全的內容之外,運算式屬於指標類型的編譯時間錯誤。 確切地說,在不安全的內容之外,如果任何 simple_name (§12.8.4)、 member_access (§12.8.7)、 invocation_expression (§12.8.10) 或 element_access (§12.8.12) 是指標類型,則會發生編譯階段錯誤。
在不安全的情況下, primary_expression (§12.8) 和 unary_expression (§12.9) 作品允許額外的結構,這些結構在以下子條款中進行了描述。
附註: 文法隱含了不安全運算子的優先順序和關聯性。 結尾註釋
24.6.2 指標間接
pointer_indirection_expression由星號 (*) 後面接著unary_expression組成。
pointer_indirection_expression
: '*' unary_expression
;
一元 * 運算子表示指標間接,用於取得指標指向的變數。 評估 *P的結果 ,其中 P 是指標類型的 T*運算式 ,是類型 T為 的變數。 將一元 * 運算子套用至類型的 void* 運算式,或非指標類型的運算式,這是編譯階段錯誤。
將一元 * 運算子 null套用至值指標的效果是實作定義的。 特別是,不保證此作業會擲出 System.NullReferenceException.
如果已將無效值指派給指標,則一元 * 運算子的行為是未定義的。
附註: 一元
*運算子取消引用指標的無效值包括針對所指向的類型不適當對齊的位址 (請參閱 §24.5 中的範例),以及變數在其生命週期結束後的位址。
出於確定賦值分析的目的,透過評估該形式的 *P 表達式產生的變數被視為初始賦值 (§9.4.2)。
24.6.3 指標成員存取
pointer_member_access由一個primary_expression、後面接著一個「->」記號、後面接著一個識別碼和一個選用type_argument_list組成。
pointer_member_access
: primary_expression '->' identifier type_argument_list?
;
在指標成員 P->I存取中,格式 , P 應是指標類型的運算式,並 I 應表示指向該類型的 P 可存取成員。
表單 P->I 的指標成員存取權的評估方式完全符合 (*P).I。 如需指標間接運算子 ()* 的描述,請參閱 §24.6.2。 有關成員存取運算子 (). 的描述,請參閱 §12.8.7。
範例:在下列程式代碼中
struct Point { public int x; public int y; public override string ToString() => $"({x},{y})"; } class Test { static void Main() { Point point; unsafe { Point* p = &point; p->x = 10; p->y = 20; Console.WriteLine(p->ToString()); } } }運算
->子用於存取欄位,並透過指標呼叫結構體的方法。 因為該操作P->I完全等同(*P).I於 ,所以該Main方法同樣可以寫成:class Test { static void Main() { Point point; unsafe { Point* p = &point; (*p).x = 10; (*p).y = 20; Console.WriteLine((*p).ToString()); } } }結束範例
24.6.4 指標元素存取
pointer_element_access由一個primary_expression組成,後面接著一個包含在 “” 和 “[]” 中的表達式。
pointer_element_access
: primary_expression '[' expression ']'
;
在識別 primary_expression 時,如果 element_access 和 pointer_element_access (§24.6.4) 替代方案都適用,則如果嵌入 primary_expression 是指標類型 (§24.3),則應選擇後者。
在形式的指標元素存取 P[E]中,應 P 是 以外的指標類型的 void*運算式,且 E 應是可隱式轉換為 int、 uint、 long或 ulong的運算式。
表單 P[E] 的指標元素存取的評估方式完全符合 *(P + E)。 如需指標間接運算子 ()* 的描述,請參閱 §24.6.2。 如需指標加法運算子 ()+ 的描述,請參閱 §24.6.7。
範例:在下列程式代碼中
class Test { static void Main() { unsafe { char* p = stackalloc char[256]; for (int i = 0; i < 256; i++) { p[i] = (char)i; } } } }指標元素存取可用來初始化迴圈中的
for字元緩衝區。 因為該操作P[E]完全等同*(P + E)於 ,所以該示例同樣可以編寫:class Test { static void Main() { unsafe { char* p = stackalloc char[256]; for (int i = 0; i < 256; i++) { *(p + i) = (char)i; } } } }結束範例
指標元素存取運算子不會檢查越界錯誤,且存取越界元素時的行為未定義。
注意:這與 C 和 C++ 相同。 結尾註釋
24.6.5 位址運算子
addressof_expression
: '&' unary_expression
;
給定一個屬於類型E並被分類為固定變數 (T) 的表達式,該結構&E會計算由 給E出的變數的位址。 結果的類型是 T* 和 分類為值。 如果未分類為變數、如果E分類為唯讀區域變數,或表示E可移動變數,則E會發生編譯階段錯誤。 在最後一種情況下,可以使用固定語句 (§24.7) 在取得變數位址之前暫時「固定」變數。
附註: 如 §12.8.7 所述,在定義
readonly欄位之結構或類別的實例建構函式或靜態建構函式之外,該欄位會被視為值,而不是變數。 因此,其地址無法被獲取。 同樣地,常數的位址也無法取得。 結尾註釋
&運算子不需要明確指派其引數,但在作業之後&,套用運算子的變數會被視為在作業發生的執行路徑中明確指派。 程式設計師有責任確保在這種情況下確實會正確起始設定變數。
範例:在下列程式代碼中
class Test { static void Main() { int i; unsafe { int* p = &i; *p = 123; } Console.WriteLine(i); } }
i被視為在用於初始化&i的操作之後p確定分配。 實際上,指派會*p初始化i,但包含此初始化是程式設計師的責任,而且如果移除指派,則不會發生編譯時期錯誤。結束範例
附註: 運算子的
&確定指派規則存在,因此可以避免局部變數的冗餘起始設定。 例如,許多外部 API 會採用 API 所填入結構的指標。 對此類 API 的呼叫通常會傳遞本機結構變數的位址,如果沒有規則,則需要結構變數的冗餘初始化。 結尾註釋
附註: 當匿名函式擷取局部變數、值參數或參數陣列時 (§12.8.24) ,該局部變數、參數或參數陣列不再被視為固定變數 (§24.7),而是被視為可移動變數。 因此,任何不安全的程式碼採用匿名函數所擷取的局部變數、值參數或參數陣列的位址都是錯誤。 結尾註釋
24.6.6 指標增減
在不安全的內容中, ++ and -- 運算子 (§12.8.16 和 §12.9.7) 可以套用至除 之外的所有類型的 void*指標變數。 因此,對於每個指標類型 T*,都會隱含定義下列運算子:
T* operator ++(T* x);
T* operator --(T* x);
運算子分別產生與 x+1 和 x-1相同的結果 (§24.6.7)。 換句話說,對於類型 T*為 的指標變數, ++ 運算子會加 sizeof(T) 到變數中包含的位址,運算子會 -- 從變數中包含的位址中減去 sizeof(T) 。
如果指標遞增或遞減作業溢位指標類型的網域,則結果是實作定義的,但不會產生例外狀況。
24.6.7 指標算術
在不安全的內容中, + 運算子 (§12.12.5) 和 - 運算子 (§12.12.6) 可以套用至除 之外的所有 void*指標類型的值。 因此,對於每個指標類型 T*,都會隱含定義下列運算子:
T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
T* operator +(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);
T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);
long operator –(T* x, T* y);
給定指標類型的表達式P和類型T*為 、 N、 int或 uint的表達式long,ulongP + N並計算加到N + P給定的T*地址所產生的類型的N * sizeof(T)指標P值。 同樣地,運算式P – N會計算從 給T*出的位址中減去N * sizeof(T)所產生的類型P指標值。
給定兩個指標類型的表達式 PQ和 ,該表達式T*計算 和 P – Q 給P出的地址之間的差異,然後將該差異除以 Q。sizeof(T) 結果的類型一律 long為 。 實際上, P - Q 計算為 ((long)(P) - (long)(Q)) / sizeof(T)。
Example:
class Test { static void Main() { unsafe { int* values = stackalloc int[20]; int* p = &values[1]; int* q = &values[15]; Console.WriteLine($"p - q = {p - q}"); Console.WriteLine($"q - p = {q - p}"); } } }這會產生輸出:
p - q = -14 q - p = 14結束範例
如果指標算術運算溢位指標類型的網域,則會以實作定義的方式截斷結果,但不會產生例外狀況。
24.6.8 指標比較
在不安全的內容中,==、 !=<><=>= 、 和運算子 (§12.14) 可以套用至所有指標類型的值。 指標比較運算子包括:
bool operator ==(void* x, void* y);
bool operator !=(void* x, void* y);
bool operator <(void* x, void* y);
bool operator >(void* x, void* y);
bool operator <=(void* x, void* y);
bool operator >=(void* x, void* y);
因為存在從任何指標類型 void* 到類型的隱含轉換,所以可以使用這些運算子來比較任何指標類型的運算元。 比較運算子會比較兩個運算元所給出的位址,就好像它們是無符號整數一樣。
24.6.9 sizeof 運算子
針對某些預先定義的類型 (§12.8.19), sizeof 運算子會產生常數 int 值。 對於所有其他類型,運算子的 sizeof 結果是實作定義的,並分類為值,而不是常數。
未指定成員封裝到結構中的順序。
基於對齊方式,在結構的開頭、結構內和結構的結尾可能會有未命名的填補。 用作填充的位的內容是不確定的。
套用至具有結構類型的運算元時,結果是該類型變數中的位元組總數,包括任何填補。
24.7 固定語句
在不安全的上下文中, embedded_statement (§13.1) 生產允許一個額外的結構,即固定語句,它用於“固定”可移動變量,使其地址在語句的持續時間內保持不變。
fixed_statement
: 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement
;
fixed_pointer_declarators
: fixed_pointer_declarator (',' fixed_pointer_declarator)*
;
fixed_pointer_declarator
: identifier '=' fixed_pointer_initializer
;
fixed_pointer_initializer
: '&' variable_reference
| expression
;
每個 fixed_pointer_declarator 都會宣告給定 pointer_type 的局部變數,並使用對應 fixed_pointer_initializer計算的位址初始化該局部變數。 在固定語句中宣告的局部變數可以在該變數宣告右側的任何 fixed_pointer_initializer中存取,以及在固定語句的 embedded_statement 中。 固定陳述式所宣告的局部變數會被視為唯讀。 如果內嵌陳述式嘗試修改此局部變數 (透過指派或 ++ and -- 運算子) 或將它傳遞為參照或輸出參數,則會發生編譯階段錯誤。
在fixed_pointer_initializer中使用擷取的局部變數 (§12.21.6.2)、值參數或參數陣列是錯誤。 fixed_pointer_initializer可以是下列其中一項:
- 權杖 “
&” 後面接著非受控類型的可移動變數 (§24.4) 的variable_reference (T) ,前提是該類型T*可隱含地轉換為語句中fixed指定的指標類型。 在這種情況下,初始化器會計算給定變數的位址,並且變數保證在固定語句的持續時間內保持在固定位址。 - 具有非受控類型元素的
T的運算式,前提是該類型T*可以隱含地轉換為固定語句中指定的指標類型。 在這種情況下,初始化器會計算陣列中第一個元素的位址,並且保證整個陣列在語句持續fixed時間內保持在固定位址。 如果陣列運算式為null或陣列具有零元素,則初始化運算式會計算等於零的位址。 - 類型
string的運算式 ,前提是該類型char*可隱含地轉換為語句中fixed給出的指標類型。 在這種情況下,初始化器會計算字串中第一個字元的位址,並且保證整個字串在語句的fixed持續時間內保持在固定位址。 如果字串運算式為fixed,則陳述式的null行為是實作定義的。 -
array_type 或
string以外的類型的運算式,前提是存在符合簽章ref [readonly] T GetPinnableReference()的可存取方法或可存取擴充方法,其中T是unmanaged_type,且T*可隱含轉換成陳述式中fixed指定的指標類型。 在此情況下,初始化運算式會計算傳回變數的位址,而該變數保證在陳述式持續fixed期間保持在固定位址。GetPinnableReference()當多載解析 (fixed) 只產生一個函式成員,且該函式成員符合上述條件時,陳述式可以使用方法。 此GetPinnableReference方法應該傳回等於零的位址參考,例如當沒有要釘選的資料時傳回System.Runtime.CompilerServices.Unsafe.NullRef<T>()的位址。 - 參考可移動變數的固定大小緩衝區成員的 simple_name 或 member_access ,前提是固定大小緩衝區成員的類型可隱含地轉換為陳述式中
fixed指定的指標類型。 在此情況下,初始化運算式會計算固定大小緩衝區第一個元素的指標 (§24.8.3) ,而固定大小緩衝區保證在陳述式持續期間fixed保留在固定位址。
對於fixed_pointer_initializerfixed計算的每個位址,陳述式可確保位址所參照的變數在陳述式期間fixed不會受到記憶體回收器的重新定位或處置。
範例:如果 fixed_pointer_initializer 計算的位址參考了物件的欄位或陣列實例的元素,則固定語句保證在語句的生命週期內不會重新定位或處置包含的物件實例。 結束範例
程式設計師有責任確保固定陳述式所建立的指標不會在執行這些陳述式之後繼續存在。
範例:當陳述式建立的
fixed指標傳遞至外部 API 時,程式設計師有責任確保 API 不會保留這些指標的記憶體。 結束範例
固定物件可能會導致堆積碎片化 (因為它們無法移動)。 因此,只有在絕對必要時才應固定物體,並且只能在盡可能短的時間內固定。
範例:範例
class Test { static int x; int y; unsafe static void F(int* p) { *p = 1; } static void Main() { Test t = new Test(); int[] a = new int[10]; unsafe { fixed (int* p = &x) F(p); fixed (int* p = &t.y) F(p); fixed (int* p = &a[0]) F(p); fixed (int* p = a) F(p); } } }示範了該
fixed陳述式的幾種用法。 第一個語句固定並取得一個靜態欄位的位址,第二個語句固定並取得一個實例欄位的位址,第三個語句固定並取得一個陣列元素的位址。 在每種情況下,使用常規&運算子都是錯誤的,因為變數都被歸類為可移動變數。上述範例中的第三個和第四
fixed個陳述式會產生相同的結果。 一般而言,對於陣列實例a,在語句中a[0]指定fixed與簡單指定a是一樣的。結束範例
在不安全的上下文中,單維數組的陣列元素以索引遞增順序存儲,以 index 0 開頭,以 index Length – 1結尾。 對於多維陣列,陣列元素的儲存方式是先增加最右邊維度的索引,然後是下一個左維度,依此類推。
在取得fixed陣列實例p指標的陳述式中a,指標值範圍為 代表pp + a.Length - 1陣列中元素的位址。 同樣,變數範圍為 代表p[0]p[a.Length - 1]實際的陣列元素。 根據陣列的儲存方式,任何維度的陣列都可以被視為線性。
Example:
class Test { static void Main() { int[,,] a = new int[2,3,4]; unsafe { fixed (int* p = a) { for (int i = 0; i < a.Length; ++i) // treat as linear { p[i] = i; } } } for (int i = 0; i < 2; ++i) { for (int j = 0; j < 3; ++j) { for (int k = 0; k < 4; ++k) { Console.Write($"[{i},{j},{k}] = {a[i,j,k],2} "); } Console.WriteLine(); } } } }這會產生輸出:
[0,0,0] = 0 [0,0,1] = 1 [0,0,2] = 2 [0,0,3] = 3 [0,1,0] = 4 [0,1,1] = 5 [0,1,2] = 6 [0,1,3] = 7 [0,2,0] = 8 [0,2,1] = 9 [0,2,2] = 10 [0,2,3] = 11 [1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15 [1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19 [1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23結束範例
範例:在下列程式代碼中
class Test { unsafe static void Fill(int* p, int count, int value) { for (; count != 0; count--) { *p++ = value; } } static void Main() { int[] a = new int[100]; unsafe { fixed (int* p = a) Fill(p, 100, -1); } } }
fixed語句用於修復陣列,以便將其位址傳遞給接受指標的方法。結束範例
char*修正字串實例所產生的值一律指向以 Null 結尾的字串。 在取得p字串實例s指標的固定陳述式中,指標值範圍為 代表pp + s.Length ‑ 1字串中字元的位址,而指標值p + s.Length一律指向空字元 (值為 '\0' 的字元) 。
Example:
class Test { static string name = "xx"; unsafe static void F(char* p) { for (int i = 0; p[i] != '\0'; ++i) { System.Console.WriteLine(p[i]); } } static void Main() { unsafe { fixed (char* p = name) F(p); fixed (char* p = "xx") F(p); } } }結束範例
範例:下列程式碼顯示運算式類型為 array_type 或 以外的
string:public class C { private int _value; public C(int value) => _value = value; public ref int GetPinnableReference() => ref _value; } public class Test { unsafe private static void Main() { C c = new C(10); fixed (int* p = c) { // ... } } }Type
C具有具有正確簽章的可存取GetPinnableReference方法。 在語句中fixed,呼叫 onref int時從該方法傳回的 用於c初始化int*指標p。 結束範例
透過固定指標修改受控類型的物件可能會導致未定義的行為。
附註: 例如,因為字串是不可變的,所以程式設計師有責任確保固定字串指標所參照的字元不會被修改。 結尾註釋
注意:在呼叫需要「C 樣式」字串的外部 API 時,字串的自動 Null 終止特別方便。 不過請注意,允許字串實例包含空值字元。 如果存在這類空字元,則當將字串視為以空值結尾時
char*,字串會顯示為截斷。 結尾註釋
24.8 固定大小緩衝區
24.8.1 一般規定
固定大小緩衝區可用來將「C 樣式」內嵌陣列宣告為結構的成員,而且主要用於與非受控 API 介面。
24.8.2 固定大小緩衝區宣告
固定大小緩衝區是代表指定類型變數固定長度緩衝區記憶體的成員。 固定大小緩衝區宣告會引進指定元素類型的一或多個固定大小緩衝區。
注意:與陣列一樣,固定大小的緩衝區可以被視為包含元素。 因此,針對陣列定義的 術語元素類型 也會與固定大小緩衝區搭配使用。 結尾註釋
固定大小緩衝區只允許在結構宣告中,而且只能發生在不安全的內容中 (§24.2) 。
fixed_size_buffer_declaration
: attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type
fixed_size_buffer_declarators ';'
;
fixed_size_buffer_modifier
: 'new'
| 'public'
| 'internal'
| 'private'
| 'unsafe'
;
buffer_element_type
: type
;
fixed_size_buffer_declarators
: fixed_size_buffer_declarator (',' fixed_size_buffer_declarator)*
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
固定大小的緩衝區宣告可以包含一組屬性 (§23) 、 new 修飾詞 (§15.3.5)、對應至結構成員允許的任何宣告可存取性的協助工具修飾詞 (§16.4.3) ,以及修 unsafe 飾詞 (§24.2) 。 屬性和修飾詞會套用至固定大小緩衝區宣告所宣告的所有成員。 相同的修飾符在固定大小緩衝區宣告中多次出現是錯誤。
不允許固定大小緩衝區宣告包含 static 修飾詞。
固定大小緩衝區宣告的緩衝區元素類型會指定宣告所引進之緩衝區的元素類型。 緩衝元素類型應該是預先定義的類型sbyte之一 、 byte、 short、 ushort、 intuintlongulongcharfloatdoublebool或 。
緩衝區元素類型後面接著固定大小緩衝區宣告子的清單,每個宣告子都會引進一個新成員。 固定大小的緩衝區宣告子是由命名成員的識別碼所組成,後面接著括在 和 [ 權杖中的]常數運算式。 常數運算式表示該固定大小緩衝區宣告子所引進的成員中的元素數目。 常數表達式的類型應隱式轉換為類型 int,且值應為非零正整數。
固定大小緩衝區的元素應依序佈置在記憶體中。
宣告多個固定大小緩衝區的固定大小緩衝區宣告相當於具有相同屬性和元素類型的單一固定大小緩衝區宣告的多個宣告。
Example:
unsafe struct A { public fixed int x[5], y[10], z[100]; }等同於
unsafe struct A { public fixed int x[5]; public fixed int y[10]; public fixed int z[100]; }結束範例
24.8.3 運算式中的固定大小緩衝區
固定大小緩衝區成員的成員查閱 (§12.5) 會繼續進行,就像欄位的成員查閱一樣。
固定大小緩衝區可以使用 simple_name (§12.8.4)、 member_access (§12.8.7) 或 element_access (§12.8.12) 在運算式中參考。
當固定大小緩衝區成員參考為簡單名稱時,效果與形式 this.I的成員存取相同,其中 I 是固定大小緩衝區成員。
在形式 E.IE. 的成員存取中,如果 是結構this.類型,且該結構類型中的成員查閱 識別E固定大小的成員,I則E.I會評估並分類如下:
- 如果運算式
E.I未發生在不安全的內容中,則會發生編譯階段錯誤。 - 如果分類為值,則
E會發生編譯時間錯誤。 - 否則,如果 是可移動變數 (
E),則:- 如果運算式
E.I是fixed_pointer_initializer (§24.7) ,則運算式的結果是指向 中I固定大小緩衝區成員E第一個元素的指標。 - 否則,如果運算式
E.I是 (§12.8.12) 格式 (§12.8.12) element_access內的primary_expression (E.I[J]) ,則 的E.I結果是指向 中固定大小緩衝區成員PI第一個元素的指標E,然後將封閉element_access評估為 pointer_element_access (§24.6.4)P[J]。 - 否則會發生編譯階段錯誤。
- 如果運算式
- 否則,
E會參考固定變數,而運算式的結果是指向 中固定大小緩衝區成員IE第一個元素的指標。 結果是 的類型S*,其中 S 是 的元素類型I,並分類為值。
固定大小緩衝區的後續元素可以使用第一個元素的指標作業來存取。 不同於存取陣列,存取固定大小緩衝區的元素是不安全的作業,而且不會檢查範圍。
範例:下列會宣告並使用具有固定大小緩衝區成員的結構。
unsafe struct Font { public int size; public fixed char name[32]; } class Test { unsafe static void PutString(string s, char* buffer, int bufSize) { int len = s.Length; if (len > bufSize) { len = bufSize; } for (int i = 0; i < len; i++) { buffer[i] = s[i]; } for (int i = len; i < bufSize; i++) { buffer[i] = (char)0; } } unsafe static void Main() { Font f; f.size = 10; PutString("Times New Roman", f.name, 32); } }結束範例
24.8.4 確定分配檢查
固定大小緩衝區不受明確指派檢查 (§9.4) 的約束,而且會忽略固定大小緩衝區成員,以檢查結構類型變數的明確指派。
當固定大小緩衝區成員的最外層包含結構變數是靜態變數、類別實例的實例變數或陣列元素時,固定大小緩衝區的元素會自動初始化為其預設值 (§9.3)。 在所有其他情況下,固定大小緩衝區的初始內容是未定義的。
24.9 堆疊分配
有關運營商的一般信息,請參閱 stackalloc 。 在這裡,討論了該運算子產生指標的能力。
當stackalloc_expression發生為local_variable_declaration (§13.6.2) 的初始化運算式時,其中local_variable_type是指標類型 (§24.3) 或推斷 (var) ,stackalloc_expression的結果是類型的T*指標,其中 T 是stackalloc_expression的unmanaged_type。 在此情況下,結果是已配置區塊開頭的指標。
Example:
unsafe { // Memory uninitialized int* p1 = stackalloc int[3]; // Memory initialized int* p2 = stackalloc int[3] { -10, -15, -30 }; // Type int is inferred int* p3 = stackalloc[] { 11, 12, 13 }; // Can't infer context, so pointer result assumed var p4 = stackalloc[] { 11, 12, 13 }; // Error; no conversion exists long* p5 = stackalloc[] { 11, 12, 13 }; // Converts 11 and 13, and returns long* long* p6 = stackalloc[] { 11, 12L, 13 }; // Converts all and returns long* long* p7 = stackalloc long[] { 11, 12, 13 }; }結束範例
與存取數組或 stackalloc類型的 'ed 區塊 Span<T> 不同,存取指標類型的 'ed 區塊的 stackalloc元素是不安全的作業,而且不會進行範圍檢查。
範例:在下列程式代碼中
class Test { static string IntToString(int value) { if (value == int.MinValue) { return "-2147483648"; } int n = value >= 0 ? value : -value; unsafe { char* buffer = stackalloc char[16]; char* p = buffer + 16; do { *--p = (char)(n % 10 + '0'); n /= 10; } while (n != 0); if (value < 0) { *--p = '-'; } return new string(p, 0, (int)(buffer + 16 - p)); } } static void Main() { Console.WriteLine(IntToString(12345)); Console.WriteLine(IntToString(-999)); } }方法中使用
stackalloc運算IntToString式,在堆疊上配置 16 個字元的緩衝區。 當方法傳回時,緩衝區會自動捨棄。不過請注意,可以在安全模式下重寫;
IntToString也就是說,不使用指標,如下所示:class Test { static string IntToString(int value) { if (value == int.MinValue) { return "-2147483648"; } int n = value >= 0 ? value : -value; Span<char> buffer = stackalloc char[16]; int idx = 16; do { buffer[--idx] = (char)(n % 10 + '0'); n /= 10; } while (n != 0); if (value < 0) { buffer[--idx] = '-'; } return buffer.Slice(idx).ToString(); } }結束範例
條件規範文本的結尾。