Freigeben über


23 Unsicherer Code

23.1 Allgemein

Eine Implementierung, die unsicheren Code nicht unterstützt, muss jede Verwendung der in dieser Klausel definierten syntaktischen Regeln diagnostizieren.

Der Rest dieser Klausel, einschließlich aller ihrer Unterklauseln, ist bedingt normativ.

Hinweis: Die Kernsprache von C#, wie sie in den vorangegangenen Abschnitten definiert wurde, unterscheidet sich von C und C++ vor allem dadurch, dass es keine Zeiger als Datentyp gibt. Stattdessen bietet C# Referenzen und die Möglichkeit, Objekte zu erstellen, die von einem Garbage Collector verwaltet werden. Dieses Design, zusammen mit anderen Funktionen, macht C# zu einer viel sichereren Sprache als C oder C++. In der Kernsprache C# ist es einfach nicht möglich, eine nicht initialisierte Variable, einen "baumelnden" Zeiger oder einen Ausdruck zu haben, der ein Array über dessen Grenzen hinaus indiziert. Ganze Kategorien von Fehlern, die routinemäßig C- und C++-Programme plagen, werden so beseitigt.

Obwohl praktisch jedes Zeigerkonstrukt in C oder C++ ein Referenztyp-Gegenstück in C# hat, gibt es dennoch Situationen, in denen der Zugriff auf Zeigertypen eine Notwendigkeit ist. So ist beispielsweise die Anbindung an das zugrunde liegende Betriebssystem, der Zugriff auf ein speicherbelegtes Gerät oder die Implementierung eines zeitkritischen Algorithmus ohne den Zugriff auf Zeiger möglicherweise nicht möglich oder nicht sinnvoll. Um diesen Bedarf zu decken, bietet C# die Möglichkeit, unsicheren Codezu schreiben.

In unsicherem Code ist es möglich, Zeiger zu deklarieren und mit ihnen zu arbeiten, Konvertierungen zwischen Zeigern und ganzzahligen Typen durchzuführen, die Adresse von Variablen zu übernehmen und so weiter. In gewissem Sinne ist das Schreiben von unsicherem Code ähnlich wie das Schreiben von C-Code in einem C#-Programm.

Unsicherer Code ist in der Tat eine "sichere" Funktion, sowohl aus Sicht der Entwickler als auch der Benutzer. Unsicherer Code wird deutlich mit dem Modifikator unsafegekennzeichnet, so dass Entwickler nicht versehentlich unsichere Funktionen verwenden können. Die Ausführungsengine sorgt dafür, dass unsicherer Code nicht in einer nicht vertrauenswürdigen Umgebung ausgeführt werden kann.

Hinweisende

23.2 Unsichere Kontexte

Die unsicheren Funktionen von C# sind nur in unsicheren Kontexten verfügbar. Ein unsicherer Kontext wird durch die Aufnahme eines unsafe Modifikators in die Deklaration eines Typs, eines Mitglieds oder einer lokalen Funktion oder durch die Verwendung einer unsicheren Anweisungeingeführt:

  • Eine Deklaration einer Klasse, eines Strukturs, einer Schnittstelle oder eines Delegaten kann einen unsafe -Modifikator enthalten. In diesem Fall wird der gesamte textuelle Bereich dieser Typdeklaration (einschließlich des Körpers der Klasse, des Strukturs oder der Schnittstelle) als unsicherer Kontext betrachtet.

    Hinweis: Wenn die type_declaration teilweise ist, ist nur dieser Teil ein unsicherer Kontext. Hinweisende

  • Eine Deklaration eines Feldes, einer Methode, einer Eigenschaft, eines Ereignisses, eines Indexers, eines Operators, eines Instanzkonstruktors, eines Finalisierers, eines statischen Konstruktors oder einer lokalen Funktion kann einen unsafe Modifikator enthalten. In diesem Fall wird der gesamte Textbereich dieser Member-Deklaration als unsicherer Kontext betrachtet.
  • Ein unsafe_statement ermöglicht die Verwendung eines unsicheren Kontexts innerhalb eines Blocks. Der gesamte textuelle Umfang des zugehörigen Blocks wird als unsicherer Kontext betrachtet. Eine lokale Funktion, die in einem unsicheren Kontext deklariert wird, ist selbst unsicher.

Die zugehörigen Grammatik-Erweiterungen werden unten und in den folgenden Unterklauseln gezeigt.

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

Beispiel: Im folgenden Code

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

der in der struct-Deklaration angegebene Modifikator unsafe bewirkt, dass der gesamte textuelle Umfang der struct-Deklaration zu einem unsicheren Kontext wird. So ist es möglich, die Felder Left und Right als Zeigertyp zu deklarieren. Das obige Beispiel könnte auch so geschrieben werden

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

Hier führen die unsafe Modifikatoren in den Felddeklarationen dazu, dass diese Deklarationen als unsichere Kontexte betrachtet werden.

Ende des Beispiels

Abgesehen von der Einrichtung eines unsicheren Kontexts, der die Verwendung von Zeigertypen ermöglicht, hat der Modifikator unsafe keine Auswirkungen auf einen Typ oder ein Mitglied.

Beispiel: Im folgenden Code

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

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

der unsichere Modifikator für die Methode F in A bewirkt einfach, dass die textuelle Ausdehnung von F zu einem unsicheren Kontext wird, in dem die unsicheren Funktionen der Sprache verwendet werden können. Bei der Überschreibung von F in Bmuss der Modifikator unsafe nicht neu spezifiziert werden - es sei denn, die Methode F in B benötigt selbst Zugriff auf unsichere Funktionen.

Die Situation ist etwas anders, wenn ein Zeigertyp Teil der Signatur der Methode ist

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

public class B: A
{
    public unsafe override void F(char* p) {...}
}

Da die Signatur von Feinen Zeigertyp enthält, kann sie hier nur in einem unsicheren Kontext geschrieben werden. Der unsichere Kontext kann jedoch eingeführt werden, indem entweder die gesamte Klasse unsicher gemacht wird, wie es in Ader Fall ist, oder indem ein unsafe Modifikator in die Methodendeklaration aufgenommen wird, wie es in Bder Fall ist.

Ende des Beispiels

Wenn der Modifikator unsafe für eine partielle Typdeklaration verwendet wird (§15.2.7), wird nur dieser bestimmte Teil als unsicherer Kontext betrachtet.

23.3 Zeiger-Typen

In einem unsicheren Kontext kann ein Typ (§8.1) sowohl ein pointer_type als auch ein value_type, ein reference_typeoder ein type_parametersein. In einem unsicheren Kontext kann ein pointer_type auch der Elementtyp eines Arrays sein (§17). Ein pointer_type kann auch in einem typeof-Ausdruck (§12.8.18) außerhalb eines unsicheren Kontexts verwendet werden (da eine solche Verwendung nicht unsicher ist).

