共用方式為


23 不安全的程序代碼

23.1 一般

需要不支援不安全程式碼的實作,才能診斷此子句中定義之語法規則的任何用法。

這一條款的其餘部分,包括其所有子條款,是有條件的規範。

注意:如上述子句中所定義的核心 C# 語言,與 C 和 C++不同,因為其遺漏指標做為數據類型。 相反地,C# 會提供參考和建立垃圾收集行程所管理的物件的能力。 此設計加上其他功能,可讓 C# 成為比 C 或 C++更安全的語言。 在核心 C# 語言中,不可能有未初始化的變數、「懸空」指標,或索引超出其界限之陣列的運算式。 因此,會消除經常困擾 C 和C++程式之各種 Bug 類別。

雖然 C 或 C++ 中幾乎每個指標類型建構都有 C# 的參考型別對應專案,但在某些情況下,存取指標類型會成為必要專案。 例如,與基礎操作系統互動、存取記憶體對應裝置,或實作時間關鍵演算法可能無法或實際執行,而不需要存取指標。 為了解決這項需求,C# 提供撰寫 不安全程式代碼的能力。

在不安全的程式代碼中,可以宣告及操作指標、執行指標與整數類型之間的轉換、取得變數的位址等等。 從某種意義上說,撰寫不安全的程式代碼就像在 C# 程式中撰寫 C 程式代碼一樣。

從開發人員和用戶的觀點來看,不安全的程式代碼實際上是「安全的」功能。 Unsafe 程式代碼應清楚標示為 修飾詞 unsafe,因此開發人員無法意外使用不安全的功能,而且執行引擎可以確保不安全的程式代碼無法在不受信任的環境中執行。

end note

23.2 不安全的內容

C# 的不安全功能僅適用於不安全的內容。 不安全的內容是藉由在 unsafe 類型、成員或本機函式的宣告中包含修飾詞,或採用 unsafe_statement來引進:

  • 類別、結構、介面或委派的宣告可能包含 unsafe 修飾詞,在此情況下,該類型宣告的整個文字範圍(包括類別、結構或介面的主體)被視為不安全的內容。

    注意:如果 type_declaration 部分,則只有該部分是不安全的內容。 end note

  • 欄位、方法、屬性、事件、索引器、運算符、實例建構函式、完成項、靜態建構函式或區域函式的宣告可能包含 unsafe 修飾詞,在此情況下,該成員宣告的整個文字範圍會被視為不安全的內容。
  • unsafe_statement可讓您在區塊使用不安全的內容。 相關聯 區塊 的整個文字範圍會被視為不安全的內容。 在不安全內容中宣告的本機函式本身不安全。

相關聯的文法延伸模組如下所示,並在後續的子控件中顯示。

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

範例:在下列程式代碼中

public unsafe struct Node
{
    public int Value;
    public Node* Left;
    public Node* Right;
}

結構 unsafe 宣告中指定的修飾詞會導致結構宣告的整個文字範圍變成不安全的內容。 因此,可以宣告 LeftRight 欄位為指標類型。 上述範例也可以寫入

public struct Node
{
    public int Value;
    public unsafe Node* Left;
    public unsafe Node* Right;
}

在這裡, unsafe 欄位宣告中的修飾詞會導致這些宣告被視為不安全的內容。

end 範例

除了建立不安全的內容,因此允許使用指標類型之外, unsafe 修飾詞不會影響類型或成員。

範例:在下列程式代碼中

public class A
{
    public unsafe virtual void F() 
    {
        char* p;
        ...
    }
}

public class B : A
{
    public override void F() 
    {
        base.F();
        ...
    }
}

A 方法的 F unsafe 修飾詞只會造成 的文字範圍F變成不安全的內容,其中可以使用語言的不安全功能。 在中的覆寫 FB,不需要重新指定 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一樣。

end 範例

unsafe當修飾詞用於部分類型宣告時({15.2.7),只有該特定部分會被視為不安全的內容。

23.3 指標類型

在不安全的內容中,類型 (~8.1) 可以是pointer_type,也可以value_type、reference_typetype_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~23.2)。 non_array_type是本身不是array_type的任何類型。

