不安全的程式代碼、指標類型和函式指標

你寫的大部分 C# 程式碼都是可驗證安全的。 可驗證安全代碼表示.NET工具能驗證該代碼是否安全。 一般來說,安全程式碼不會直接使用指標存取記憶體。 它也不會配置未經處理的記憶體。 它會改為建立受管理的物件。

C# 語言參考資料記錄了 C# 語言最新版本。 同時也包含即將推出語言版本公開預覽功能的初步文件。

文件中標示了語言最近三個版本或目前公開預覽版中首次引入的任何功能。

小提示

欲查詢某功能何時首次在 C# 中引入,請參閱 C# 語言版本歷史的條目。

C# 也支援一個 unsafe 上下文,讓你能寫 出無法驗證 的程式碼。 不安全的程式碼不一定危險;它只是程式碼,安全性無法被 .NET 工具驗證。 使用不安全的程式碼呼叫需要指標的原生函式,有時則透過直接記憶體存取來提升效能,避免陣列邊界檢查。 不安全的程式碼也會帶來安全與穩定性風險。 若要編譯包含 unsafe 上下文的程式碼,請新增 AllowUnsafeBlocks 編譯器選項。

C# 定義了兩種不安全程式碼模型:原始模型和更新後的記憶體安全模型,後者在 C# 15 和 .NET 11 預覽中。 關於兩種模型的差異,請參閱 「兩種不安全程式碼模型」。

如需 C# 中不安全程式碼最佳做法的相關資訊,請參閱 不安全的程式碼最佳做法。

兩種不安全程式碼模型

C# 定義了兩種不安全程式碼模型。 模型實際上決定哪些操作需要上下文 unsafe ,以及成員修飾符如何 unsafe 影響呼叫者。

  • 原始不安全模型:上下文 unsafe 涵蓋指標 特徵的存在。 你可以宣告指標型別、取變數位址、取消參考指標、將表達式轉換stackalloc成指標,或只在上下文sizeof中套用unsafe到任意型別。 (在安全碼中允許指 stackalloc 派給 Span<T> 或 ReadOnlySpan<T> 的表達式。) unsafe 對型別、成員或區塊的修飾符建立該上下文,但對呼叫者不施加義務。 C# 1.0 引入了此模型,至今仍是預設。
  • 更新的記憶體安全模型:上下文 unsafe 涵蓋執行 時無法管理的記憶體操作。 指標的存在並不危險;指標的去參照是。 unsafe成員的修飾符會成為合約,將審計安全義務傳達給來電者。 此模型目前在 C# 15 和 .NET 11 中預覽。

下表比較了每個模型中哪些操作需要上下文 unsafe 。

運算 原始型號 更新模型
宣告指標型別或取一個位址 & 需求 unsafe 在安全碼中允許
fixed 陳述式 需求 unsafe 在安全碼中允許
將表達 stackalloc 式轉換成指標 需求 unsafe 在安全碼中允許
sizeof任意非管理型態的運算子 需求 unsafe 在安全碼中允許
指標間接存取(*p)、成員存取(p->m或元素存取)p[i] 需求 unsafe 需求 unsafe
函式指標調用 需求 unsafe 需求 unsafe
固定大小緩衝區上的元素存取 需求 unsafe 需求 unsafe
請呼叫標記為會員的成員 unsafe 沒有來電要求 需求 unsafe

要啟用預覽語法與指標放鬆,請使用 .NET 11 SDK 並將編譯器選項設LangVersion為 preview。 若要啟用更新後的規則,包括呼叫者義務,請參見 啟用更新後的記憶體安全規則。

原始不安全型號

在原始模型中, unsafe 關鍵字會在類型、成員或區塊上建立一個不安全的上下文,而該上下文會解鎖以下章節中描述的指標特徵。 unsafe修飾符只改變標記代碼能做的事;對呼叫者沒有要求。 要編譯這些範例,請設定 AllowUnsafeBlocks 編譯器選項。

指標類型

在不安全的情境中,型別除了值型態或參考型態外,也可以是指標型別。 指標類型宣告採用下列其中一種形式:

