Unsicherer Code, Zeigertypen und Funktionszeiger

Der großteil des von Ihnen geschriebenen C#-Codes ist sicherer Code. Verifizierbar sicheren Code bedeutet, dass .NET Tools überprüfen können, ob der Code sicher ist. Im Allgemeinen greift sicherer Code nicht direkt mithilfe von Zeigern auf den Speicher zu. Außerdem ordnet er keinen unformatierten Arbeitsspeicher zu. Stattdessen werden verwaltete Objekte erstellt.

Die C#-Sprachreferenz dokumentiert die zuletzt veröffentlichte Version der C#-Sprache. Außerdem enthält sie erste Dokumentation für Features in der öffentlichen Vorschau für die kommende Sprachversion.

In der Dokumentation werden alle Features identifiziert, die in den letzten drei Versionen der Sprache oder in der aktuellen öffentlichen Vorschau eingeführt wurden.

Tipp

Informationen dazu, wann ein Feature erstmals in C# eingeführt wurde, finden Sie im Artikel zum Versionsverlauf der C#-Sprache.

C# unterstützt auch einen unsafe Kontext, in dem Sie unverifizierbaren Code schreiben können. Unsicherer Code ist nicht unbedingt gefährlich; Es ist nur Code, dessen Sicherheit nicht von .NET Tools überprüft werden kann. Sie verwenden unsicheren Code, um systemeigene Funktionen aufzurufen, die Zeiger erfordern, und in einigen Fällen die Leistung durch direkten Speicherzugriff zu verbessern, der Array-gebundene Prüfungen verhindert. Unsicherer Code führt auch zu Sicherheits- und Stabilitätsrisiken. Um Code zu kompilieren, der einen unsafe Kontext enthält, fügen Sie die Compileroption AllowUnsafeBlocks hinzu.

C# definiert zwei Modelle für unsicheren Code: das ursprüngliche Modell und ein aktualisiertes Speichersicherheitsmodell, das sich in der Vorschau in C# 15 und .NET 11 befindet. Informationen dazu, wie sich die beiden Modelle unterscheiden, finden Sie unter Zwei Modelle für unsicheren Code.

Informationen zu bewährten Methoden für unsicheren Code in C# finden Sie unter Bewährte Methoden für unsicheren Code.

Zwei Modelle für unsicheren Code

C# definiert zwei Modelle für unsicheren Code. Das Modell bestimmt, welche Vorgänge einen unsafe Kontext erfordern und wie sich der unsafe Modifizierer eines Elements auf Aufrufer auswirkt.

  • Ursprüngliches unsicheres Modell: Der unsafe Kontext deckt das Vorhandensein von Zeigerfeatures ab. Sie deklarieren einen Zeigertyp, leiten die Adresse einer Variablen ab, leiten einen Zeiger ab, konvertieren einen stackalloc Ausdruck in einen Zeiger oder wenden sie nur innerhalb eines Kontexts auf einen unsafe beliebigen Typ ansizeof. (Ein stackalloc Ausdruck, der einem oder ReadOnlySpan<T> einem Span<T> sicheren Code zugewiesen ist.) Der unsafe Modifizierer für einen Typ, ein Mitglied oder ein Block legt diesen Kontext fest, legt jedoch keine Verpflichtung für Anrufer fest. In C# 1.0 wurde dieses Modell eingeführt, und es bleibt die Standardeinstellung.
  • Aktualisiertes Speichersicherheitsmodell: Der unsafe Kontext behandelt die Vorgänge, die auf den Arbeitsspeicher zugreifen, die von der Laufzeit nicht verwaltet werden. Das Vorhandensein eines Zeigers ist nicht unsicher; die Ableitung eines Zeigers ist. Der unsafe Modifizierer für ein Mitglied wird zu einem Vertrag, der die Verpflichtung zur Überwachung der Sicherheit an den Anrufer weitergibt. Dieses Modell befindet sich in der Vorschau in C# 15 und .NET 11.

In der folgenden Tabelle wird verglichen, welche Vorgänge in jedem Modell einen unsafe Kontext erfordern.

