共用方式為


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

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

C# 支援 unsafe 上下文,在其中您可以編寫 無法驗證的 程式碼。 在 unsafe 內容中,程式代碼可以使用指標、配置和釋放記憶體區塊,以及使用函式指標呼叫方法。 C# 中的不安全程式代碼不一定很危險;這隻是無法驗證其安全性的程序代碼。

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 表示在 myVariable中所含位址找到的 int 變數。

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。 任何指標類型都可以使用轉換表達式明確轉換成任何其他指標類型。 您也可以將任何整數類型轉換成指標類型,或將任何指標類型轉換成整數類型。 這些轉換需要明確的型別轉換。

下列範例會將 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: {0}", number);

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

固定大小緩衝區

您可以使用 fixed 關鍵詞,在數據結構中建立具有固定大小陣列的緩衝區。 當您撰寫方法與其他語言或平台的數據源互作時,固定大小緩衝區很有用。 固定大小緩衝器可以接受通常允許用於結構成員的任何屬性或修飾詞。 唯一的限制是陣列類型必須是 boolbytecharshortintlongsbyteushortuintulongfloatdouble

private fixed char name[30];

在安全程式代碼中,包含數位列的 C# 結構不包含數位元素。 結構會改為包含元素的參考。 在 不安全 的程式碼區塊中使用時,您可以將固定大小的陣列內嵌在 結構體 中。

下列 struct 的大小不取決於陣列中的元素數目,因為 pathName 是參考類型:

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

結構可以在不安全的代碼中包含內嵌陣列。 在下列範例中,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[]

如何使用指標來複製位元組陣列

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

這個範例會使用不安全的 關鍵詞,這可讓您在 Copy 方法中使用指標。 固定 語句是用來宣告來源和目的地陣列的指標。 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
    */
}

函式指標

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* 的方法(或返回 delegate*)只能在 unsafe 上下文中呼叫。
  • static 函式允許使用 & 運算符以取得函式位址。 (此規則同時適用於成員函式和區域函式。

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

您可以使用 關鍵詞 managedunmanaged來指定 delegate* 的呼叫慣例。 此外,針對 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# 語言規格

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