type* identifier;
void* identifier; //allowed but not recommended

你在指標 * 類型中指定前的類型是 參考類型。

指標類型不會繼承自 物件,指標類型與 object之間也沒有轉換。 此外,Boxing 和 unboxing 不支持指標。 不過,您可以在不同的指標類型與指標類型和整數類型之間轉換。

當你在同一宣告中宣告多個指標時,請只寫上星號(*)和底層型別。 它不會作為每個指標名稱的前置詞。 例如:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

垃圾收集器不會追蹤物件是否由任何指標類型所指向。 如果參照對象是管理堆積中的物件(包括由 lambda 表達式或匿名代理捕捉的本地變數),你必須在指標使用期間釘 選 該物件。

類型 MyType* 的指標變數值是 類型 MyType變數的位址。 以下是指針類型宣告的範例:

  • int* p:p 是整數的指標。
  • int** p:p 是整數指標的指標。
  • int*[] p:p 是整數指標的單維陣列。
  • char* p:p 是char的指標。
  • void* p:p 是未知類型的指標。

你可以使用指標間接運算 * 子來存取指標變數指向的位置的內容。 例如,請考慮下列宣告:

int* myVariable;

表達式 *myVariable 表示在 int中所含位址找到的 myVariable 變數。

在 fixed 語句的文章中有幾個指標範例,。 下列範例使用 unsafe 關鍵詞和 fixed 語句,並示範如何遞增內部指標。 您可以將此程式碼貼到主控台應用程式的 Main 函式中,以執行它。 這些範例必須使用 AllowUnsafeBlocks 編譯程式選項集進行編譯。

// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

您無法將間接運算子套用至類型 void*的指標。 不過,您可以使用型別轉換將 void 指標轉換成任何其他指標類型,反過來亦然。

指標可以是 null。 將間接運算子應用於空指標會導致實作定義的行為。

在方法之間傳遞指標可能會導致未定義的行為。 請考慮透過 in、out或 ref 參數或函式結果傳回局部變數指標的方法。 如果指標是在固定區塊中設定,它指向的變數可能不再固定。

下表列出可在不安全內容中的指標上運作的運算符和語句:

運算子/語句 用
* 執行指標的間接取值。
-> 透過指標存取結構的成員。
[] 為指標編製索引。
& 取得變數的位址。
++ 和 -- 遞增和遞減指標。
+ 和 - 執行指標算術。
==、!=、<、>、<=和 >= 對比指標。
stackalloc 在堆疊上配置記憶體。
fixed 聲明 暫時修正變數,以便找到其位址。

如需指標相關運算子的詳細資訊,請參閱 指標相關運算子。

任何指標類型都可以隱含地轉換成 void* 類型。 任何指標類型都可以被賦予值 null。 你可以用 cast 表達式明確地將任何指標類型轉換成其他指標類型。 您也可以將任何整數類型轉換成指標類型,或將任何指標類型轉換成整數類型。 這些轉換需要明確的型別轉換。

下列範例會將 int* 轉換成 byte*。 請注意,指標指向變數的最低尋址位元組。 當您逐步增加結果時,可以累加到 int 的大小(4 個位元組),以顯示變數的剩餘位元組。

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine($"The value of the integer: {number}");

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

固定大小緩衝區

陣列是參考型別,所以在安全程式碼中,結構體欄位(陣列)只會儲存對陣列元素的參考,而非元素本身。 以下 struct 的大小不取決於陣列中元素的數量,因為 pathName 是一個參考:

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

若要將陣列內容儲存在結構體本身,請使用 fixed 關鍵字宣告 固定大小的緩衝區。 關鍵字 fixed 需要上下文 unsafe 。 當您撰寫方法與其他語言或平台的數據源互作時,固定大小緩衝區很有用。 固定大小緩衝區可以接受任何一般結構成員允許的屬性或修飾符。 唯一的限制是陣列類型必須為 bool, byte, charshortintlongsbyteushortuintulongfloat:double

private fixed char name[30];

在下列範例中,fixedBuffer 數位列的大小固定。 你用 fixed 一個陳述 式取得指向第一個元素的指標,然後透過該指標存取陣列中的元素。 此 fixed 陳述會將實例欄位釘 fixedBuffer 在記憶體中的特定位置:

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