Vorgang Originalmodell Aktualisiertes Modell
Deklarieren eines Zeigertyps oder Annehmen einer Adresse mit & Erfordert unsafe Zulässig im sicheren Code
Die Anweisung fixed Erfordert unsafe Zulässig im sicheren Code
Konvertieren eines stackalloc Ausdrucks in einen Zeiger Erfordert unsafe Zulässig im sicheren Code
Der sizeof Operator für einen beliebigen nicht verwalteten Typ Erfordert unsafe Zulässig im sicheren Code
Zeigerreferenz (*p), Memberzugriff (p->m) oder Elementzugriff (p[i]) Erfordert unsafe Erfordert unsafe
Funktionszeigeraufruf Erfordert unsafe Erfordert unsafe
Elementzugriff auf einen Puffer mit fester Größe Erfordert unsafe Erfordert unsafe
Mitglied anrufen, das markiert ist unsafe Keine Anruferanforderung Erfordert unsafe

Verwenden Sie zum Testen des aktualisierten Modells das .NET 11 SDK (in der Vorschau), und legen Sie die LangVersion Compileroption auf preview. Die Zeigerentspannungen gelten immer dann, wenn Sie mit dem C# 15-Compiler und der preview Sprachversion kompilieren. Die vollständige Erzwingung, einschließlich Anruferverpflichtungen und der Versammlungs-Opt-In, wird noch entwickelt. Weitere Informationen finden Sie im aktualisierten Speichersicherheitsmodell (Vorschau).

Das ursprüngliche unsichere Modell

Im ursprünglichen Modell stellt das unsafe Schlüsselwort einen unsicheren Kontext für einen Typ, ein Element oder einen Block her, und dieser Kontext entsperrt die in den folgenden Abschnitten beschriebenen Zeigerfeatures. Der unsafe Modifizierer ändert nur, was der markierte Code tun kann; er stellt keine Anforderung für Aufrufer. Um eines dieser Beispiele zu kompilieren, legen Sie die Compileroption AllowUnsafeBlocks fest.

Zeigertypen

In einem unsicheren Kontext kann ein Typ ein Zeigertyp sein, zusätzlich zu einem Werttyp oder einem Verweistyp. Eine Zeigertypdeklaration akzeptiert eine der folgenden Formen:

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

Der Typ, den Sie vor dem * Zeigertyp angeben, ist der Referenttyp.

Zeigertypen erben nicht vom Objekt, und zwischen Zeigertypen und object. Weiterhin unterstützen Boxing und Unboxing keine Zeiger. Sie können jedoch zwischen verschiedenen Zeigertypen und zwischen Zeigertypen und integralen Typen konvertieren.

Wenn Sie mehrere Zeiger in derselben Deklaration deklarieren, schreiben Sie das Sternchen (*) nur mit dem zugrunde liegenden Typ. Sie wird nicht als Präfix für jeden Zeigernamen verwendet. Zum Beispiel:

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

In der Garbage Collection wird nicht nachgehalten, ob von einem der Zeigertypen auf ein Objekt verwiesen wird. Wenn der Referent ein Objekt im verwalteten Heap ist (einschließlich lokaler Variablen, die von Lambda-Ausdrücken oder anonymen Delegaten erfasst werden), müssen Sie das Objekt so lange anheften , wie der Zeiger verwendet wird.

Der Wert der Zeigervariable vom Typ MyType* ist die Adresse einer Variablen vom Typ MyType. Im Folgenden sind Beispiele für Zeigertypdeklarationen aufgeführt:

  • int* p: p ist ein Zeiger auf eine ganze Zahl.
  • int** p: p ist ein Zeiger auf einen Zeiger auf einen ganzzahligen Wert.
  • int*[] p: p ist ein eindimensionales Array von Zeigern auf ganze Zahlen.
  • char* p: p ist ein Zeiger auf eine char-Variable.
  • void* p: p ist ein Zeiger auf einen unbekannten Typ.