與參考(參考型別的值)不同,垃圾收集行程不會追蹤指標,垃圾收集行程不知道指標及其指向的數據。 基於這個理由,指標不允許指向參考或包含參考的結構,而指標的引用類型應該是 unmanaged_type。 指標類型本身是 Unmanaged 類型,因此指標類型可用來做為另一個指標類型的引用類型。

混合指標和參考的直覺規則是,允許參考(物件)的引用包含指標,但不允許參考的指標包含參考。

範例:下表提供指標類型的一些範例:

範例 說明
byte* 指標 byte
char* 指標 char
int** 指標的指標 int
int*[] 指向的單一維度指標陣列 int
void* 未知類型的指標

end 範例

對於指定的實作,所有指標類型都應該具有相同的大小和表示法。

注意:不同於 C 和 C++,當多個指標在相同的宣告中宣告時,在 C# * 中只會與基礎類型一起寫入,而不是在每個指標名稱上做為前置標點符號。 例如:

int* pi, pj; // NOT as int *pi, *pj;  

end note

具有 型 T* 別的指標值代表 型 T別變數的位址。 指標間接運算子 *^23.6.2) 可用來存取此變數。

範例:假設類型int*為 的變數P,表達式*P表示int在 中包含的P位址中找到的變數。 end 範例

就像物件參考一樣,指標可能是 null。 將間接運算子套用至 null-valued 指標會導致實作定義的行為(^23.6.2)。 具有值的 null 指標會以all-bits-zero表示。

void* 類型代表未知類型的指標。 因為引用型別未知,所以間接運算符無法套用至類型的 void*指標,也無法對這類指標執行任何算術。 不過,型 void* 別的指標可以轉換成任何其他指標類型(反之亦然),相較於其他指標類型的值(23.6.8)。

指標類型是不同的類型類別。 不同於參考型別和實值型別,指標型別不會繼承自 object ,而且指標型別與 object之間沒有轉換。 特別是,指標不支援Boxing和unboxing(\8.3.13)。 不過,在不同的指標類型與指標類型與整數類型之間,允許轉換。 這在 •23.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.7)。

任何指標類型的預設值 (~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
        }
    }
}

end note

方法可以傳回某些類型的值,而該類型可以是指針。

範例:當指定連續序列的 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;
}

end 範例

在不安全的內容中,有數個建構可用於在指標上操作:

  • 一元 * 運算子可用來執行指標間接取值(^23.6.2)。
  • ->運算子可用來透過指標存取結構的成員(^23.6.3)。
  • []運算子可用來編製指標的索引(^23.6.4)。
  • 一元 & 運算子可用來取得變數的位址(^23.6.5)。
  • ++-- 運算子可用來遞增和遞減指標(~23.6.6)。
  • 二元 +- 運算子可用來執行指標算術(~23.6.7)。
  • ==!=<>、 、 <=>= 運算子可用來比較指標 (~23.6.8)。
  • stackalloc運算子可用來從呼叫堆疊配置記憶體(^23.9)。
  • fixed語句可用來暫時修正變數,以便取得其位址(~23.7)。

23.4 已修正和可移動的變數

運算子位址 (^23.6.5) 和 fixed 語句 (^23.7) 會將變數分成兩個類別:固定變數可移動的變數。

固定變數位於垃圾收集行程作業未受影響的儲存位置。 (固定變數的範例包括局部變數、值參數,以及取值指標所建立的變數。另一方面,可移動的變數位於垃圾收集行程受限於重新配置或處置的儲存位置。 (可移動變數的範例包括物件和陣列元素中的欄位。

&運算子 (^23.6.5) 允許取得固定變數的位址,而不受限制。 不過,由於可移動的變數受限於垃圾收集行程的重新配置或處置,因此只能使用 fixed statement{23.7) 取得可移動變數的位址,而且該位址只有在該語句的 fixed 持續時間內才有效。

確切地說,固定變數是下列其中一項:

  • 除非匿名函式擷取變數,否則由 參考局部變數、值參數或參數陣列的simple_name~12.8.4)產生的變數(\12.19.6.2)。
  • 窗體V.I的 member_access~12.8.7) 產生的變數,其中 V 是struct_type固定變數。
  • 窗體的pointer_indirection_expression~23.6.2)、窗體*P的pointer_member_access(~23.6.3)或窗體P[E]P->I的pointer_element_access\23.6.4) 產生的變數。