Ein pointer_type wird als unmanaged_type (§8.8) oder das Schlüsselwort voidgeschrieben, gefolgt von einem * Token:

pointer_type
    : value_type ('*')+
    | 'void' ('*')+
    ;

Der Typ, der vor dem * in einem Zeigertyp angegeben ist, wird als referenter Typ des Zeigertyps bezeichnet. Er stellt den Typ der Variablen dar, auf die ein Wert vom Typ Zeiger zeigt.

Ein pointer_type darf in einem array_type nur in einem unsicheren Kontext verwendet werden (§23.2). Ein non_array_type ist jeder Typ, der nicht selbst ein array_typeist.

Im Gegensatz zu Referenzen (Werte von Referenztypen) werden Zeiger nicht vom Garbage Collector verfolgt. Der Garbage Collector hat keine Kenntnis von Zeigern und den Daten, auf die sie zeigen. Aus diesem Grund darf ein Zeiger nicht auf eine Referenz oder auf eine Struktur zeigen, die Referenzen enthält, und der Referenztyp eines Zeigers muss ein unmanaged_typesein. Zeigertypen selbst sind nicht verwaltete Typen, so dass ein Zeigertyp als Referenztyp für einen anderen Zeigertyp verwendet werden kann.

Die intuitive Regel für die Vermischung von Zeigern und Referenzen lautet, dass Referenzen von Referenzen (Objekten) Zeiger enthalten dürfen, aber Referenzen von Zeigern dürfen keine Referenzen enthalten.

Beispiel: Einige Beispiele für Zeigertypen finden Sie in der folgenden Tabelle:

Beispiel Beschreibung
byte* Zeiger auf byte
char* Zeiger auf char
int** Zeiger auf Zeiger auf int
int*[] Eindimensionales Array von Zeigern auf int
void* Zeiger auf unbekannten Typ

Ende des Beispiels

Bei einer bestimmten Implementierung müssen alle Zeigertypen die gleiche Größe und Darstellung haben.

Hinweis: Im Gegensatz zu C und C++ wird in C#, wenn mehrere Zeiger in derselben Deklaration deklariert werden, * nur zusammen mit dem zugrundeliegenden Typ geschrieben und nicht als vorangestelltes Interpunktionszeichen bei jedem Zeigernamen. Zum Beispiel:

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

Hinweisende

Der Wert eines Zeigers vom Typ T* repräsentiert die Adresse einer Variablen vom Typ T. Der Zeiger-Indirektionsoperator * (§23.6.2) kann für den Zugriff auf diese Variable verwendet werden.

Beispiel: Bei einer Variablen P vom Typ int* bezeichnet der Ausdruck *P die Variable int, die an der in P enthaltenen Adresse gefunden wird. Ende des Beispiels

Wie eine Objektreferenz kann auch ein Zeiger nullsein. Die Anwendung des Indirektionsoperators auf einen null-wertigen Zeiger führt zu einem implementierungsdefinierten Verhalten (§23.6.2). Ein Zeiger mit dem Wert null wird durch all-bits-zero dargestellt.

Der Typ void* stellt einen Zeiger auf einen unbekannten Typ dar. Da der Referenztyp unbekannt ist, kann der Umleitungsoperator nicht auf einen Zeiger vom Typ void*angewandt werden und es kann auch keine Arithmetik mit einem solchen Zeiger durchgeführt werden. Ein Zeiger vom Typ void* kann jedoch in jeden anderen Zeigertyp umgewandelt werden (und umgekehrt) und mit Werten anderer Zeigertypen verglichen werden (§23.6.8).

Zeigertypen sind eine eigene Kategorie von Typen. Anders als Referenztypen und Werttypen erben Zeigertypen nicht von object und es gibt keine Konvertierungen zwischen Zeigertypen und object. Insbesondere werden Boxing und Unboxing (§8.3.13) bei Zeigern nicht unterstützt. Es sind jedoch Konvertierungen zwischen verschiedenen Zeigertypen und zwischen Zeigertypen und ganzzahligen Typen erlaubt. Dies wird in §23.5beschrieben.

Ein pointer_type kann nicht als Typargument (§8.4) verwendet werden, und die Typinferenz (§12.6.3) schlägt bei generischen Methodenaufrufen fehl, die aus einem Typargument einen Zeigertyp abgeleitet hätten.

Ein pointer_type kann nicht als Typ eines Unterausdrucks einer dynamisch gebundenen Operation verwendet werden (§12.3.3).

Ein pointer_type kann nicht als Typ des ersten Parameters in einer Erweiterungsmethode verwendet werden (§15.6.10).

Ein pointer_type kann als Typ eines flüchtigen Feldes verwendet werden (§15.5.4).

Das dynamische Löschen eines Typs E* ist der Zeigertyp mit Referenztyp des dynamischen Löschens von E.

Ein Ausdruck mit einem Zeigertyp kann nicht verwendet werden, um den Wert in einem member_declarator innerhalb eines anonymous_object_creation_expression bereitzustellen (§12.8.17.3).

Der Standardwert (§9.3) für jeden Zeigertyp ist null.

Hinweis: Obwohl Zeiger als By-Reference-Parameter übergeben werden können, kann dies zu undefiniertem Verhalten führen, da der Zeiger möglicherweise auf eine lokale Variable zeigt, die nicht mehr existiert, wenn die aufgerufene Methode zurückkehrt, oder das feste Objekt, auf das er früher zeigte, nicht mehr fest ist. Zum Beispiel:

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

Hinweisende

Eine Methode kann einen Wert eines bestimmten Typs zurückgeben, und dieser Typ kann ein Zeiger sein.

Beispiel: Wenn Sie einen Zeiger auf eine zusammenhängende Sequenz von ints, die Elementanzahl dieser Sequenz und einen anderen int-Wert erhalten, gibt die folgende Methode die Adresse dieses Wertes in dieser Sequenz zurück, wenn eine Übereinstimmung vorliegt; andernfalls gibt sie zurück 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;
}

Ende des Beispiels