Sie können den Zeigerdereferenzierungsoperator * verwenden, um auf den Inhalt an der Position zuzugreifen, auf die die Zeigervariable verweist. Betrachten Sie beispielsweise die folgende Deklaration:

int* myVariable;

Der Ausdruck *myVariable bezeichnet die int Variable, die in der Adresse myVariablegespeichert ist.

Es gibt mehrere Beispiele für Zeiger in den Artikeln zur fixed-Anweisung. Im folgenden Beispiel wird das Schlüsselwort unsafe und die fixed-Anweisung verwendet, und es wird gezeigt, wie ein Innenzeiger inkrementiert wird. Sie können diesen Code in die Hauptfunktion einer Konsolenanwendung einfügen, um ihn auszuführen. Diese Beispiele müssen mit dem AllowUnsafeBlocks Compileroptionssatz kompiliert werden.

// 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
*/

Sie können den Dereferenzierungsoperator nicht auf einen Zeiger vom Typ void*anwenden. Sie können jedoch eine Umwandlung verwenden, um einen void-Zeiger in einen anderen Zeigertyp und umgekehrt zu konvertieren.

Ein Zeiger kann nullsein. Das Anwenden des Dereferenzierungsoperators auf einen Nullzeiger bewirkt ein implementierungsdefiniertes Verhalten.

Das Übergeben von Zeigern zwischen Methoden kann zu einem nicht definierten Verhalten führen. Erwägen Sie eine Methode, die einen Zeiger auf eine lokale Variable über eine in, outoder ref Parameter oder als Funktionsergebnis zurückgibt. Wenn der Zeiger in einem festen Block gesetzt wurde, ist die Variable, auf die er zeigt, möglicherweise nicht mehr fest.

In der folgenden Tabelle sind die Operatoren und Anweisungen aufgeführt, die auf Zeigern in einem unsicheren Kontext ausgeführt werden können:

Operator/Anweisung Zweck
* Führt eine Zeigerdereferenzierung aus.
-> Greift über einen Zeiger auf ein Element einer Struktur zu.
[] Indiziert einen Zeiger.
& Ruft die Adresse einer Variablen ab.
++ und -- Inkrementiert und dekrementiert Zeiger.
+ und - Führt Zeigerarithmetik aus.
==, !=, <, >, <=und >= Vergleicht Zeiger miteinander.
stackalloc Belegt Speicher für den Stapel.
fixed-Anweisung Sperrt vorübergehend eine Variable, damit ihre Adresse ermittelt werden kann.

Weitere Informationen zu zeigerbezogenen Operatoren finden Sie unter Zeiger-Operatoren.

Jeder Zeigertyp kann implizit in einen void* Typ konvertiert werden. Jedem Zeigertyp kann der Wert nullzugewiesen werden. Sie können jeden Zeigertyp explizit in einen anderen Zeigertyp konvertieren, indem Sie einen Umwandlungsausdruck verwenden. Sie können auch jeden integralen Typ in einen Zeigertyp oder einen beliebigen Zeigertyp in einen integralen Typ konvertieren. Diese Konvertierungen erfordern eine explizite Umwandlung.

Im folgenden Beispiel wird ein int* in eine byte*konvertiert. Beachten Sie, dass der Zeiger auf das niedrigste adressierte Byte der Variablen zeigt. Wenn Sie das Ergebnis nacheinander erhöhen, können Sie bis zur Größe von int (4 Byte) die verbleibenden Bytes der Variablen anzeigen.

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
    */
}

Puffer fester Größe

Arrays sind Referenztypen, daher speichert ein Strukturfeld, das ein Array ist, nur einen Verweis auf die Elemente des Arrays, nicht die Elemente selbst. Die Größe der folgenden struct Elemente hängt nicht von der Anzahl der Elemente im Array ab, da pathName es sich um einen Verweis handelt:

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