所有其他變數都會分類為可移動的變數。

靜態欄位會分類為可移動的變數。 此外,參考參數會分類為可移動的變數,即使為參數指定的自變數是固定變數也一樣。 最後,取值指標所產生的變數一律分類為固定變數。

23.5 指標轉換

23.5.1 一般

在不安全的內容中,會擴充一組可用的隱含轉換(~10.2),以包含下列隱含指標轉換:

  • 從任何 pointer_type 到 型別 void*
  • null從常值 (~6.4.5.7) 到任何pointer_type

此外,在不安全的內容中,會擴充一組可用的明確轉換(~10.3),以包含下列明確指標轉換:

  • 從任何 pointer_type 到任何其他 pointer_type
  • sbytebyteshort、、intushortuintlongulong 到任何pointer_type
  • 從任何 pointer_typesbytebyteshortushortint、、 uintlongulong

最後,在不安全的內容中,標準隱含轉換集 (~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
}

end 範例

當指標類型轉換成 的 byte指標時,結果會指向變數尋址的最低位址 byte 。 結果的後續遞增,最多為變數的大小,產生該變數剩餘位元組的指標。

範例:下列方法會將 中的 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"

end 範例

指標和整數之間的對應是實作定義的。

注意:不過,在具有線性位址空間的 32 位和 64 位 CPU 架構上,對整數型別或整數型別的指標轉換通常與或值的轉換uintulong完全相同。 end note

23.5.2 指標數組

在不安全的內容中 ,可以使用array_creation_expression #12.8.17.5)來建構指標陣列。 指標陣列只允許套用至其他數位型態的一些轉換:

  • 從任何array_type到的隱含參考轉換(~10.2.8),以及其實作的介面也會套用至指標陣列。System.Array 不過,任何嘗試透過 System.Array 或它實作的介面存取陣列專案,都可能會導致運行時間發生例外狀況,因為指標類型無法轉換成 object
  • 從單一維度陣列類型到 的隱含和明確參考轉換(~10.2.8\10.3.5),以及其泛型基底介面永遠不會套用至指標陣列。System.Collections.Generic.IList<T> S[]
  • System.Array明確參考轉換 (~10.3.5) 和其實作至任何array_type的介面會套用至指標陣列。
  • System.Collections.Generic.IList<S> 及其基底介面到單一維度陣列類型的T[]明確參考轉換 (~10.3.5)永遠不會套用至指標陣列,因為指標類型不能當做類型自變數使用,而且不會從指標類型轉換成非指標類型。

這些限制表示語句的擴充 foreach 不能套用至指標陣列中所 描述的陣列。9.4.4.17 。 相反地, foreach 表單的語句

foreach (V v in x)embedded_statement

其中 的型別是窗體 T[,,...,]的陣列類型xn 是維度數目減 1,TV是指針類型,會使用巢狀 for-loops 展開,如下所示:

{
    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變數 、 、 、 i0i1... inembedded_statement或任何其他程式源代碼都看不到或無法存取x。 變數 v 在內嵌語句中是唯讀的。 如果沒有從 T (元素類型) 到 V的明確轉換 (~23.5),則會產生錯誤,而且不會採取任何進一步的步驟。 如果 x 具有 值 nullSystem.NullReferenceException 則會在執行時間擲回 。

注意:雖然指標類型不允許做為類型自變數,但指標陣列可以當做類型自變數使用。 end note

23.6 運算式中的指標

23.6.1 一般

在不安全的內容中,表達式可能會產生指標類型的結果,但在不安全的內容之外,表達式是指針類型的編譯時期錯誤。 確切地說,在不安全的內容之外,如果有任何simple_name(•12.8.4)、member_access(\12.8.7)、invocation_expression(\12.8.10)或element_access\12.8.12)為指標類型,就會發生編譯時間錯誤。

在不安全的內容中 ,primary_no_array_creation_expression\12.8) 和 unary_expression\12.9) 生產環境允許額外的建構,如下列子專案所述。