In einem unsicheren Kontext stehen mehrere Konstrukte für die Bearbeitung von Zeigern zur Verfügung:

  • Der unäre Operator * kann verwendet werden, um eine Zeigerumleitung durchzuführen (§23.6.2).
  • Der Operator -> kann verwendet werden, um auf ein Mitglied einer Struktur über einen Zeiger zuzugreifen (§23.6.3).
  • Der Operator [] kann verwendet werden, um einen Zeiger zu indizieren (§23.6.4).
  • Der unäre & Operator kann verwendet werden, um die Adresse einer Variablen zu erhalten (§23.6.5).
  • Die Operatoren ++ und -- können zum Inkrementieren und Dekrementieren von Zeigern verwendet werden (§23.6.6).
  • Die binären + und - Operatoren können verwendet werden, um Zeigerarithmetik durchzuführen (§23.6.7).
  • Die Operatoren ==, !=, <, >, <=und >= können zum Vergleich von Zeigern verwendet werden (§23.6.8).
  • Der stackalloc Operator kann verwendet werden, um Speicher aus dem Aufrufstapel zuzuweisen (§23.9).
  • Die Anweisung fixed kann verwendet werden, um eine Variable vorübergehend zu fixieren, damit ihre Adresse ermittelt werden kann (§23.7).

23.4 Feste und bewegliche Variablen

Der address-of-Operator (§23.6.5) und die fixed-Anweisung (§23.7) unterteilen Variablen in zwei Kategorien: Fixierte Variablen und bewegliche Variablen.

Feste Variablen befinden sich an Speicherorten, die von den Operationen des Garbage Collectors nicht betroffen sind. Beispiele für feste Variablen sind lokale Variablen, Wertparameter und Variablen, die durch Dereferenzierung von Zeigern erstellt werden.) Auf der anderen Seite befinden sich bewegliche Variablen an Speicherorten, die vom Garbage Collector verschoben oder entsorgt werden können. (Beispiele für bewegliche Variablen sind Felder in Objekten und Elemente von Arrays).

Der Operator & (§23.6.5) erlaubt es, die Adresse einer festen Variablen ohne Einschränkungen zu erhalten. Da eine bewegliche Variable jedoch der Verschiebung oder Entsorgung durch den Garbage Collector unterliegt, kann die Adresse einer beweglichen Variablen nur mit einer fixed statement (§23.7) ermittelt werden, und diese Adresse bleibt nur für die Dauer dieser fixed -Anweisung gültig.

Genauer gesagt ist eine feste Variable eine der folgenden Variablen:

  • Eine Variable, die aus einem simple_name (§12.8.4) resultiert, der auf eine lokale Variable, einen Wertparameter oder ein Parameterarray verweist, es sei denn, die Variable wird von einer anonymen Funktion (§12.19.6.2) erfasst.
  • Eine Variable, die aus einem member_access (§12.8.7) der Form V.Iresultiert, wobei V eine feste Variable eines struct_typeist.
  • Eine Variable, die resultiert aus einem pointer_indirection_expression (§23.6.2) der Form *P, einem pointer_member_access (§23.6.3) der Form P->I, oder ein pointer_element_access (§23.6.4) der Form P[E].

Alle anderen Variablen werden als bewegliche Variablen eingestuft.

Ein statisches Feld wird als eine bewegliche Variable eingestuft. Außerdem wird ein By-Reference-Parameter als bewegliche Variable eingestuft, auch wenn das Argument für den Parameter eine feste Variable ist. Schließlich wird eine Variable, die durch Dereferenzierung eines Zeigers erzeugt wird, immer als feste Variable eingestuft.

23.5 Zeigerkonvertierungen

23.5.1 Allgemein

In einem unsicheren Kontext wird die Menge der verfügbaren impliziten Konvertierungen (§10.2) um die folgenden impliziten Zeiger-Konvertierungen erweitert:

  • Von einem beliebigen pointer_type zum Typ void*.
  • Vom null -Literal (§6.4.5.7) zu einem beliebigen pointer_type.

Zusätzlich wird in einem unsicheren Kontext die Menge der verfügbaren expliziten Konvertierungen (§10.3) um die folgenden expliziten Zeiger-Konvertierungen erweitert:

  • Von einem pointer_type in einen anderen pointer_type.
  • Von sbyte, byte, short, ushort, int, uint, longoder ulong zu einem beliebigen pointer_type.
  • Von einem beliebigen pointer_type zu sbyte, byte, short, ushort, int, uint, long, oder ulong.

In einem unsicheren Kontext schließlich umfasst der Satz der impliziten Standardkonvertierungen (§10.4.2) die folgenden Zeigerumwandlungen:

  • Von einem beliebigen pointer_type zum Typ void*.
  • Vom null -Literal zu einem beliebigen pointer_type.

Konvertierungen zwischen zwei Zeigertypen ändern niemals den eigentlichen Zeigerwert. Mit anderen Worten, eine Konvertierung von einem Zeigertyp in einen anderen hat keine Auswirkungen auf die zugrunde liegende Adresse, die durch den Zeiger angegeben wird.

Wenn ein Zeigertyp in einen anderen konvertiert wird und der resultierende Zeiger nicht korrekt für den Typ, auf den gezeigt wird, ausgerichtet ist, ist das Verhalten undefiniert, wenn das Ergebnis dereferenziert wird. Im Allgemeinen ist das Konzept "korrekt ausgerichtet" transitiv: Wenn ein Zeiger auf Typ A korrekt ausgerichtet ist für einen Zeiger auf Typ B, der wiederum korrekt ausgerichtet ist für einen Zeiger auf Typ C, dann ist ein Zeiger auf Typ A korrekt ausgerichtet für einen Zeiger auf Typ C.

Beispiel:Betrachten Sie den folgenden Fall, in dem auf eine Variable eines Typs über einen Zeiger auf einen anderen Typ zugegriffen wird:

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
}

Ende des Beispiels

Wenn ein Zeigertyp in einen Zeiger auf byteumgewandelt wird, zeigt das Ergebnis auf den niedrigsten adressierten byte der Variablen. Aufeinanderfolgende Inkremente des Ergebnisses bis zur Größe der Variablen ergeben Zeiger auf die verbleibenden Bytes dieser Variablen.

Beispiel: Die folgende Methode zeigt jedes der acht Bytes in einem double als Hexadezimalwert an:

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

Natürlich hängt die erzeugte Ausgabe von der Endianness ab. Eine Möglichkeit ist " BA FF 51 A2 90 6C 24 45".

Ende des Beispiels

Zuordnungen zwischen Zeigern und Ganzzahlen sind implementierungsabhängig.

Hinweis: Auf 32- und 64-Bit-CPU-Architekturen mit einem linearen Adressraum verhalten sich Konvertierungen von Zeigern in oder aus Integral-Typen jedoch typischerweise genauso wie Konvertierungen von uint- bzw. ulong-Werten in oder aus diesen Integral-Typen. Hinweisende

23.5.2 Zeiger-Arrays