陣列 char 128 個元素的大小為 256 個字節。 不論編碼方式為何,固定大小 字元 緩衝區一律需要每個字元 2 個字節。 即使字元緩衝區被封送至帶有 CharSet = CharSet.Auto 或 CharSet = CharSet.Ansi的 API 方法或結構,這個陣列大小也保持不變。 如需詳細資訊,請參閱 CharSet。

上述範例示範存取 fixed 欄位而不釘選。 另一個常見的固定大小陣列是 bool 陣列。 bool 陣列中的元素大小一律為1位元組。 bool 陣列不適合建立位數組或緩衝區。

固定大小的緩衝區會使用 System.Runtime.CompilerServices.UnsafeValueTypeAttribute編譯,其會指示 Common Language Runtime (CLR) 類型包含可能溢位的 Unmanaged 陣列。 使用 stackalloc 分配的記憶體也會自動啟用 CLR 中的緩衝區超載偵測功能。 前述範例說明固定大小緩衝區如何存在於 unsafe struct。

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

編譯程式為 Buffer 產生的 C# 屬性如下:

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

固定大小緩衝區與一般陣列不同,方式如下:

  • 你只能在特定 unsafe 情境中使用它們。
  • 它們只能是結構體的實例欄位。
  • 它們一律是向量或一維陣列。
  • 宣告必須包含長度,例如 fixed char id[8]。 您無法使用 fixed char id[]。

函式指標

C# 提供 delegate 類型來定義安全函式指針對象。 叫用委派牽涉到實例化衍生自 System.Delegate 的類型,並對其 Invoke 方法進行虛擬方法呼叫。 此虛擬呼叫會使用 callvirt IL 指令。 在效能關鍵程式代碼路徑中,使用 calli IL 指令更有效率。

你可以用語 delegate* 法來定義函式指標。 編譯器透過指令 calli 呼叫函式,而非實 delegate 例化物件並呼叫 Invoke。 下列程式代碼會宣告使用 delegate 或 delegate* 來合併相同類型的兩個物件的方法。 第一個方法會使用 System.Func<T1,T2,TResult> 委派類型。 第二種方法使用具有相同參數和傳回類型的 delegate* 宣告:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

以下程式碼展示了如何宣告靜態本地函式,並透過指向該本地函式的指標來呼叫該 UnsafeCombine 方法:

int product = 0;
unsafe
{
    static int localMultiply(int x, int y) => x * y;
    product = UnsafeCombine(&localMultiply, 3, 4);
}

上述程式代碼說明了透過函式指標存取函式的數個規則:

  • 你只能在上下文 unsafe 中宣告函式指標。
  • 你只能呼叫在上下文delegate*中取 a delegate* (或回傳 aunsafe)的方法。
  • 僅& 函式允許使用 static 運算符以取得函式位址。 此規則同時適用於成員函數與局部函數。

語法類似於宣告 delegate 型別並使用指標。 * 上的 delegate 後綴表示宣告是 函式指標。 將方法群組指派給函式指標時,& 表示作業會採用 方法的位址。

你可以透過關鍵字 delegate* 和 managed來指定 a unmanaged 的呼叫慣例。 此外,針對 unmanaged 函式指標,您可以指定呼叫慣例。 下列宣告顯示每個的範例。 第一個宣告會使用呼叫慣例 managed,這是預設值。 接下來的四個會使用 unmanaged 呼叫慣例。 每個都會指定 ECMA 335 呼叫慣例:Cdecl、Stdcall、Fastcall或 Thiscall。 最後一個宣告會使用 unmanaged 呼叫慣例,指示 CLR 挑選平臺的預設呼叫慣例。 CLR 會在運行時間選擇呼叫慣例。

public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

你可以在 C# 語言規範的 函數指標 章節中了解更多關於函數指標的資訊。

範例:使用指標複製一組位元組陣列

下列範例會使用指標,將位元組從一個陣列複製到另一個陣列。