Um den Inhalt des Arrays innerhalb der Struktur selbst zu speichern, verwenden Sie das fixed Schlüsselwort, um einen Puffer mit fester Größe zu deklarieren. Für das fixed Schlüsselwort ist ein unsafe Kontext erforderlich. Puffer mit fester Größe sind nützlich, wenn Sie Methoden schreiben, die mit Datenquellen aus anderen Sprachen oder Plattformen zusammenarbeiten. Ein Puffer mit fester Größe kann alle Attribute oder Modifizierer verwenden, die für reguläre Strukturmember zulässig sind. Die einzige Einschränkung besteht darin, dass der Arraytyp bool: , byte, , char, short, longint, sbyte, uintulongushortfloatoder :double

private fixed char name[30];

Im folgenden Beispiel weist das fixedBuffer Array eine feste Größe auf. Sie verwenden eine fixed Anweisung , um einen Zeiger auf das erste Element abzurufen, und greifen dann über diesen Zeiger auf die Elemente des Arrays zu. Mit fixed der Anweisung wird das fixedBuffer Instanzfeld an einen bestimmten Speicherort im Arbeitsspeicher angeheftet:

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

Die Größe des 128-Elements char Arrays beträgt 256 Byte. char-Puffer mit fester Größe verwenden unabhängig von der Codierung immer 2 Bytes pro Zeichen. Diese Arraygröße bleibt unverändert, auch wenn Zeichenpuffer an API-Methoden oder -Strukturen mit CharSet = CharSet.Auto oder CharSet = CharSet.Ansiübergeben werden. Weitere Informationen finden Sie unter CharSet.

Im obigen Beispiel wird der Zugriff auf fixed-Felder ohne Fixieren veranschaulicht. Ein weiteres gängiges Array mit fester Größe ist das bool Array. Die Elemente in einem bool Array sind immer 1 Byte groß. bool Arrays eignen sich nicht zum Erstellen von Bitarrays oder Puffern.

Puffer mit fester Größe werden mit dem System.Runtime.CompilerServices.UnsafeValueTypeAttributekompiliert, wodurch die Common Language Runtime (CLR) angewiesen wird, dass ein Typ ein nicht verwaltetes Array enthält, das potenziell überlaufen kann. Der mithilfe von stackalloc zugewiesene Speicher ermöglicht auch automatisch Pufferüberlauferkennungsfeatures in der CLR. Das vorangehende Beispiel zeigt, wie ein Puffer mit fester Größe in einem unsafe struct.

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

Das vom Compiler generierte C# für Buffer wird wie folgt zugeordnet:

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

Puffer mit fester Größe unterscheiden sich von regulären Arrays auf folgende Weise:

  • Sie können sie nur in einem unsafe Kontext verwenden.
  • Sie können nur Instanzfelder von Strukturen sein.
  • Sie sind immer Vektoren oder eindimensionale Arrays.
  • Die Deklaration muss die Länge enthalten, z fixed char id[8]. B. . Sie können fixed char id[]nicht verwenden.

Funktionszeiger

C# stellt delegate Typen zum Definieren sicherer Funktionszeigerobjekte bereit. Das Aufrufen eines Delegaten umfasst das Instanziieren eines von System.Delegate abgeleiteten Typs und das Ausführen eines virtuellen Methodenaufrufs für dessen Invoke-Methode. Dieser virtuelle Aufruf verwendet die callvirt IL-Anweisung. Bei leistungskritischen Codepfaden ist die Verwendung der calli IL-Anweisung effizienter.

Sie können einen Funktionszeiger mithilfe der delegate* Syntax definieren. Der Compiler ruft die Funktion mithilfe der calli Anweisung auf, anstatt ein delegate Objekt zu instanziieren und aufzurufen Invoke. Der folgende Code deklariert zwei Methoden, die eine delegate oder eine delegate* verwenden, um zwei Objekte desselben Typs zu kombinieren. Die erste Methode verwendet einen Delegattyp vom Typ System.Func<T1,T2,TResult>. Die zweite Methode verwendet eine delegate*-Deklaration mit denselben Parametern und rückgabetyp:

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

Der folgende Code zeigt, wie Sie eine statische lokale Funktion deklarieren und die UnsafeCombine Methode mithilfe eines Zeigers auf diese lokale Funktion aufrufen:

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