注意:不安全運算符的優先順序和關聯性是由文法所隱含。 end note

23.6.2 指標間接取值

pointer_indirection_expression由星號 (*) 組成,後面接著unary_expression

pointer_indirection_expression
    : '*' unary_expression
    ;

一元 * 運算子表示指標間接取值,並用來取得指標指向的變數。 評估 *P的結果,其中 P 是指針類型的 T*表達式,是 類型的 T變數。 將一元 * 運算子套用至類型 void* 或不是指標類型的表達式,是編譯時期錯誤。

將一元 * 運算子套用至 null-valued 指標的效果是實作定義。 特別是,不保證此作業會 System.NullReferenceException擲回 。

如果已將無效的值指派給指標,則未定義一元 * 運算符的行為。

注意:在一元*運算符取值指標的無效值中,有一個位址對指向的類型不適當對齊(請參閱 <23.5 中的範例),以及變數在其存留期結束后的位址。

為了明確指派分析的目的,評估窗體 *P 表達式所產生的變數會被視為一開始指派 (~9.4.2)。

23.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。 如需指標間接運算符的描述(*),請參閱 ^23.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).IMain 因此方法可能同樣撰寫:

class Test
{
    static void Main()
    {
        Point point;
        unsafe
        {
            Point* p = &point;
            (*p).x = 10;
            (*p).y = 20;
            Console.WriteLine((*p).ToString());
        }
    }
}

end 範例

23.6.4 指標元素存取

pointer_element_access是由primary_no_array_creation_expression所組成,後面接著以 “” 和 “[]” 括住的表達式。

pointer_element_access
    : primary_no_array_creation_expression '[' expression ']'
    ;

在窗體 P[E]的指標專案存取中, P 應該是指標 void*型別的表達式,而且 E 應該是可以隱含轉換成 intuintlongulong的表達式。

表單 P[E] 的指標專案存取會完全評估為 *(P + E)。 如需指標間接運算符的描述(*),請參閱 ^23.6.2。 如需指標加法運算符的描述(+),請參閱 <23.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;
            }
        }
    }
}

end 範例

指標元素存取運算符不會檢查界限外的錯誤,而且未定義存取超出界限元素時的行為。

注意:這與 C 和 C++ 相同。 end note

23.6.5 運算子位址

addressof_expression由安培德 (&) 組成,後面接著unary_expression

addressof_expression
    : '&' unary_expression
    ;

假設表達式 E 屬於型 T 別,並分類為固定變數 (~23.4),建構 &E 會計算 所 E指定變數的位址。 結果的類型是 T* ,並分類為值。 如果未E分類為變數、如果分類為只讀局部變數,或EE表示可移動的變數,就會發生編譯時期錯誤。 在最後一個案例中,固定語句 (~23.7) 可用來暫時「修正」變數,再取得其位址。

注意:如 •12.8.7 中所述,定義欄位之結構或類別readonly的實例建構函式或靜態建構函式外部,該字段會被視為值,而不是變數。 因此,無法取得其位址。 同樣地,無法取得常數的位址。 end note

&運算元不需要明確指派其自變數,但在作業之後&,套用運算符的變數會被視為在作業發生所在的執行路徑中明確指派。 程序設計人員有責任確保實際上確實進行正確的變數初始化。

範例:在下列程式代碼中

class Test
{
    static void Main()
    {
        int i;
        unsafe
        {
            int* p = &i;
            *p = 123;
        }
        Console.WriteLine(i);
    }
}

i在用來初始化 p的作業之後&i,會被視為明確指派。 實際上的指派 *pi初始化 ,但包含此初始化是程式設計人員的責任,如果移除指派,則不會發生任何編譯時間錯誤。

end 範例

注意:運算子的明確指派 & 規則存在,因此可以避免局部變數的備援初始化。 例如,許多外部 API 會取得 API 所填入結構指標。 對這類 API 的呼叫通常會傳遞本機結構變數的位址,而且若沒有規則,則需要結構變數的備援初始化。 end note