這個範例使用 unsafe 關鍵字,讓你能在方法中使用 Copy 指標。 該 fixed 語句宣告指向來源陣列與目的陣列的指標。 fixed 語句 固定 記憶體中來源和目的地陣列的位置,以防止垃圾收集將陣列移動。 該 fixed 區塊將該區塊範圍內陣列的記憶體區塊釘選。 由於 Copy 此範例中的方法使用 unsafe 關鍵字,您必須使用 AllowUnsafeBlocks 編譯器選項來編譯。

此範例透過使用索引而非第二個未管理指標,存取兩個陣列的元素。 pSource 和 pTarget 指標變數的宣告會固定陣列。

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

更新後的記憶體安全模型(預覽)

Important

更新後的記憶體安全模型是 C# 15 和 .NET 11 中的預覽功能。 關於啟用說明,請參見 啟用更新的記憶體安全規則。 完整設計請參見 記憶體安全功能規範。

更新後的模型將原始模型視為一體的兩件事分離:指標碼的 存在 與安全義務向呼叫者的 傳遞 。 標記成員 unsafe 不再只是讓指標進入其身體;這會使成員 需求不安全,因此每位來電者必須要麼傳播該義務,要麼在經過驗證且安全可叫出的邊界後方解除。 為了支持這種分離,模型也縮小了不安全的上下文:指標的存在本身並不安全,只有執行時無法管理存取記憶體的操作。 縮窄功能讓你能在安全程式碼中持有、傳遞和回傳指標,同時 unsafe 標記實際可能違反記憶體安全的操作和成員。

需要-不安全成員

在原始模型中, unsafe 成員的修飾符只允許在成員的簽名和主體中放置指標。 它不會告知來電者安全相關資訊。 更新後的模型賦予呼叫者這個修飾符的意義。 當你標記成員 unsafe時,編譯器會將其視為 需求不安全:每個呼叫者必須從上下文 unsafe 中呼叫該成員,且審核安全義務轉移到該呼叫者身上。

unsafe成員簽名上的修飾符不再為該實體建立不安全的上下文。 這兩個角色分開了:

  • unsafe簽名上的修飾符會將義務傳遞給呼叫者。
  • 內部 unsafe 區塊負責存取未管理記憶體的操作範圍。

以下預覽範例中,是 ReadInt32 requires-unsafe。 簽名攜帶 unsafe 修飾符,內部 unsafe 區塊包裹去參照:

// Preview: requires the updated-memory-safety-rules feature.
public static unsafe int ReadInt32(byte* source)
{
    unsafe
    {
        return *(int*)source;
    }
}

呼叫者會將通話包裝成自己的 unsafe 區塊:

// Preview
unsafe
{
    int value = ReadInt32(buffer);
}

更新後的模型也收緊了幾項相關規則:

  • unsafe修飾符會在型別宣告、靜態建構子和終結器上產生錯誤,因為修飾符沒有呼叫者可通知。
  • 代表不能是 unsafe,因為代表是型態的。
  • 無參數建構子為 的 unsafe 型別無法在宣告中滿足 new() OR struct 約束,例如基底型別或欄位型別。 在表達式中,它可以在不安全上下文中滿足該限制,例如 unsafe { var box = new Box<NativeResource>(); } 當 Box<T> 擁有 new() 約束且 NativeResource 有 需要-不安全建構子時。 此檢查適用於通用型別與方法實例,包括擴充區塊與擴充方法型態參數。 不安全區塊可以在表達式使用時滿足需求,但它無法修正其他地方的無效基底型、欄位型別或別名宣告。 關於宣告專屬的修正,請參見 CS9376。

需要不安全上下文的操作

存取指向記憶體的操作需要上下文 unsafe :

  • 指標間接存取(*p)、指標成員存取(p->member)、指標元素存取(p[i])。
  • 函式指標調用。
  • 固定大小緩衝區上的元素存取。

以下範例釘選一個沒有 unsafe 上下文的陣列,但卻取消了其中指標的參考:

public static int ReadValue(int[] numbers)
{
    fixed (int* first = numbers)
    {
        // Dereferencing a pointer accesses unmanaged memory, so it still
        // requires an unsafe context.
        unsafe
        {
            return *first;
        }
    }
}