Der vorangehende Code veranschaulicht mehrere der Regeln für die Funktion, auf die als Funktionszeiger zugegriffen wird:

  • Sie können Funktionszeiger nur in einem unsafe Kontext deklarieren.
  • Sie können nur Methoden aufrufen, die eine delegate* (oder eine delegate*) in einem unsafe Kontext zurückgeben.
  • Der & Operator zum Abrufen der Adresse einer Funktion ist nur für static Funktionen zulässig. Diese Regel gilt sowohl für Memberfunktionen als auch für lokale Funktionen.

Die Syntax weist Parallelen zum Deklarieren von delegate-Typen und zur Verwendung von Zeigern auf. Das * Suffix für delegate gibt an, dass die Deklaration ein Funktionszeigerist. Die & beim Zuweisen einer Methodengruppe zu einem Funktionszeiger gibt an, dass der Vorgang die Adresse der Methode verwendet.

Sie können die Anrufkonvention für eine delegate* angeben, indem Sie die Schlüsselwörter managed und unmanageddie . Darüber hinaus können Sie für unmanaged Funktionszeiger die aufrufende Konvention angeben. Die folgenden Deklarationen zeigen Beispiele für jede. Die erste Deklaration verwendet die managed Aufrufkonvention, die standard ist. Die nächsten vier verwenden die unmanaged-Aufrufkonvention. Jede gibt eine der ECMA 335-Anrufkonventionen an: Cdecl, Stdcall, Fastcalloder Thiscall. Die letzte Deklaration verwendet die unmanaged Aufrufkonvention und weist die CLR an, die Standardanrufkonvention für die Plattform zu wählen. Die CLR wählt die Aufrufkonvention zur Laufzeit aus.

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

Weitere Informationen zu Funktionszeigern finden Sie im Abschnitt "Funktionszeiger " der C#-Sprachspezifikation.

Beispiel: Verwenden von Zeigern zum Kopieren eines Bytearrays

Im folgenden Beispiel werden Zeiger verwendet, um Bytes aus einem Array in ein anderes zu kopieren.

In diesem Beispiel wird das unsafe Schlüsselwort verwendet, mit dem Sie Zeiger in der Copy Methode verwenden können. Die fixed Anweisung deklariert Zeiger auf die Quell- und Zielarrays. Diese fixed-Anweisung heftet den Speicherort des Quell- und Zielarrays im Speicher an, damit die Speicherbereinigung keine Arrays verschiebt. Der fixed Block pinsiert die Speicherblöcke für die Arrays im Bereich des Blocks. Da die Copy Methode in diesem Beispiel das unsafe Schlüsselwort verwendet, müssen Sie es mithilfe der Compileroption AllowUnsafeBlocks kompilieren.

In diesem Beispiel wird auf die Elemente beider Arrays zugegriffen, indem Indizes anstelle eines zweiten nicht verwalteten Zeigers verwendet werden. Die Deklaration der Zeiger pSource und pTarget heftet die Arrays an.

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
    */
}

Das aktualisierte Speichersicherheitsmodell (Vorschau)

Important

Das aktualisierte Speichersicherheitsmodell ist ein Vorschaufeature in C# 15 und .NET 11. Sie entwickelt sich weiterhin basierend auf Feedback während der Vorschauversionen. Um das Modell zu testen, verwenden Sie das .NET 11 (Vorschau)-SDK, und legen Sie die LangVersion Compileroption auf preview. Der Compiler in .NET 11 Preview 5 implementiert die Zeigerentspannungen, erzwingt aber noch nicht die Aufruferverpflichtungen, die Assembly-Opt-In oder das safe Schlüsselwort. Den vollständigen Entwurf finden Sie in der Spezifikation der Speichersicherheitsfeatures.