注意:當匿名函式擷取局部變數、值參數或參數陣列時({12.8.24),局部變數、參數或參數數位不再被視為固定變數({23.7),而是被視為可移動的變數。 因此,任何不安全的程式代碼都擷取匿名函式所擷取之局部變數、值參數或參數數位的位址是錯誤的。 end note

23.6.6指標遞增和遞減

在不安全的內容中,++和運算符 (~12.8.16\12.9.6) 可以套用至除 以外的void*所有--類型的指標變數。 因此,針對每個指標類型 T*,會隱含定義下列運算符:

T* operator ++(T* x);
T* operator --(T* x);

運算符會分別產生與 x+1x-1相同的結果(~23.6.7)。 換句話說,對於類型的 T*指標變數, ++ 運算符會加入 sizeof(T) 變數中包含的位址,而 -- 運算符會從變數中包含的位址減去 sizeof(T)

如果指標遞增或遞減作業溢位指標類型的定義域,則結果為實作定義,但不會產生例外狀況。

23.6.7 指標算術

在不安全的內容中 + ,運算符 (^12.10.5) 和 - 運算符 (^12.10.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);

指定指標型T*別的運算式P,以及 型int別為 、uintlongulong的運算式P + NN,並N + P計算型別的指標值,這些值T*是由 新增N * sizeof(T)至 所指定位址所P產生。 同樣地,表達式P – N會計算型T*別的指標值,該值會從 所P指定的位址減去N * sizeof(T)

給定兩個運算式和 P Q指標類型的 T*, 表示式 P – Q 會計算 所 P 指定地址之間的差異, Q 然後將該差異除以 sizeof(T)。 結果的類型一律 long為 。 實際上, P - Q 會計算為 ((long)(P) - (long)(Q)) / sizeof(T)

範例:

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

end 範例

如果指標算術運算溢位指標類型的定義域,結果會以實作定義的方式截斷,但不會產生任何例外狀況。

23.6.8 指標比較

在不安全的內容中 ==,可以將 、 !=<><=>= 運算符 (~12.12) 套用至所有指標類型的值。 指標比較運算符如下:

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* 型別,因此可以使用這些運算符來比較任何指標類型的操作數。 比較運算符會比較兩個操作數所指定的位址,就像是無符號整數一樣。

23.6.9 sizeof 運算符

對於某些預先定義的類型(^12.8.19), sizeof 運算符會產生常數值 int 。 對於所有其他類型,運算符的結果是實作 sizeof 定義,並分類為值,而不是常數。

成員封裝到結構的順序未指定。

為了對齊目的,結構開頭、結構內和結構結尾可能會有未命名的填補。 用來填補的位內容不確定。

當套用至具有結構類型的操作數時,結果是該類型變數中的位元組總數,包括任何填補。

23.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存取。 固定語句所宣告的局部變數會被視為唯讀的。 如果內嵌語句嘗試修改此局部變數(透過指派或 ++-- 運算符),或將它當做參考或輸出參數傳遞,就會發生編譯時期錯誤。

在fixed_pointer_initializer中使用擷取的局部變數(•12.19.6.2)、值參數或參數數位是錯誤的。 fixed_pointer_initializer可以是下列其中一項:

  • 標記 「&後面接著 unmanaged 類型的T可移動變數 (~23.4),後面接著variable_reference\9.5),前提是類型T*可以隱含轉換成語句中指定的fixed指標類型。 在此情況下,初始化表達式會計算指定變數的位址,而且變數保證會在固定語句的持續時間維持在固定位址。
  • 具有 Unmanaged 型T別專案的array_type表示式,前提是型T*別可以隱含轉換成固定語句中提供的指標類型。 在此情況下,初始化表達式會計算數位中第一個項目的位址,而且整個數位保證在語句的持續時間 fixed 內維持在固定位址。 如果數位表示式為 null 或陣列具有零個元素,初始化表達式就會計算等於零的位址。
  • string別 的表達式,前提是型 char* 別可隱含轉換成 語句中所 fixed 指定的指標類型。 在此案例中,初始化表達式會計算字串中第一個字元的位址,而且保證整個字串在語句的持續時間 fixed 會維持在固定位址。 如果字串表示式為 null,則語句的行為fixed是實作定義。
  • array_type 或 string以外的類型表達式,前提是存在符合簽章ref [readonly] T GetPinnableReference()的可存取方法或可存取的擴充方法,其中 Tunmanaged_type,而且T*會隱含轉換成 語句中指定的fixed指標類型。 在此情況下,初始化表達式會計算傳回變數的位址,而且該變數保證會在語句的持續時間 fixed 內維持在固定位址。 GetPinnableReference()當多載解析 (~12.6.4) 只產生一個函式成員,且該函式成員符合上述條件時,語句可以使用 fixed 方法。 GetPinnableReference方法應該傳回地址的參考等於零,例如當沒有要釘選資料時所System.Runtime.CompilerServices.Unsafe.NullRef<T>()傳回的 。
  • 參考可移動變數之固定大小的緩衝區成員的simple_namemember_access,前提是固定大小的緩衝區成員的類型可以隱含轉換成 語句中指定的fixed指標類型。 在此情況下,初始化表達式會計算固定大小緩衝區 ({23.8.3) 第一個專案的指標,而且固定大小緩衝區保證在語句持續期間 fixed 維持在固定位址。

針對由 fixed_pointer_initializer 計算的每個位址fixed語句可確保地址所參考的變數不會受限於語句持續時間fixed的垃圾收集行程重新配置或處置。

範例:如果fixed_pointer_initializer計算的地址參考物件或陣列實例的元素欄位,固定語句會保證包含的物件實例不會在語句存留期間重新放置或處置。 end 範例

程序設計人員有責任確保固定語句所建立的指標不會在執行這些語句之後生存下來。

範例:當語句所 fixed 建立的指標傳遞至外部 API 時,程式設計人員有責任確保 API 不會保留這些指標的記憶體。 end 範例

固定物件可能會造成堆積的片段(因為它們無法移動)。 基於這個理由,只有在絕對必要時,才應該修正物件,然後只儘可能縮短時間量。

範例:範例

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,在語句中fixed指定 a[0] 與只指定 a一樣。

end 範例

在不安全的內容中,單一維度陣列的陣列元素會以遞增的索引順序儲存,從索引 0 開始,並以索引 Length – 1結尾。 對於多維度陣列,陣列元素會儲存,讓最右邊維度的索引先增加,再增加下一個左維度,依此類移。

fixed在取得陣列實例a指標p的語句中,從 開始p + a.Length - 1表示陣列中專案位址的指標值p。 同樣地,從 p[0] 開始代表 p[a.Length - 1] 實際陣列元素的變數。 鑒於數位儲存方式,任何維度的陣列都可以視為線性。

範例:

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

end 範例

範例:在下列程式代碼中

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語句可用來修正陣列,以便將其地址傳遞至接受指標的方法。

end 範例

char*修正字串實例所產生的值一律指向 Null 終止的字串。 在取得字串實例指標p的固定語句中,從 開始p + s.Length ‑ 1表示字元串中字元位址的指標值p,而指標值p + s.Length一律指向 Null 字元(值為 '\0' 的s字元)。

範例:

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);
        }
    }
}