Arrays von Zeigern können mit array_creation_expression (§12.8.17.4) in einem unsicheren Kontext konstruiert werden. Nur einige der Konvertierungen, die für andere Array-Typen gelten, sind für Zeiger-Arrays zulässig:

  • Die implizite Referenzkonvertierung (§10.2.8) von jedem array_type zu System.Array und den von ihm implementierten Schnittstellen gilt auch für Zeiger-Arrays. Jeder Versuch, über System.Array oder die von ihm implementierten Schnittstellen auf die Array-Elemente zuzugreifen, kann jedoch zur Laufzeit zu einer Ausnahme führen, da Zeigertypen nicht in objectkonvertierbar sind.
  • Die impliziten und expliziten Referenzkonvertierungen (§10.2.8, §10.3.5) von einem eindimensionalen Array-Typ S[] zu System.Collections.Generic.IList<T> und seinen generischen Basisschnittstellen gelten nie für Zeiger-Arrays.
  • Die explizite Referenzkonvertierung (§10.3.5) von System.Array und den von ihm implementierten Schnittstellen zu einem beliebigen array_type gilt für Zeiger-Arrays.
  • Die expliziten Referenzkonvertierungen (§10.3.5) von System.Collections.Generic.IList<S> und seinen Basisschnittstellen zu einem eindimensionalen Array-Typ T[] gelten nie für Zeiger-Arrays, da Zeigertypen nicht als Typargumente verwendet werden können und es keine Konvertierungen von Zeigertypen in Nicht-Zeigertypen gibt.

Diese Einschränkungen bedeuten, dass die in foreach beschriebene Erweiterung für die -Anweisung über Arrays nicht auf Pointer-Arrays angewendet werden kann. Stattdessen wird eine foreach -Anweisung der Form

foreach (V v in x) embedded_statement

wobei der Typ von x ein Array-Typ der Form T[,,...,]ist, n die Anzahl der Dimensionen minus 1 ist und T oder V ein Zeigertyp ist, wird mit verschachtelten for-Schleifen wie folgt erweitert:

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

Die Variablen a, i0, i1, ... in sind weder für x noch für embedded_statement oder einen anderen Quellcode des Programms sichtbar oder zugänglich. Die Variable v ist in der eingebetteten Anweisung schreibgeschützt. Wenn es keine explizite Konvertierung (§23.5) von T (dem Elementtyp) nach Vgibt, wird ein Fehler erzeugt und es werden keine weiteren Schritte unternommen. Wenn x den Wert nullhat, wird zur Laufzeit ein System.NullReferenceException ausgelöst.

Hinweis: Obwohl Zeigertypen als Typargumente nicht zulässig sind, können Zeigerarrays als Typargumente verwendet werden. Hinweisende

23.6 Zeiger in Ausdrücken

23.6.1 Allgemein

In einem unsicheren Kontext kann ein Ausdruck ein Ergebnis vom Typ Zeiger liefern, aber außerhalb eines unsicheren Kontexts ist es ein Kompilierfehler, wenn ein Ausdruck vom Typ Zeiger ist. Genauer gesagt, tritt außerhalb eines unsicheren Kontexts ein Kompilierfehler auf, wenn ein simple_name (§12.8.4), member_access (§12.8.7), invocation_expression (§12.8.10), oder element_access (§12.8.12) ein Zeigertyp ist.

Im unsicheren Kontext ermöglichen die primary_expression (§12.8) und unary_expression (§12.9) zusätzliche Konstrukte, die in den folgenden Unterabschnitten beschrieben werden.

Hinweis: Die Vorrangigkeit und Assoziativität der unsicheren Operatoren wird durch die Grammatik impliziert. Hinweisende

23.6.2 Zeigerumleitung

Ein pointer_indirection_expression besteht aus einem Sternchen (*) gefolgt von einem unary_expression.

pointer_indirection_expression
    : '*' unary_expression
    ;

Der unäre Operator * bezeichnet die Zeigerumleitung und wird verwendet, um die Variable zu erhalten, auf die ein Zeiger zeigt. Das Ergebnis der Auswertung von *P, wobei P ein Ausdruck vom Zeigertyp T*ist, ist eine Variable vom Typ T. Es ist ein Kompilierfehler, den unären Operator * auf einen Ausdruck vom Typ void* oder auf einen Ausdruck anzuwenden, der nicht vom Typ Zeiger ist.

Der Effekt der Anwendung des unären * -Operators auf einen null-wertigen Zeiger ist implementierungsabhängig. Insbesondere gibt es keine Garantie, dass diese Operation einen System.NullReferenceExceptionauslöst.

Wenn dem Zeiger ein ungültiger Wert zugewiesen wurde, ist das Verhalten des unären Operators * undefiniert.

Hinweis: Zu den ungültigen Werten für die Dereferenzierung eines Zeigers durch den unären *-Operator gehören eine Adresse, die für den Typ, auf den gezeigt wird, unangemessen ausgerichtet ist (siehe Beispiel in §23.5), und die Adresse einer Variablen nach dem Ende ihrer Lebensdauer.

Für die Zwecke der Analyse der definitiven Zuweisung gilt eine Variable, die durch die Auswertung eines Ausdrucks der Form *P erzeugt wird, als ursprünglich zugewiesen (§9.4.2).

23.6.3 Zugriff auf Zeigermitglieder

Ein pointer_member_access besteht aus einem primary_expression, gefolgt von einem „->“-Token, gefolgt von einem Identifikator und einer optionalen type_argument_list.

pointer_member_access
    : primary_expression '->' identifier type_argument_list?
    ;

In einem Zeiger-Member-Zugriff der Form P->Imuss P ein Ausdruck eines Zeigertyps sein und I muss ein zugreifbares Member des Typs bezeichnen, auf den P zeigt.

Ein Zeigermitgliedzugriff der Form P->I wird genau wie (*P).Iausgewertet. Für eine Beschreibung des Zeiger-Indirektionsoperators (*), siehe §23.6.2. Für eine Beschreibung des Operators member access (.), siehe §12.8.7.

Beispiel: Im folgenden Code

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

der -> Operator wird verwendet, um auf Felder zuzugreifen und eine Methode einer Struktur über einen Zeiger aufzurufen. Da die Operation P->I genau der Operation (*P).Ientspricht, hätte die Methode Main genauso gut geschrieben werden können:

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

Ende des Beispiels

23.6.5. Zeigerelementzugriff

Ein pointer_element_access besteht aus einem primary_expression gefolgt von einem Ausdruck, der in "[" und "]" eingeschlossen ist.

pointer_element_access
    : primary_expression '[' expression ']'
    ;

Wenn eine primary_expression erkannt wird und sowohl die element_access als auch die pointer_element_access (§23.6.4) anwendbar sind, dann wird die letztere gewählt, wenn die eingebettete primary_expression vom Zeigertyp (§23.3) ist.