Das aktualisierte Modell trennt zwei Dinge, die das ursprüngliche Modell als eins behandelt: das Vorhandensein von Zeigercode und die Weitergabe von Sicherheitsverpflichtungen an Aufrufer. Das Kennzeichnen eines Elements unsafe erlaubt nicht mehr nur Zeiger im Textkörper. Er macht den Memberaufrufer unsicher, sodass jeder Aufrufer diese Verpflichtung weitergeben oder hinter einer validierten, sicher bewählbaren Grenze entlasten muss. Um diese Trennung zu unterstützen, schränkt das Modell auch den unsicheren Kontext ein: Das Vorhandensein eines Zeigers ist nicht sicher, nur die Vorgänge, die auf den Arbeitsspeicher zugreifen, die die Laufzeit nicht verwaltet. Mit der Verengung können Sie Zeiger im sicheren Code halten, übergeben und zurückgeben, während unsafe die Vorgänge und Member markiert werden, die tatsächlich die Speichersicherheit verletzen können.

Unsichere Anrufermitglieder

Im ursprünglichen Modell lässt der unsafe Modifizierer für ein Element nur Zeiger in der Signatur und im Textkörper des Elements zu. Sie informiert Anrufer nicht über Die Sicherheit. Das aktualisierte Modell gibt dem Modifizierer die Bedeutung für Anrufer. Wenn Sie ein Mitglied unsafemarkieren, behandelt der Compiler es als aufruferunsicher (auch als "requires-unsicher" bezeichnet): Jeder Aufrufer muss es aus einem unsafe Kontext aufrufen, und die Verpflichtung zur Überwachung der Sicherheit wird zu diesem Aufrufer verschoben.

Der unsafe Modifizierer für eine Membersignatur stellt keinen unsicheren Kontext für den Textkörper mehr her. Die beiden Rollen teilen sich:

  • Der unsafe Modifizierer für die Signatur gibt die Verpflichtung an Aufrufer weiter.
  • Ein innerer unsafe Block beschreibt die Vorgänge, die auf nicht verwalteten Speicher zugreifen.

Im folgenden Vorschau-Modell ReadInt32 ist caller-unsafe. Die Signatur trägt den unsafe Modifizierer, und ein innerer unsafe Block umschließt die Ableitung:

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

Ein Anrufer umschließt den Anruf in seinem eigenen unsafe Block:

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

Das aktualisierte Modell verschärft auch einige verwandte Regeln:

  • Der unsafe Modifizierer erzeugt einen Fehler bei einer Typdeklaration, einem statischen Konstruktor und einem Finalizer, da der Modifizierer keinen Aufrufer zur Information hat.
  • Stellvertretungen können nicht sein unsafe, da eine Stellvertretung typförmig ist.
  • Ein Typ, dessen parameterloser Konstruktor unsafe die new() Einschränkung nicht erfüllt.

Vorgänge, die einen unsicheren Kontext erfordern

Vorgänge, die auf den verweisenden Speicher zugreifen, erfordern einen unsafe Kontext:

  • Zeigerreferenzion (*p), Zeigermemmzugriff (p->member) und Zeigerelementzugriff (p[i]).
  • Funktionszeigeraufruf.
  • Elementzugriff auf einen Puffer mit fester Größe.

Im folgenden Beispiel wird ein Array ohne unsafe Kontext angeheftet, der Zeiger wird jedoch innerhalb eines Arrays abgeleitet:

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

Entspannter Betrieb

Vorgänge, die nicht auf den verweisenden Speicher zugreifen, erfordern unsafe keinen Kontext mehr:

  • Deklarieren eines Zeigerstyps und Verwenden der Adresse einer Variablen mit dem & Operator.
  • Die fixed Anweisung, die eine Variable anheftet.
  • Konvertieren eines stackalloc Ausdrucks in einen Zeiger.
  • Der sizeof Operator, der auf einen nicht verwalteten Typ angewendet wird.

Im folgenden Beispiel werden Zeiger ohne unsafe Kontext erstellt und angeheftet:

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

Diese Entspannungen gelten immer dann, wenn Sie mit der preview Sprachversion kompilieren, unabhängig davon, ob eine Assembly die aktualisierten Speichersicherheitsregeln aktiviert.