end 範例

範例:下列程式代碼顯示fixed_pointer_initializer,其表達式的類型為 array_typestring

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)
        {
            // ...
        }
    }
}

類型 C 具有具有正確簽章的可存取 GetPinnableReference 方法。 在語句中fixed,在 上c呼叫該方法時,從該方法傳回的 ,用來初始化int*指標 pref intend 範例

透過固定指標修改 Managed 類型的物件可能會導致未定義的行為。

注意:例如,因為字串是不可變的,所以程式設計人員有責任確保固定字串指標所參考的字元不會修改。 end note

注意:呼叫預期「C 樣式」字串的外部 API 時,自動終止字串特別方便。 不過請注意,允許字串實例包含 Null 字元。 如果存在這類 Null 字元,當視為 Null 終止 char*時,字串將會出現截斷。 end note

23.8 固定大小緩衝區

23.8.1 一般

固定大小的緩衝區可用來將內嵌數位宣告為結構的成員,而且主要用於與 Unmanaged API 的介面。

23.8.2 固定大小的緩衝區宣告

固定大小緩衝區是一個成員,代表指定類型之變數之固定長度緩衝區的記憶體。 固定大小的緩衝區宣告引進指定項目類型的一或多個固定大小緩衝區。

注意:如同數位,固定大小的緩衝區可以視為包含元素。 因此,針對數位定義的字詞 元素類型 也會與固定大小的緩衝區搭配使用。 end note