Bei einem Zeigerelementzugriff der Form P[E]muss P ein Ausdruck eines anderen Zeigertyps als void*sein, und E muss ein Ausdruck sein, der implizit in int, uint, longoder ulongumgewandelt werden kann.

Ein Zeigerelementzugriff der Form P[E] wird genau wie *(P + E)ausgewertet. Für eine Beschreibung des Zeiger-Indirektionsoperators (*), siehe §23.6.2. Für eine Beschreibung des Zeiger-Additionsoperators (+), siehe §23.6.7.

Beispiel: Im folgenden Code

class Test
{
    static void Main()
    {
        unsafe
        {
            char* p = stackalloc char[256];
            for (int i = 0; i < 256; i++)
            {
                p[i] = (char)i;
            }
        }
    }
}

wird ein Zeigerelementzugriff verwendet, um den Zeichenpuffer in einer for Schleife zu initialisieren. Da die Operation P[E] genau der Operation *(P + E)entspricht, hätte das Beispiel genauso gut geschrieben werden können:

class Test
{
    static void Main()
    {
        unsafe
        {
            char* p = stackalloc char[256];
            for (int i = 0; i < 256; i++)
            {
                *(p + i) = (char)i;
            }
        }
    }
}

Ende des Beispiels

Der Operator für den Zugriff auf Zeigerelemente prüft nicht auf Out-of-Bounds-Fehler und das Verhalten beim Zugriff auf ein Out-of-Bounds-Element ist undefiniert.

Hinweis: Dies ist dasselbe wie bei C und C++. Hinweisende

23.6.5 Der address-of-Operator

Ein -Ausdruck besteht aus einem kaufmännischen Und-Zeichen (&), gefolgt von einem unary_expression.

addressof_expression
    : '&' unary_expression
    ;

Bei einem Ausdruck E , der vom Typ T ist und als feste Variable klassifiziert ist (§23.4), berechnet das Konstrukt &E die Adresse der durch Egegebenen Variablen. Der Typ des Ergebnisses ist T* und wird als Wert klassifiziert. Ein Kompilierfehler tritt auf, wenn E nicht als Variable klassifiziert ist, wenn E als lokale Variable klassifiziert ist, die nur gelesen werden kann, oder wenn E eine bewegliche Variable bezeichnet. Im letzten Fall kann eine fixe Anweisung (§23.7) verwendet werden, um die Variable vorübergehend zu "fixieren", bevor Sie ihre Adresse erhalten.

Hinweis: Wie in §12.8.7 angegeben, wird außerhalb eines Instanzkonstruktors oder statischen Konstruktors für eine Struktur oder Klasse, die ein readonly-Feld definiert, dieses Feld als Wert und nicht als Variable betrachtet. Daher kann die Adresse nicht übernommen werden. Ebenso kann die Adresse einer Konstante nicht übernommen werden. Hinweisende

Der & -Operator erfordert nicht, dass sein Argument definitiv zugewiesen ist, aber nach einer & -Operation gilt die Variable, auf die der Operator angewendet wird, in dem Ausführungspfad, in dem die Operation stattfindet, als definitiv zugewiesen. Es liegt in der Verantwortung des Programmierers, dafür zu sorgen, dass die Initialisierung der Variablen in dieser Situation tatsächlich korrekt erfolgt.

Beispiel: Im folgenden Code

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

i gilt nach der Operation &i, die zur Initialisierung von p verwendet wurde, als definitiv zugewiesen. Die Zuweisung an *p bewirkt eine Initialisierung von i, aber die Einbeziehung dieser Initialisierung liegt in der Verantwortung des Programmierers, und es würde kein Kompilierzeitfehler auftreten, wenn die Zuweisung entfernt würde.

Ende des Beispiels

Hinweis: Die Regeln der definitiven Zuweisung für den Operator & existieren so, dass eine redundante Initialisierung von lokalen Variablen vermieden werden kann. Viele externe APIs nehmen zum Beispiel einen Zeiger auf eine Struktur, die von der API ausgefüllt wird. Aufrufe solcher APIs übergeben in der Regel die Adresse einer lokalen Strukturvariablen, und ohne die Regel wäre eine redundante Initialisierung der Strukturvariablen erforderlich. Hinweisende

Hinweis: Wenn eine lokale Variable, ein Wertparameter oder ein Parameter-Array von einer anonymen Funktion erfasst wird (§12.8.24), wird diese lokale Variable, dieser Parameter oder dieses Parameter-Array nicht mehr als feste Variable betrachtet (§23.7), sondern als bewegliche Variable. Daher ist es ein Fehler, wenn ein unsicherer Code die Adresse einer lokalen Variablen, eines Wertparameters oder eines Parameterarrays übernimmt, die von einer anonymen Funktion erfasst wurden. Hinweisende

23.6.6 Zeiger inkrementieren und dekrementieren

In einem unsicheren Kontext können die Operatoren ++ und -- (§12.8.16 und §12.9.6) auf Zeigervariablen aller Typen außer void*angewendet werden. Daher werden für jeden Zeigertyp T* die folgenden Operatoren implizit definiert:

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

Die Operatoren liefern die gleichen Ergebnisse wie x+1 und x-1(§23.6.7). Mit anderen Worten, für eine Zeigervariable vom Typ T*addiert der Operator ++sizeof(T) zu der in der Variablen enthaltenen Adresse, und der Operator -- subtrahiert sizeof(T) von der in der Variablen enthaltenen Adresse.

Wenn eine Zeiger-Inkrement- oder Dekrement-Operation den Bereich des Zeigertyps überläuft, ist das Ergebnis implementierungsabhängig, aber es werden keine Ausnahmen erzeugt.

23.6.7 Zeigerarithmetik

In einem unsicheren Kontext können der + -Operator (§12.10.5) und der - -Operator (§12.10.6) auf Werte aller Zeigertypen außer void*angewendet werden. Daher werden für jeden Zeigertyp T* die folgenden Operatoren implizit definiert:

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

Bei einem Ausdruck P eines Zeigertyps T* und einem Ausdruck N vom Typ int, uint, longoder ulongberechnen die Ausdrücke P + N und N + P den Zeigerwert vom Typ T* , der sich aus der Addition von N * sizeof(T) zu der durch Pgegebenen Adresse ergibt. Ähnlich berechnet der Ausdruck P – N den Zeigerwert des Typs T*, der das Ergebnis der Subtraktion von N * sizeof(T) von der Adresse ist, die von P angegeben wird.

Bei zwei Ausdrücken P und Qeines Zeigertyps T*berechnet der Ausdruck P – Q die Differenz zwischen den durch P und Q gegebenen Adressen und teilt diese Differenz dann durch sizeof(T). Der Typ des Ergebnisses ist immer long. In Wirklichkeit wird P - Q als ((long)(P) - (long)(Q)) / sizeof(T)berechnet.