寬鬆操作

不存取指向記憶體的操作不再需要上下文 unsafe :

  • 宣告指標型別並用運算 & 子取變數位址。
  • 就是 fixed 用來釘住變數的陳述。
  • 將 stackalloc 表達式轉換成指標。
  • 運算子 sizeof 適用於任何未管理型態。

以下範例建立並釘選指標,無需上下文 unsafe :

public static void CreatePointer()
{
    int value = 42;
    // Creating a pointer doesn't require an unsafe context.
    int* pointer = &value;
    int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
    // The fixed statement no longer requires an unsafe context.
    fixed (int* first = numbers)
    {
        int* current = first;
    }
}

這些放寬措施適用於你編譯語言 preview 版本時,無論組合是否選擇加入更新的記憶體安全規則。

不安全表達

一個 unsafe 表達式 unsafe(expression),會為評估單一表達式創造一個不安全的上下文。 它在 unsafe 區塊無法在語法上出現的情況非常有用,例如欄位初始化器、建構子初始化器或 when 子句的 catch 子句。 不安全上下文在括號結束,因此不延伸至包含語句的其他部分。

以下範例是將欄位初始化器內的指標去參照,該位置無法包含 unsafe 區塊:

// A field initializer can't contain an unsafe block, but it can contain an
// unsafe expression. The unsafe context ends at the closing parenthesis.
public static readonly int Signature = unsafe(ReadSignature());

private static unsafe int ReadSignature()
{
    int rawValue = 0x1234;
    int* pointer = &rawValue;
    return *pointer;
}

表達 unsafe 式也讓你能將不安全的上下文範圍集中在一個運算元 await上,而不是把整個 await 表達式包裹在一個 unsafe 區塊裡。 該 await 本身不在不安全上下文中;只有產生等待任務的呼叫會運行於該上下文中:

public static async Task<int> ReadSignatureAsync()
{
    // The unsafe expression scopes the unsafe context to the call that
    // produces the task. The 'await' itself stays outside that context.
    return await unsafe(ReadSignatureCoreAsync());
}

private static unsafe Task<int> ReadSignatureCoreAsync()
{
    int rawValue = 0x1234;
    int* pointer = &rawValue;
    return Task.FromResult(*pointer);
}

和其他 unsafe 程式碼一樣,unsafe表達式需要 AllowUnsafeBlocks 編譯器選項,且需要語言版本。preview

解除需要不安全的義務

會員若呼叫要求不安全作業,有兩個選擇:執行義務或解除。

  • 傳播:標記你自己的成員 unsafe。 責任會轉移給你的來電者。 當你無法完全自己驗證義務時,就使用傳播。
  • 出院:請將會員簽名保管妥當。 在成員內部驗證義務,通常使用執行時守衛,然後在內部 unsafe 區塊執行不安全操作。 包含內部 unsafe 區塊但不標記自身簽名 unsafe 的成員是 不安全的邊界:它會將不安全的程式碼變成安全可呼叫的表面。

以下預覽模型會用守衛驗證輸入,釘選管理陣列,並讀取指標。 來電者不需要上下文 unsafe ,因為這個方法已經履行了義務:

// Preview
public static int SumBytes(byte[] source)
{
    ArgumentNullException.ThrowIfNull(source);

    fixed (byte* first = source)
    {
        unsafe
        {
            // SAFETY: the null check and source.Length bound every read to the pinned array.
            int total = 0;
            for (int i = 0; i < source.Length; i++)
            {
                total += first[i];
            }

            return total;
        }
    }
}

空檢查和陣列長度排除了會讓讀取通過緩衝區的輸入,因此區塊內 unsafe 的去參照是合理的。 此方法不留殘餘義務,因此暴露出安全可呼叫的簽名。

安全文件

要求不安全的會員應記錄來電者必須保證的事項。 更新後的模型鼓勵兩種互補的評論風格:

  • 簽名上方的文件 /// <safety> 區塊說明正式合約:呼叫者必須遵守的條件。 分析儀可以標記缺少一個不 安全的 成員。
  • 區塊內// SAFETY:的unsafe註解記錄了該處操作合理的原因,供開發商和審核員閱讀正文。