Entlastung unsicherer Verpflichtungen des Anrufers

Ein Mitglied, das einen aufruferunsicheren Vorgang aufruft, hat zwei Möglichkeiten: die Verpflichtung zu verbreiten oder ihn zu entlasten.

  • Verteilung: Markieren Sie Ihr eigenes Mitglied unsafe. Die Verpflichtung geht an Ihre Anrufer weiter. Verwenden Sie die Verteilung, wenn Sie die Verpflichtung selbst nicht vollständig überprüfen können.
  • Entlastung: Lassen Sie die Signatur Ihres Mitglieds sicher. Überprüfen Sie die Verpflichtung innerhalb des Mitglieds, in der Regel mit Laufzeitschutz, und führen Sie dann den unsicheren Vorgang in einem inneren unsafe Block aus. Ein Element, das einen inneren unsafe Block enthält, aber seine eigene Signatur unsafe nicht kennzeichnet, ist eine unsichere Grenze: Er wandelt unsicheren Code in eine sichere aufrufbare Oberfläche um.

Das folgende Vorschau-Modell überprüft seine Eingabe mit einem Schutz, pinsiert ein verwaltetes Array und liest den Zeiger durch. Aufrufer benötigen unsafe keinen Kontext, da die Methode die Verpflichtung entlastet:

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

Die NULL-Prüfung und die Arraylänge schließen die Eingaben aus, die eine Leseausführung über den Puffer zulassen würden, sodass die Ableitung innerhalb des unsafe Blocks sound ist. Die Methode hinterlässt keine Restverpflichtung, so dass sie eine sichere aufrufbare Signatur verfügbar macht.

Sicherheitsdokumentation

Ein unsicheres Anrufermitglied sollte dokumentieren, was der Anrufer garantieren muss. Das aktualisierte Modell ermutigt zwei ergänzende Kommentarformatvorlagen:

  • Ein /// <safety> Dokumentationsblock oberhalb der Signatur gibt den formalen Vertrag an: Die Bedingungen, die ein Anrufer erfüllen muss. Eine Analyse kann ein unsicheres Anruferelement kennzeichnen, das fehlt.
  • Ein // SAFETY: Kommentar in einem unsafe Block zeichnet auf, warum der Vorgang an dieser Stelle klingt, für die Entwickler und Auditoren, die den Text lesen.

Das folgende Vorschaumodell zeigt beide Formatvorlagen für eine aufrufer-unsichere ReadByte Methode an:

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

Der /// <safety> Block teilt Ihnen den Vertrag mit. Der Vertrag gehört in die Dokumentation, in der jeder Anrufer und Prüfer sie sieht.

Unsichere Felder

Verwenden Sie den unsafe Modifizierer für ein Feld, wenn der deklarierte Typ keine Verträge ausdrückt, die der eingeschlossene Typ verwaltet und von anderen Code abhängt. Die unsichere Sicherheit besteht in der Lücke zwischen dem, was das Typsystem sieht und was der Typ verspricht. Der Modifizierer erzwingt jeden Schreibvorgang in das Feld in einen unsafe Block, wodurch die Schreibvorgänge an einer Zentralen Stelle bearbeitbar bleiben.

Der klarste Fall ist ein Feld, das einen systemeigenen Zeiger enthält. Der Zeiger deklariert nicht, wie viele Byte er adressieren System.Span<T> kann, sodass der enthaltende Typ diese Informationen selbst verwaltet:

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

Ein readonly unsafe Feld koppelt den Vertrag mit einem integrierten Schutz: unsafe benennt den invarianten Schutz und readonly verhindert einen Schreibvorgang, der nach der Konstruktion abgebrochen werden könnte. Das Markieren einer Eigenschaft oder eines Ereignisses unsafe macht den Sicherungsfeldaufrufer nicht unsicher. In einer Struktur mit [StructLayout(LayoutKind.Explicit)], markieren Sie jedes Feld entweder safe oder unsafe.

Das sichere Schlüsselwort

