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

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

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

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

小提示

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

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

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

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

兩種不安全程式碼模型

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

  • 原始不安全模型:上下文 unsafe 涵蓋指標 特徵的存在。 你可以宣告指標型別、取變數位址、取消參考指標、將表達式轉換stackalloc成指標,或只在上下文unsafe中套用sizeof到任意型別。 (在安全碼中允許指 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(預覽版),並將編譯器選項設LangVersionpreview。 指標放寬是用 C# 15 編譯器和 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* pp 是整數的指標。
  • int** pp 是整數指標的指標。
  • int*[] pp 是整數指標的單維陣列。
  • char* pp 是char的指標。
  • void* pp 是未知類型的指標。

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

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。 將間接運算子應用於空指標會導致實作定義的行為。

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

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

運算子/語句
* 執行指標的間接取值。
-> 透過指標存取結構的成員。
[] 為指標編製索引。
& 取得變數的位址。
++-- 遞增和遞減指標。
+- 執行指標算術。
==!=<><=>= 對比指標。
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 。 當您撰寫方法與其他語言或平台的數據源互作時,固定大小緩衝區很有用。 固定大小緩衝區可以接受任何一般結構成員允許的屬性或修飾符。 唯一的限制是陣列類型必須為 boolbytefloatulonguintcharsbyteshortintlongushortdouble

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.AutoCharSet = 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。 下列程式代碼會宣告使用 delegatedelegate* 來合併相同類型的兩個物件的方法。 第一個方法會使用 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 呼叫慣例:CdeclStdcallFastcallThiscall。 最後一個宣告會使用 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 編譯器選項來編譯。

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

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 中的預覽功能。 它會根據預覽版期間的回饋持續演進。 要試用這個模型,請使用 .NET 11(預覽版)SDK 並將編譯器選項設LangVersionpreview。 .NET 11 預覽版 5 的編譯器實作了指標的放寬,但尚未強制執行呼叫者義務、組合語言的選擇加入或關鍵字。safe 完整設計請參見 記憶體安全功能規範

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

來電者-不安全成員

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

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

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

在以下預覽模型中,來 ReadInt32 電者是否不安全? 簽名攜帶 unsafe 修飾符,內部 unsafe 區塊包裹去參照:

// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
    unsafe
    {
        return *(int*)source;
    }
}

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

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

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

  • unsafe修飾符會在型別宣告、靜態建構子和終結器上產生錯誤,因為修飾符沒有呼叫者可通知。
  • 代表不能是 unsafe,因為代表是型態的。
  • 一個無參數建構子的 unsafe 型別不符合該 new() 限制。

需要不安全上下文的操作

存取指向記憶體的操作需要上下文 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 區塊執行不安全操作。 包含內部 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> 區塊說明正式合約:呼叫者必須遵守的條件。 分析器可以標記缺少一個來電者不安全成員。
  • 區塊內unsafe// SAFETY:註解記錄了該處操作合理的原因,供開發商和審核員閱讀正文。

以下預覽模型展示了兩種風格在呼叫者不安全(caller-unsafe ReadByte )方法下的表現:

// 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 並不代表其後方現場呼叫者不安全。 在一個結構體中,標記每個欄位 [StructLayout(LayoutKind.Explicit)],要標記 safeunsafe

保險箱關鍵字

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

成員 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 ,並且會將義務傳播給呼叫者。 省略兩個修飾符會造成錯誤,迫使你做出安全決策。 結構體中明確配置的欄位也使用相同規則。

選擇加入與跨組裝行為

更新後的模型包含兩個獨立的專案層級交換器:

  • 一個新的選擇加入權是根據更新後的規則來決定的。 當該物業關閉時,原始規則仍然適用。 當它開啟時, unsafe 成員會傳達給呼叫者,編譯器會用屬性記錄組合語言 MemorySafetyRulesAttribute 中的選擇。
  • 現有的 AllowUnsafeBlocks 屬性會限制關鍵字的每次出現 unsafe ,包括呼叫站的內部區塊。 預設是 false,所以專案預設時不能呼叫任何不安全的 API。

這兩種性質結合如下:

選擇加入的權利 AllowUnsafeBlocks Result
開啟 關閉(預設) 最安全的配置。 專案採用更新後的模型,且不允許任何不安全的程式碼。
開啟 開啟 專案採用更新後的模型,允許不安全的程式碼。
關閉 關閉 原始模型適用,專案無法使用指標類型。
關閉 開啟 原始模型適用,專案可以使用指標類型。

是否會對另一議會執行更新規則,取決於哪一方選擇加入:

  • 更新模型呼叫者,更新模型呼叫者:被呼叫者的標記會 unsafe 透過元資料傳送。 呼叫者會將每通電話給來電者不安全成員的通話封裝成區 unsafe 塊。
  • 更新模型呼叫者,原始模型被叫者:相容模式將任何標記中指標類型的被叫者視為不安全,因此呼叫站點需要一個包含的 unsafe 區塊。 此模式避免指標式 API 無聲中失去 unsafe 其需求。
  • 原始模型呼叫者,更新模型呼叫者:原始指標規則仍然適用。 若 caller-unsafe 成員的簽名中沒有指標型別,則可從安全碼中呼叫,因為原始模型的呼叫者無法讀取新的標記。

C# 語言規格

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

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

另請參閱