以下預覽範例展示了兩種樣式在 requires-unsafeReadByte 方法上的表現:

// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
    byte* address = (byte*)ptr;
    unsafe
    {
        // SAFETY: relies on the caller obligation stated in the <safety> block.
        return address[offset];
    }
}

/// <safety>區塊告訴你合約內容。 合約應該放在所有來電者和審核者都能看到的文件中。

不安全場

當欄位宣告的類型沒有表達該包覆型別維護的合約,而其他程式碼依賴該合約時,可以使用 unsafe 修飾符。 不安全存在於類型系統所見與類型承諾之間的落差。 修飾符會強制每次寫入欄位, unsafe 形成一個區塊,讓寫入能集中在一個地方審查。

最明顯的情況是包含原生指標的欄位。 指標不會像 a System.Span<T> 那樣宣告它位址的位元組數,因此包含的型別會自行維護這些資訊:

// Preview
public class NativeBuffer
{
    /// <safety>
    /// Null, or points to a buffer of Length bytes.
    /// </safety>
    private unsafe byte* _pointer;

    public int Length { get; }

    public byte ReadAt(int index)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
        unsafe
        {
            // SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
            return _pointer[index];
        }
    }
}

欄位 readonly unsafe 將合約與內建保護配對: unsafe 命名不變量,並 readonly 防止寫入在構造後可能破壞該守衛。 標記一個房產或活動 unsafe 並不代表其背地 不安全。

在更新後的規則下,該類型中的每個實例欄位都標記為明確或擴展版面配置safeunsafe。 這些類型以 為 LayoutKind.Explicit,或 StructLayoutAttribute 標記為 ExtendedLayoutAttribute。 即使使用序列或自動排版,也可以對非零或非零StructLayoutAttribute.PackStructLayoutAttribute.Size的實例欄位套用相同的修飾符。 未指定或零值本身不會觸發額外需求。 靜態場和恆定場則不適用。 註解合成支持場的擁有屬性或事件。

關於隱式產生儲存的診斷與修正,請參見 CS9392:現場佈局安全。

保險箱關鍵字

更新後的模型新增了一個 safe 情境關鍵字,證明宣告在編譯器要求你明確做出選擇時是合理的。

普通宣告預設是安全的,所以不需要修 safe 飾符。 由於 unsafe 程式碼應該很稀有,大多數程式碼庫只在預設不安全或編譯器需要明確選擇時使用 safe 。 修飾符將宣告標記為非要求-不安全;它不會建立安全的詞彙語境,也不會取消包圍的不安全語境。 你無法合併 safe ,並且 unsafe 在同一個聲明上。 在較新的編譯器中,宣unsafesafe告修飾符都要求 AllowUnsafeBlocks 選項,獨立於更新規則強制的選擇加入。 若無此功能,宣告修飾符會產生 CS9401,而不安全的區塊則持續產生 CS0227。 Roslyn 記錄了 Visual Studio 2026 版本 18.13 中引入的修飾鍵檢查,接續 .NET 11.0.100。 .NET 11 編譯器不需要這個選項,safe並使用 CS0227 作為unsafe宣告修飾符。 詳情請參見 不安全上下文需求 。 記憶體安全模型本身仍是預覽功能。

成員 extern 會呼叫原生程式碼,因此編譯器無法對其安全性進行分類。 在更新後的模型中,你標記每個 extern 宣告,包括 LibraryImport 部分方法,可以標記 safe 為 或 unsafe:

// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);

getpid 不取參數並回傳原始碼,因此作者證明呼叫是安全的,呼叫者無需儀式地使用。 strlen 會取一個原始指標,而原生程式碼會將其去引用,因此宣告是 unsafe ,並且會將義務傳播給呼叫者。 省略兩個修飾符會造成錯誤,迫使你做出安全決策。 在具有明確或擴展配置的類型中,欄位也會使用相同的規則。

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格的 Unsafe 程式代碼 一章。

關於更新後記憶體安全模型的設計,請參閱 記憶體安全功能規範。

另請參閱