Beispiel:

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

die die Ausgabe erzeugt:

p - q = -14
q - p = 14

Ende des Beispiels

Wenn eine arithmetische Zeigeroperation den Bereich des Zeigertyps überläuft, wird das Ergebnis auf eine implementierungsdefinierte Weise abgeschnitten, aber es werden keine Ausnahmen erzeugt.

23.6.8 Zeigervergleich

In einem unsicheren Kontext können die Operatoren ==, !=, <, >, <=und >= (§12.12) auf Werte aller Zeigertypen angewendet werden. Die Zeiger-Vergleichsoperatoren sind:

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

Da eine implizite Konvertierung von jedem Zeigertyp in den Typ void* existiert, können Operanden eines beliebigen Zeigertyps mit diesen Operatoren verglichen werden. Die Vergleichsoperatoren vergleichen die von den beiden Operanden angegebenen Adressen, als ob es sich um ganze Zahlen ohne Vorzeichen handeln würde.

23.6.9 Der sizeof-Operator

Für bestimmte vordefinierte Typen (§12.8.19) liefert der Operator sizeof einen konstanten int Wert. Bei allen anderen Typen ist das Ergebnis des sizeof -Operators implementierungsabhängig und wird als Wert und nicht als Konstante eingestuft.

Die Reihenfolge, in der die Mitglieder in eine Struktur gepackt werden, ist nicht spezifiziert.

Zu Ausrichtungszwecken kann es am Anfang einer Struktur, innerhalb einer Struktur und am Ende der Struktur unbenannte Auffüllungen geben. Der Inhalt der Bits, die zum Auffüllen verwendet werden, ist unbestimmt.

Bei Anwendung auf einen Operanden vom Typ „struct“ ist das Ergebnis die Gesamtzahl der Bytes in einer Variablen dieses Typs, einschließlich aller Auffüllungen.

23.7 Die feste Anweisung

In einem unsicheren Kontext erlaubt die Produktion embedded_statement (§13.1) ein zusätzliches Konstrukt, die fixierte Anweisung, die dazu dient, eine bewegliche Variable zu „fixieren“, sodass ihre Adresse für die Dauer der Anweisung konstant bleibt.

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
    ;

Jeder fixed_pointer_declarator deklariert eine lokale Variable des gegebenen pointer_type und initialisiert diese lokale Variable mit der vom entsprechenden fixed_pointer_initializerberechneten Adresse. Eine lokale Variable, die in einer festen Anweisung deklariert ist, ist in jedem fixed_pointer_initializer, der rechts von der Deklaration dieser Variable auftritt, und in der embedded_statement der festen Anweisung zugänglich. Eine lokale Variable, die durch eine feste Anweisung deklariert wurde, gilt als schreibgeschützt. Ein Kompilierzeitfehler tritt auf, wenn die eingebettete Anweisung versucht, diese lokale Variable zu ändern (über Zuweisung oder die Operatoren ++ und -- ) oder sie als Referenz oder Ausgabeparameter zu übergeben.

Es ist ein Fehler, eine erfasste lokale Variable (§12.19.6.2), einen Wertparameter oder ein Parameterarray in einem fixed_pointer_initializer zu verwenden. Ein fixed_pointer_initializer kann einer der folgenden sein:

  • Das Token „&“ gefolgt von einem Variablenverweis (§9.5) auf eine bewegliche Variable (§23.4) eines nicht verwalteten Typs T, sofern der Typ T* implizit in den in der fixed -Anweisung angegebenen Zeigertyp konvertierbar ist. In diesem Fall berechnet der Initialisierer die Adresse der gegebenen Variable und die Variable bleibt garantiert für die Dauer der festen Anweisung an einer festen Adresse.
  • Ein Ausdruck eines array_type mit Elementen eines nicht verwalteten Typs T, sofern der Typ T* implizit in den in der festen Anweisung angegebenen Zeigertyp konvertierbar ist. In diesem Fall berechnet der Initialisierer die Adresse des ersten Elements im Array und das gesamte Array bleibt für die Dauer der Anweisung fixed garantiert an einer festen Adresse. Wenn der Array-Ausdruck null ist oder wenn das Array null Elemente hat, berechnet der Initialisierer eine Adresse gleich Null.
  • Ein Ausdruck vom Typ string, sofern der Typ char* implizit in den in der Anweisung fixed angegebenen Zeigertyp konvertierbar ist. In diesem Fall berechnet der Initialisierer die Adresse des ersten Zeichens in der Zeichenkette, und die gesamte Zeichenkette bleibt garantiert für die Dauer der fixed -Anweisung an einer festen Adresse. Das Verhalten der Anweisung fixed ist implementierungsabhängig, wenn der Zeichenfolgenausdruck nullist.
  • Ein Ausdruck eines anderen Typs als array_type oder string, vorausgesetzt, es existiert eine zugängliche Methode oder zugängliche Erweiterungsmethode, die der Signatur ref [readonly] T GetPinnableReference()entspricht, wobei T ein unmanaged_typeist und T* implizit in den in der fixed -Anweisung angegebenen Zeigertyp konvertierbar ist. In diesem Fall berechnet der Initialisierer die Adresse der zurückgegebenen Variablen, und diese Variable bleibt garantiert für die Dauer der fixed -Anweisung an einer festen Adresse. Eine GetPinnableReference() -Methode kann von der fixed -Anweisung verwendet werden, wenn die Überladungsauflösung (§12.6.4) genau ein Funktionsmitglied erzeugt und dieses Funktionsmitglied die vorhergehenden Bedingungen erfüllt. Die Methode GetPinnableReference sollte einen Verweis auf eine Adresse gleich Null zurückgeben, wie z.B. die von System.Runtime.CompilerServices.Unsafe.NullRef<T>() zurückgegebene, wenn es keine Daten zu pinnen gibt.
  • Ein simple_name oder member_access , der ein Pufferelement mit fester Größe einer beweglichen Variablen referenziert, vorausgesetzt, der Typ des Pufferelements mit fester Größe ist implizit in den in der fixed -Anweisung angegebenen Zeigertyp konvertierbar. In diesem Fall berechnet der Initialisierer einen Zeiger auf das erste Element des Puffers mit fester Größe (§23.8.3), und der Puffer mit fester Größe bleibt garantiert für die Dauer der fixed -Anweisung an einer festen Adresse.

Für jede von einem fixed_pointer_initializer berechnete Adresse stellt die fixed -Anweisung sicher, dass die Variable, auf die die Adresse verweist, für die Dauer der fixed -Anweisung nicht vom Garbage Collector verschoben oder entsorgt wird.