Das aktualisierte Modell fügt ein safe kontextbezogenes Schlüsselwort hinzu, das eine Deklaration bestätigt, bei der der Compiler die Auswahl explizit festlegen muss.

Ein extern Mitglied ruft systemeigenen Code auf, sodass der Compiler seine Sicherheit nicht klassifizieren kann. Unter dem aktualisierten Modell markieren Sie jede extern Deklaration, einschließlich einer LibraryImport partiellen Methode, entweder safe oder unsafe:

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

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

getpid akzeptiert keine Parameter und gibt einen Grundtyp zurück, sodass der Autor bestätigt, dass der Aufruf sicher ist und Anrufer ihn ohne Zeremonie verwenden. strlen verwendet einen unformatierten Zeiger, den der systemeigene Code ableiten soll, sodass die Deklaration unsafe die Verpflichtung an Aufrufer weiterleitet und weiterleitet. Das Auslassen beider Modifizierer ist ein Fehler, der Sie zwingt, die Sicherheitsentscheidung zu treffen. Ein Feld in einer Struktur mit explizitem Layout verwendet dieselbe Regel.

Opt-In- und Crossassembly-Verhalten

Das aktualisierte Modell verfügt über zwei unabhängige Schalter auf Projektebene:

  • Eine neue Opt-In-Eigenschaft aktiviert die aktualisierten Regeln. Wenn die Eigenschaft deaktiviert ist, gelten die ursprünglichen Regeln. Wenn dies aktiviert ist, unsafe verteilt sich ein Element an Aufrufer, und der Compiler zeichnet die Auswahl in der Assembly mit dem MemorySafetyRulesAttribute Attribut auf.
  • Die vorhandene AllowUnsafeBlocks-Eigenschaft tort jedes Erscheinungsbild des unsafe Schlüsselworts, einschließlich der inneren Blöcke an Anrufstandorten. Standardmäßig kann falseein Projekt am Standard keine unsichere API aufrufen.

Die beiden Eigenschaften werden wie folgt kombiniert:

Opt-In-Eigenschaft AllowUnsafeBlocks Result
Ein Aus (Standard) Die sicherste Konfiguration. Das Projekt verwendet das aktualisierte Modell und lässt keinen unsicheren Code zu.
Ein Ein Das Projekt verwendet das aktualisierte Modell und lässt unsicheren Code zu.
Aus Aus Das ursprüngliche Modell gilt, und das Projekt kann keine Zeigertypen verwenden.
Aus Ein Das ursprüngliche Modell gilt, und das Projekt kann Zeigertypen verwenden.

Gibt an, ob eine Assembly die aktualisierten Regeln gegen eine andere erzwingt, hängt davon ab, welche Seite sich für Folgendes entscheidet:

  • Aufrufer des aktualisierten Modells, Angerufener mit aktualisiertem Modell: Die Marker des unsafe Angerufenen durchlaufen Metadaten. Der Aufrufer umschließt jeden Anruf an einen anruferunsicheren Member in einem unsafe Block.
  • Anrufer mit aktualisiertem Modell, originalmodellbasierter Angerufener: Ein Kompatibilitätsmodus behandelt alle Angerufenen mit einem Zeigertyp in seiner Signatur als anruferunsicher, sodass die Anrufwebsite einen eingeschlossenen unsafe Block benötigt. Dieser Modus verhindert, dass eine zeigerbasierte API ihre unsafe Anforderung im Hintergrund verliert.
  • Originalmodellanrufer, Angerufener mit aktualisiertem Modell: Die ursprünglichen Zeigerregeln gelten weiterhin. Ein aufruferunsicheres Element, das keinen Zeigertyp in seiner Signatur hat, wird aus sicherem Code aufgerufen, da der ursprüngliche Modellaufrufer die neuen Markierungen nicht lesen kann.

C#-Sprachspezifikation

Weitere Informationen finden Sie im Kapitel Unsicherer Code in der C#-Sprachspezifikation.

Informationen zum Entwurf des aktualisierten Speichersicherheitsmodells finden Sie in der Spezifikation der Speichersicherheitsfunktion.

Siehe auch