固定大小緩衝區只能在結構宣告中允許,而且可能只發生在不安全的內容中({23.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 ']'
    ;

固定大小的緩衝區宣告可能包含一組屬性 ({22)、修飾詞 ({15.3.5)、對應至結構成員允許的任何宣告輔助功能修飾詞 ({16.4.3) 和unsafe修飾詞 ({23.2)。new 屬性和修飾詞會套用至固定大小緩衝區宣告所宣告的所有成員。 同一個修飾詞在固定大小的緩衝區宣告中出現多次是錯誤的。

不允許固定大小的緩衝區宣告包含 static 修飾詞。

固定大小緩衝區宣告的buffer元素類型會指定宣告所引進之buffer(s) 的元素類型。 緩衝區項目類型應該是其中一個預先定義的類型sbytebyteuintshortlongintushortulongfloatchardouble或 。bool

緩衝區專案類型後面接著固定大小的緩衝區宣告子清單,每個宣告子都會引進新的成員。 固定大小的緩衝區宣告子是由名稱成員的標識碼所組成,後面接著括在 和 ] 標記中的[常數表達式。 常數表達式表示該固定大小緩衝區宣告子所引進之成員中的元素數目。 常數表達式的類型應隱含轉換成 類型 int,而值應為非零正整數。

固定大小的緩衝區元素應該循序配置在記憶體中。

宣告多個固定大小緩衝區的固定大小緩衝區宣告相當於具有相同屬性和專案類型的單一固定大小緩衝區宣告的多個宣告。

範例:

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];
}

end 範例

23.8.3 表達式中的固定大小緩衝區

固定大小緩衝區成員的成員查閱 (~12.5) 會繼續與欄位的成員查閱完全相同。

您可以使用simple_name(12.8.4)、member_access(~12.8.7)或element_access\12.8.12)來參考固定大小緩衝區。

當固定大小緩衝區成員參考為簡單名稱時,效果會與窗體 this.I的成員存取相同,其中 I 是固定大小的緩衝區成員。

在可以是隱含 this.之表單E.IE.的成員存取中,如果 E 結構類型為 ,且該結構類型中的成員查閱I會識別固定大小成員,則會E.I評估並分類如下:

  • 如果表達式 E.I 未在不安全的內容中發生,則會發生編譯時期錯誤。
  • 如果 E 分類為值,則會發生編譯時期錯誤。
  • 否則,如果 E 是可移動的變數(~23.4),則:
  • 否則,E參考固定變數,而表達式的結果是 中E固定大小緩衝區成員I之第一個專案的指標。 結果的類型為 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);
    }
}

end 範例

23.8.4 明確指派檢查

固定大小緩衝區不會受限於明確的指派檢查(~9.4),而固定大小的緩衝區成員則為了明確指派檢查結構類型變數而遭到忽略。

當包含固定大小緩衝區成員之結構變數的最外層是靜態變數、類別實例的實例變數或數位元素時,固定大小的緩衝區元素會自動初始化為其預設值 (\9.3)。 在其他所有情況下,固定大小緩衝區的初始內容是未定義的。

23.9 堆疊配置

如需運算子 stackalloc的一般資訊,請參閱 <12.8.22>。 在這裡,討論該運算符產生指標的能力。

在不安全的內容中,如果發生stackalloc_expression\12.8.22) 的初始化表達式,local_variable_declaration的初始化表達式\13.6.2),其中local_variable_type是指針類型 (\23.3) 或推斷的 (var),則stackalloc_expression的結果是要開始配置區塊的T *型別指標, 其中 Tstackalloc_expression的unmanaged_type

在其他所有方面,local_variable_declarations (&13.6.2) 和不安全內容中的stackalloc_expressions (~12.8.22) 語意遵循針對安全內容定義的語意。

範例:

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 };
}

end 範例

不同於對數位或 stackalloc類型 『ed 區塊的存取,對 』ed 區塊指標 Span<T> 類型的專案 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();
    }
}

end 範例

有條件地規範文字的結尾。