Beispiel: Wenn die von einem fixed_pointer_initializer berechnete Adresse auf ein Feld eines Objekts oder ein Element einer Array-Instanz verweist, garantiert die fixed-Anweisung, dass die enthaltende Objektinstanz während der Lebensdauer der Anweisung nicht verschoben oder entsorgt wird. Ende des Beispiels

Es liegt in der Verantwortung des Programmierers, dafür zu sorgen, dass Zeiger, die durch feste Anweisungen erzeugt werden, nicht über die Ausführung dieser Anweisungen hinaus bestehen bleiben.

Beispiel: Wenn durch fixed-Anweisungen erstellte Zeiger an externe APIs weitergegeben werden, liegt es in der Verantwortung des Programmierers, dafür zu sorgen, dass die APIs keine Erinnerung an diese Zeiger zurückbehalten. Ende des Beispiels

Feste Objekte können eine Fragmentierung des Heaps verursachen (weil sie nicht verschoben werden können). Aus diesem Grund sollten Objekte nur dann fixiert werden, wenn es absolut notwendig ist, und dann auch nur für die kürzest mögliche Zeit.

Beispiel: Das Beispiel

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

demonstriert mehrere Verwendungen der Anweisung fixed . Die erste Anweisung fixiert und erhält die Adresse eines statischen Feldes, die zweite Anweisung fixiert und erhält die Adresse eines Instanzfeldes und die dritte Anweisung fixiert und erhält die Adresse eines Array-Elements. In jedem Fall wäre es ein Fehler gewesen, den regulären & Operator zu verwenden, da die Variablen alle als bewegliche Variablen klassifiziert sind.

Die dritte und vierte fixed -Anweisung im obigen Beispiel führen zu identischen Ergebnissen. Im Allgemeinen ist für eine Array-Instanz adie Angabe von a[0] in einer fixed -Anweisung dasselbe wie die einfache Angabe von a.

Ende des Beispiels

In einem unsicheren Kontext werden Array-Elemente von eindimensionalen Arrays in aufsteigender Indexreihenfolge gespeichert, beginnend mit Index 0 und endend mit Index Length – 1. Bei mehrdimensionalen Arrays werden die Arrayelemente so gespeichert, dass die Indizes der ganz rechten Dimension zuerst erhöht werden, dann die der nächsten linken Dimension und so weiter nach links.

Innerhalb einer fixed -Anweisung, die einen Zeiger p auf eine Array-Instanz aerhält, stellen die Zeigerwerte von p bis p + a.Length - 1 Adressen der Elemente im Array dar. Ebenso stellen die Variablen von p[0] bis p[a.Length - 1] die eigentlichen Array-Elemente dar. Angesichts der Art und Weise, wie Arrays gespeichert werden, kann ein Array beliebiger Dimension so behandelt werden, als wäre es linear.

Beispiel:

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

die die Ausgabe erzeugt:

[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

Ende des Beispiels

Beispiel: Im folgenden Code

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

eine fixed -Anweisung wird verwendet, um ein Array zu fixieren, damit seine Adresse an eine Methode übergeben werden kann, die einen Zeiger annimmt.

Ende des Beispiels

Ein char* -Wert, der durch das Fixieren einer Zeichenfolgeninstanz erzeugt wird, zeigt immer auf eine null-terminierte Zeichenfolge. Innerhalb einer festen Anweisung, die einen Zeiger p auf eine Zeichenfolgeninstanz serhält, stellen die Zeigerwerte von p bis p + s.Length ‑ 1 Adressen der Zeichen in der Zeichenfolge dar, und der Zeigerwert p + s.Length zeigt immer auf ein Null-Zeichen (das Zeichen mit dem Wert '\0').

Beispiel:

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

Ende des Beispiels

Beispiel: Der folgende Code zeigt einen fixed_pointer_initializer mit einem Ausdruck von einem anderen Typ als array_type oder string:

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

Typ C hat eine zugängliche Methode GetPinnableReference mit der richtigen Signatur. In der Anweisung fixed wird der ref int , der von dieser Methode zurückgegeben wird, wenn sie auf c aufgerufen wird, zur Initialisierung des int* -Zeigers pverwendet. Ende des Beispiels

Die Änderung von Objekten des verwalteten Typs durch feste Zeiger kann zu undefiniertem Verhalten führen.

Hinweis: Da Zeichenfolgen zum Beispiel unveränderlich sind, liegt es in der Verantwortung des Programmierers, dafür zu sorgen, dass die Zeichen, auf die ein Zeiger auf einen festen Zeichenfolgen verweist, nicht verändert werden. Hinweisende

Hinweis: Die automatische Null-Terminierung von Zeichenfolgens ist besonders praktisch, wenn Sie externe APIs aufrufen, die Zeichenfolgen im „C-Stil“ erwarten. Beachten Sie jedoch, dass eine Zeichenfolgeninstanz auch Null-Zeichen enthalten darf. Wenn solche Null-Zeichen vorhanden sind, wird die Zeichenfolge abgeschnitten erscheinen, wenn sie als char*mit Null-Ende behandelt wird. Hinweisende

23.8 Puffer mit fester Größe

23.8.1 Allgemein

Puffer mit fester Größe werden verwendet, um Inline-Arrays im "C-Stil" als Mitglieder von Structs zu deklarieren. Sie sind vor allem für Schnittstellen zu nicht verwalteten APIs nützlich.

23.8.2 Puffer-Deklarationen mit fester Größe

Ein Puffer mit fester Größe ist ein Element, das den Speicher für einen Puffer mit fester Länge für Variablen eines bestimmten Typs darstellt. Eine Pufferdeklaration mit fester Größe führt einen oder mehrere Puffer mit fester Größe eines bestimmten Elementtyps ein.

Hinweis: Wie bei einem Array kann man sich einen Puffer mit fester Größe so vorstellen, dass er Elemente enthält. Daher wird der Begriff Elementtyp , wie er für ein Array definiert ist, auch für einen Puffer mit fester Größe verwendet. Hinweisende

Puffer mit fester Größe sind nur in struct-Deklarationen erlaubt und dürfen nur in unsicheren Kontexten auftreten (§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 ']'
    ;

Eine Pufferdeklaration mit fester Größe kann eine Reihe von Attributen (§22), einen new Modifikator (§15.3.5), Modifikatoren für die Zugänglichkeit, die einer der deklarierten Zugänglichkeiten entsprechen, die für Strukturmitglieder erlaubt sind (§16.4.3) und einen unsafe Modifikator (§23.2). Die Attribute und Modifikatoren gelten für alle Mitglieder, die durch die Pufferdeklaration mit fester Größe deklariert wurden. Es ist ein Fehler, wenn derselbe Modifikator mehrmals in einer Pufferdeklaration mit fester Größe erscheint.

Eine Pufferdeklaration mit fester Größe darf den Modifikator static nicht enthalten.

Der Pufferelementtyp einer Pufferdeklaration mit fester Größe gibt den Elementtyp der Puffer an, die durch die Deklaration eingeführt werden. Der Typ des Pufferelements muss einer der vordefinierten Typen sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, oder boolsein.

Auf den Typ des Pufferelements folgt eine Liste von Pufferdeklaratoren mit fester Größe, von denen jeder ein neues Element einführt. Ein Pufferdeklarator mit fester Größe besteht aus einem Bezeichner, der das Mitglied benennt, gefolgt von einem konstanten Ausdruck, der in [ - und ] -Tokens eingeschlossen ist. Der Constant-Ausdruck gibt die Anzahl der Elemente in dem Element an, das durch diesen Pufferdeklarator mit fester Größe eingeführt wird. Der Typ des konstanten Ausdrucks muss implizit in den Typ intkonvertierbar sein, und der Wert muss eine positive ganze Zahl ungleich Null sein.

Die Elemente eines Puffers mit fester Größe werden nacheinander im Speicher angeordnet.

Eine Pufferdeklaration mit fester Größe, die mehrere Puffer mit fester Größe deklariert, ist gleichbedeutend mit mehreren Deklarationen einer einzigen Pufferdeklaration mit fester Größe und denselben Attributen und Elementtypen.

Beispiel:

unsafe struct A
{
    public fixed int x[5], y[10], z[100];
}

entspricht

unsafe struct A
{
    public fixed int x[5];
    public fixed int y[10];
    public fixed int z[100];
}

Ende des Beispiels

23.8.3 Puffer mit fester Größe in Ausdrücken

Die Member-Suche (§12.5) eines Puffer-Members mit fester Größe verläuft genau wie die Member-Suche eines Feldes.

Ein Puffer mit fester Größe kann in einem Ausdruck mit einem simple_name (§12.8.4), ein member_access (§12.8.7) oder ein element_access (§12.8.12).

Wenn ein Pufferelement mit fester Größe als einfacher Name referenziert wird, hat dies den gleichen Effekt wie ein Elementzugriff der Form this.I, wobei I das Pufferelement mit fester Größe ist.

Bei einem Memberzugriff der Form E.I , bei dem E. das implizit this. sein kann, wird E wie folgt ausgewertet und klassifiziert, wenn I von einem struct-Typ ist und ein Member Lookup von E.I in diesem struct-Typ einen Member mit fester Größe identifiziert:

  • Wenn der Ausdruck E.I nicht in einem unsicheren Kontext vorkommt, tritt ein Kompilierfehler auf.
  • Wenn E als Wert klassifiziert wird, tritt ein Kompilierfehler auf.
  • Andernfalls, wenn E eine bewegliche Variable ist (§23.4), dann:
    • Wenn der Ausdruck E.I ein fixed_pointer_initializer (§23.7) ist, dann ist das Ergebnis des Ausdrucks ein Zeiger auf das erste Element des Puffermitglieds mit fester Größe I in E.
    • Andernfalls ist der Ausdruck E.I ein primary_expression (§12.8.12.1) innerhalb eines element_access (§12.8.12) des Formulars E.I[J], dann ist das Ergebnis E.I ein Zeiger, Pauf das erste Element des Pufferelements I fester Größe in Eund die eingeschlossene element_access wird dann als pointer_element_access (§23.6.4) P[J]ausgewertet.
    • Andernfalls tritt ein Kompilierfehler auf.
  • Andernfalls verweist E auf eine feste Variable und das Ergebnis des Ausdrucks ist ein Zeiger auf das erste Element des Pufferelements I mit fester Größe in E. Das Ergebnis ist vom Typ S*, wobei S der Elementtyp von Iist, und wird als Wert klassifiziert.

Auf die nachfolgenden Elemente des Puffers mit fester Größe kann mit Zeigeroperationen vom ersten Element aus zugegriffen werden. Im Gegensatz zum Zugriff auf Arrays ist der Zugriff auf die Elemente eines Puffers mit fester Größe eine unsichere Operation und wird nicht auf einen Bereich geprüft.

Beispiel: Im Folgenden wird eine Struktur mit einem Pufferelement fester Größe deklariert und verwendet.

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

Ende des Beispiels

23.8.4 Prüfung der definitiven Zuordnung

Puffer mit fester Größe unterliegen nicht der definitiven Zuweisungsprüfung (§9.4) und Mitglieder von Puffern mit fester Größe werden für die Zwecke der definitiven Zuweisungsprüfung von Variablen vom Typ struct ignoriert.

Wenn die äußerste enthaltende Strukturvariable eines Pufferelements mit fester Größe eine statische Variable, eine Instanzvariable einer Klasseninstanz oder ein Arrayelement ist, werden die Elemente des Puffers mit fester Größe automatisch auf ihre Standardwerte initialisiert (§9.3). In allen anderen Fällen ist der anfängliche Inhalt eines Puffers mit fester Größe undefiniert.

23.9 Stapelzuweisung

Siehe §12.8.22 für allgemeine Informationen über den Operator stackalloc. Hier wird die Fähigkeit dieses Operators, einen Zeiger zu erzeugen, diskutiert.

Wenn ein stackalloc_expression als initialisierender Ausdruck einer local_variable_declaration (§13.6.2) auftritt, wobei der local_variable_type entweder ein Zeigertyp ist (§23.3) oder abgeleitet (var) ist, ist das Ergebnis des stackalloc_expression ein Zeiger vom Typ T*, wobei T der unmanaged_type des stackalloc_expression ist. In diesem Fall ist das Ergebnis ein Zeiger auf den Anfang des zugewiesenen Blocks.

Beispiel:

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

Ende des Beispiels

Anders als der Zugriff auf Arrays oder stackalloc'ed-Blöcke vom Typ Span<T> ist der Zugriff auf die Elemente eines stackalloc'ed-Blocks vom Typ Zeiger eine unsichere Operation und wird nicht auf den Bereich geprüft.

Beispiel: Im folgenden Code

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

ein stackalloc -Ausdruck wird in der IntToString -Methode verwendet, um einen Puffer von 16 Zeichen auf dem Stack zuzuweisen. Der Puffer wird automatisch verworfen, wenn die Methode zurückkehrt.

Beachten Sie jedoch, dass IntToString im sicheren Modus, d.h. ohne Verwendung von Zeigern, wie folgt umgeschrieben werden kann:

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

Ende des Beispiels

Ende des bedingt normativen Textes.