Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
24.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.
Im unsicheren Code ist es möglich, Zeiger zu deklarieren und zu betreiben, Konvertierungen zwischen Datenzeigern und integralen Typen durchzuführen, die Adresse von Variablen und Methoden zu übernehmen usw. 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 muss eindeutig mit dem Modifizierer
unsafegekennzeichnet sein, sodass Entwickler möglicherweise nicht versehentlich unsichere Features verwenden können, und das Ausführungsmodul funktioniert, um sicherzustellen, dass unsicherer Code nicht in einer nicht vertrauenswürdigen Umgebung ausgeführt werden kann.Hinweisende
24.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
unsafeModifikator 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
unsafebewirkt, dass der gesamte textuelle Umfang der struct-Deklaration zu einem unsicheren Kontext wird. So ist es möglich, die FelderLeftundRightals Zeigertyp zu deklarieren. Das obige Beispiel könnte auch so geschrieben werdenpublic struct Node { public int Value; public unsafe Node* Left; public unsafe Node* Right; }Hier führen die
unsafeModifikatoren 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
FinAbewirkt einfach, dass die textuelle Ausdehnung vonFzu einem unsicheren Kontext wird, in dem die unsicheren Funktionen der Sprache verwendet werden können. Bei der Überschreibung vonFinBmuss der Modifikatorunsafenicht neu spezifiziert werden - es sei denn, die MethodeFinBbenö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 inAder Fall ist, oder indem einunsafeModifikator in die Methodendeklaration aufgenommen wird, wie es inBder 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.
24.3 Zeigertypen
24.3.1 Allgemein
Ein Zeiger ist eine Variable, die die Adresse einer Variablen oder statischen Methode enthalten kann, die als Ziel dieses Zeigers bezeichnet wird. Ein Zeiger mit Wert null ist ein Nullzeiger und zeigt derzeit nicht auf eine Variable oder statische Methode. Der Versuch, auf das Ziel eines Zeigers zuzugreifen, wird als Dereferencing (§24.6.2 und §24.6.4) bezeichnet.
In einem unsicheren Kontext kann ein Typ (§8.1) ein pointer_type sein. Ein pointer_type kann auch der Elementtyp eines Arrays (§17) sein. 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).
pointer_type
: dataptr_type
| funcptr_type
| voidptr_type
;
Der Typ des Ziels eines Zeigertyps wird als Referenttyp des Zeigertyps bezeichnet. Er stellt den Typ der Variablen dar, auf die ein Wert vom Typ Zeiger zeigt.
Eine pointer_type darf nur in einem array_type in einem unsicheren Kontext (§24.2) verwendet werden. Ein non_array_type ist jeder Typ, der nicht selbst ein array_typeist.
Im Gegensatz zu Verweisen (Werte von Bezugstypen) werden Zeiger nicht vom Garbage Collector nachverfolgt– der Garbage Collector verfügt nicht über Zeiger und die Daten oder statischen Methoden, auf die sie verweisen. 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 zum Mischen von Zeigern und Bezügen besteht darin, dass Referenten von Verweisen (Objekten) Zeiger enthalten dürfen, aber Referenten von Zeigern dürfen keine Verweise enthalten.
Bei einer bestimmten Implementierung müssen alle Zeigertypen die gleiche Größe und Darstellung haben. Ein Nullzeigerwert muss durch alle Bits-Null dargestellt werden.
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 §24.5 beschrieben.
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.4).
Der Standardwert (§9.3) für jeden Zeigertyp ist null.
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 anderenint-Wert erhalten, gibt die folgende Methode die Adresse dieses Wertes in dieser Sequenz zurück, wenn eine Übereinstimmung vorliegt; andernfalls gibt sie zurücknull: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
24.3.2 Datenzeiger
Ein Datenzeiger ist ein Zeiger, der die Adresse einer Variablen mit value_type (§8.3.1), funcptr_type (§24.3.3) oder voidptr_type (§24.3.4) enthält.
dataptr_type
: value_type ('*')+
| funcptr_type ('*')+
| voidptr_type ('*')+
;
Ein dataptr_type wird als value_type geschrieben, bei dem es sich um eine unmanaged_type (§8.8), funcptr_type oder voidptr_type handelt, gefolgt von einem oder * mehreren Token.
Beispiel: Einige Beispiele für Datenzeigertypen werden in der folgenden Tabelle angegeben:
Beispiel Beschreibung byte*Zeiger auf byteint*[]Eindimensionales Array von Zeigern auf intchar**Zeiger auf Zeiger auf chardelegate*<void>*Zeiger auf einen Zeiger auf eine statische Methode ohne Parameter und rückgabetyp voidvoid**Zeiger auf zeiger auf unbekannten Typ Ende des Beispiels
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 ungleich NULL eines Datenzeigers mit Typ T* stellt die Adresse einer Variablen vom Typ Tdar. Der Zeigerdereferenzierungsoperator * (§24.6.2) kann für den Zugriff auf diese Variable verwendet werden.
Beispiel: Bei einer Variablen
Pvom Typint*bezeichnet der Ausdruck*Pdie Variableint, die an der inPenthaltenen Adresse gefunden wird. Ende des Beispiels
Hinweis: Obwohl Zeiger als Nachverweisparameter übergeben werden können, kann dies mit Datenzeigern zu einem nicht definierten Verhalten führen, da der Zeiger möglicherweise gut auf eine lokale Variable verweist, die nicht mehr vorhanden ist, wenn die aufgerufene Methode zurückgegeben wird, oder das feste Objekt, auf das sie verweist, nicht mehr festgelegt ist. Zum Beispiel:
using System; class Test { static int value = 20; unsafe static void F(out int* pi1, ref int* pi2) { int i = 10; pi1 = &i; fixed (int* pj = &value) { // ... pi2 = pj; } } static void Main() { int i = 10; unsafe { int* px1; int* px2 = &i; F(out px1, ref px2); // Undefined behavior // Console.WriteLine($"*px1 = {*px1}, *px2 = {*px2}"); } } }Hinweisende
In einem unsicheren Kontext stehen mehrere Konstrukte für das Arbeiten mit Datenzeigern zur Verfügung:
- Der unäre
*Operator kann zum Ausführen einer Zeigerdereferenzierung (§24.6.2) verwendet werden. - Der
->Operator kann für den Zugriff auf ein Mitglied einer Struktur über einen Zeiger (§24.6.3) verwendet werden. - Der
[]Operator kann zum Indizieren eines Zeigers (§24.6.4) verwendet werden. - Der unäre
&Operator kann verwendet werden, um die Adresse einer Variablen zu erhalten (§24.6.5). - Die
++Operatoren--können verwendet werden, um Zeiger zu erhöhen und zu erhöhen (§24.6.6). - Die Binären
+und-Operatoren können zum Ausführen von Zeigerarithmetik (§24.6.7) verwendet werden. - Die
==,!=,<,>,<=und>=Operatoren können zum Vergleichen von Zeigern (§24.6.8) verwendet werden. - Der
stackallocOperator kann verwendet werden, um Speicher aus dem Aufrufstapel (§24.9) zuzuweisen. - Die
fixedAnweisung kann verwendet werden, um eine Variable vorübergehend zu korrigieren, damit ihre Adresse abgerufen werden kann (§24.7).
24.3.3 Funktionszeiger
Ein Funktionszeiger ist ein Zeiger, der die Adresse einer statischen Methode enthält.
funcptr_type
: 'delegate' '*' calling_convention_specifier?
'<' funcptr_parameter_list funcptr_return_type '>'
;
calling_convention_specifier
: 'managed'
| 'unmanaged' ('[' unmanaged_calling_convention ']')?
;
unmanaged_calling_convention
: 'Cdecl'
| 'Stdcall'
| 'Thiscall'
| 'Fastcall'
| identifier (',' identifier)*
;
funcptr_parameter_list
: (funcptr_parameter ',')*
;
funcptr_parameter
: parameter_mode_modifier? type
;
funcptr_return_type
: ref_kind? return_type
;
Ebenso wie eine Methode über eine Signatur (§7.6) verfügt ein Funktionszeigertyp über eine Signatur für den Methodentyp, auf den er verweisen kann. Diese Signatur enthält die Aufrufkonvention.
Der Wert ungleich NULL eines Funktionszeigers mit Typ T stellt die Adresse einer Methode dar, die mit dem Typ Tkompatibel ist.
Wenn keine calling_convention_specifier bereitgestellt wird, lautet managedder Standardwert , was dazu führt, dass der Standardmechanismus der Ausführungsumgebung verwendet wird. Bestimmte nicht verwaltete Konventionen können mithilfe von unmanaged_calling_convention angegeben werden, deren Token implementierungsdefinierten Namen mit implementierungsdefinierter Semantik zugeordnet sind. Der Satz gültiger Kombinationen dieser Token ist implementierungsdefiniert.
Hinweis: Die calling_convention_specifier ermöglicht die Auswahl eines potenziell effizienteren Aufrufmechanismus oder für Methoden, die in anderen Sprachen als C# geschrieben wurden. Endnote.
Beispiel: Einige Beispiele für Funktionszeigertypen werden in der folgenden Tabelle angegeben:
Beispiel Beschreibung delegate*<void>Zeiger auf eine verwaltete Methode ohne Parameter und rückgabetyp voiddelegate*<void>[]Array von Zeigern auf eine verwaltete Methode ohne Parameter und einen voidRückgabetypdelegate*<string, string, bool>Zeiger auf eine verwaltete Methode mit zwei stringParametern und einemboolRückgabetypdelegate*<ref readonly int>Zeiger auf eine verwaltete Methode ohne Parameter und Zurückgeben eines ref readonly intdelegate*<delegate*<int>, void>Zeiger auf eine verwaltete Methode mit einem Parameter, bei dem es sich um einen Zeiger auf eine Methode ohne Parameter und einen intRückgabetyp handelt, und einenvoidRückgabetypdelegate* unmanaged[Stdcall]<void>Zeiger auf eine nicht verwaltete Methode ohne Parameter und einen voidRückgabetyp unter Verwendung derStdcallaufrufenden KonventionBeachte Folgendes:
unsafe class Util { static void Log() { ... } static void Log(string p1) { ... } static void User() { delegate*<void>[] ary1 = new delegate*<void>[] { &Log, null }; foreach (var element in ary1) { if (element != null) { element(); // call the method being pointed to } } } }Da die Funktionszeiger im Array auf Methoden ohne Parameter verweisen,
&Logwird die Adresse derLogMethode ohne Parameter verwendet. Ende des Beispiels
unmanaged_calling_convention unterstützt eine kleine Anzahl vordefinierter Konventionen (Cdecl, Stdcall, Thiscallund Fastcall, die alle kontextbezogene Schlüsselwörter sind), die eigenständig oder als Bezeichner in einer unmanaged_calling_convention-Bezeichnerliste verwendet werden können. Andere implementierungsdefinierte Konventionen sind zulässig, und mehrere Konventionen können mithilfe einer Bezeichnerliste kombiniert werden, die möglicherweise eine oder mehrere dieser vordefinierten Konventionen enthält. Die Suche und Verarbeitung von Bezeichnern in dieser Liste erfolgt auf implementierungsdefinierte Weise.
Beispiel: Aufgrund einer implementierungsdefinierten Anrufkonvention
SuppressGCTransition,unsafe class C { delegate* unmanaged[SuppressGCTransition]<int, int> fpx; delegate* unmanaged[Stdcall, SuppressGCTransition]<int, int> fpy; }in beiden Fällen wird die Grammatikregel für die Bezeichnerliste verwendet. Ende des Beispiels
Benutzerdefinierte Attribute können nicht auf eine funcptr_type oder auf eines der zugehörigen Elemente angewendet werden.
Ein Parameter vom Typ funcptr_type darf nicht als params (§15.6.2.1) gekennzeichnet werden.
In einem unsicheren Kontext stehen die folgenden Konstrukte für das Arbeiten mit Funktionszeigern zur Verfügung:
- Der
&Operator kann verwendet werden, um die Adresse einer statischen Methode zu erhalten (§24.6.5) - Die
==,!=,<,>,<=und=>Operatoren können zum Vergleichen von Zeigern (§24.6.8) verwendet werden. - Der invocation_expression-Operator kann verwendet werden, um die Methode aufzurufen,
()auf die verwiesen wird (§12.8.9.1).
24.3.4 Leere Zeiger
Ein leerer Zeiger ist ein Zeiger, der den Wert eines Datenzeigers oder eines Funktionszeigers enthalten kann.
voidptr_type
: 'void' '*'
;
Ein voidptr_type wird als Schlüsselwort void geschrieben, gefolgt von Thye-Token * .
Beispiel: Einige Beispiele für "void-pointer"-Typen werden in der folgenden Tabelle angegeben:
Beispiel Beschreibung void*Zeiger auf unbekannten Typ void*[,,]Dreidimensionales Array von Zeigern auf unbekannten Typ Ende des Beispiels
Ein voidptr_type 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 einen beliebigen Zeigertyp (und umgekehrt) und mit Werten anderer Zeigertypen (§24.6.8) verglichen werden.
24.4 Feste und verschiebebare Variablen
Die Adresse des Operators (§24.6.5) und die fixed Anweisung (§24.7) unterteilen Variablen in zwei Kategorien: Feste Variablenund verschiebbare Variablens.
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 das Ableiten von Datenzeigern erstellt wurden.) Auf der anderen Seite befinden sich verschiebefähige Variablen an Speicherorten, die der Verlagerung oder Entsorgung durch den Garbage Collector unterliegen. (Beispiele für bewegliche Variablen sind Felder in Objekten und Elemente von Arrays).
Der & Operator (§24.6.5) erlaubt, die Adresse einer festen Variablen ohne Einschränkungen zu erhalten. Da eine verschiebebare Variable jedoch der Verlagerung oder Entsorgung durch den Garbage Collector unterliegt, kann die Adresse einer verschiebebaren Variablen nur mithilfe einer fixed statement (§24.7) abgerufen 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 sich aus einer simple_name (§12.8.4) bezieht, die auf eine lokale Variable, einen Wertparameter oder ein Parameterarray verweist, es sei denn, die Variable wird von einer nicht
staticanonymen Funktion (§12.22.6.2) erfasst. - Eine Variable, die aus einem member_access (§12.8.7) der Form
V.Iresultiert, wobeiVeine feste Variable eines struct_typeist. - Eine Variable, die sich aus einer pointer_indirection_expression (§24.6.2) des Formulars
*P, einer pointer_member_access (§24.6.3) des FormularsP->Ioder einer pointer_element_access (§24.6.4) des FormularsP[E]ergibt.
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 das Ableiten eines Datenzeigers erzeugt wird, immer als feste Variable klassifiziert.
24.5 Zeigerkonvertierungen
24.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*. - Von null_literal (§6.4.5.7) bis zu einer beliebigen pointer_type.
- Von funcptr_type
F0bis funcptr_typeF1, vorausgesetzt, alle folgenden Sind erfüllt:-
F0undF1die gleiche Anzahl von Parametern aufweisen, und jeder ParameterD0ninF0verfügt über die gleichen Zusatzparameter wie der entsprechende ParameterD1ninF1. - Für jeden Wertparameter ist eine Identitätskonvertierung, implizite Verweiskonvertierung oder implizite Zeigerkonvertierung vom Parametertyp in
F0den entsprechenden Parametertyp vorhanden inF1. - Für jeden By-Reference-Parameter ist der Parametertyp
F0identisch mit dem entsprechenden Parametertyp inF1. - Wenn der Rückgabetyp nach Wert ist, ist eine Identität, ein impliziter Verweis oder eine implizite Zeigerkonvertierung vom Rückgabetyp bis
F1zum Rückgabetyp vonF0vorhanden. - Wenn der Rückgabetyp referenziert ist, sind der Rückgabetyp und
refdie Modifizierer identisch mit dem Rückgabetyp undrefden ModifizierernF1vonF0. - Die Aufrufkonvention von
F0entspricht der Aufrufkonvention vonF1.
-
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,uintint,nint, ,nuint, , , oderlongulongzu einem beliebigen pointer_type. - Von jedem pointer_type bis ,
sbytebyte,short,ushort,int, ,uint, ,nint,nuint, , oderulonglong.
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*. - Von null_literal bis zu allen 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 eine dataptr_type konvertiert wird, wird das Verhalten nicht definiert, wenn der resultierende Zeiger für den Zeigertyp nicht ordnungsgemäß ausgerichtet ist, wenn das Ergebnis abgeleitet 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
doubleals 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
24.5.2 Zeigerarrays
Arrays von Zeigern können mithilfe von array_creation_expression (§12.8.17.5) in einem unsicheren Kontext erstellt 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.Arrayund den von ihm implementierten Schnittstellen gilt auch für Zeiger-Arrays. Jeder Versuch, überSystem.Arrayoder die von ihm implementierten Schnittstellen auf die Array-Elemente zuzugreifen, kann jedoch zur Laufzeit zu einer Ausnahme führen, da Zeigertypen nicht inobjectkonvertierbar sind. - Die impliziten und expliziten Referenzkonvertierungen (§10.2.8, §10.3.5) von einem eindimensionalen Array-Typ
S[]zuSystem.Collections.Generic.IList<T>und seinen generischen Basisschnittstellen gelten nie für Zeiger-Arrays. - Die explizite Referenzkonvertierung (§10.3.5) von
System.Arrayund 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-TypT[]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 (§24.5) von T (dem Elementtyp) Vin gibt, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt. 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
24.6 Zeiger in Ausdrücken
24.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
Alle Aspekte der Typausleitung in Bezug auf Funktionszeiger werden in den entsprechenden Unterlisten von §12.6 und §12.8 beschrieben.
24.6.2 Zeiger-Dereferenzierung
Ein pointer_indirection_expression besteht aus einem Sternchen (*) gefolgt von einem unary_expression.
pointer_indirection_expression
: '*' unary_expression
;
Der unäre * Operator bezeichnet die Zeigerdereferenzierung und wird verwendet, um die Variable abzurufen, auf die ein Datenzeiger verweist. Das Ergebnis der Auswertung von *P, wobei P ein Ausdruck vom Zeigertyp T*ist, ist eine Variable vom Typ T. Es handelt sich um einen Kompilierungsfehler, um den unären * Operator auf einen Operanden anzuwenden, der den Typ funcptr_type oder voidptr_type hat.
Hinweis: In C/C++ kann ein Funktionszeiger abgeleitet werden, um ihn wie in
(*fp)()der zugrunde liegenden Funktion aufzurufen. Eine solche explizite Ableitung ist in C# nicht zulässig. Hinweisende
Der Effekt der Anwendung des unären * Operators auf einen Null-Datenzeiger ist implementierungsdefiniert. Insbesondere gibt es keine Garantie, dass diese Operation einen System.NullReferenceExceptionauslöst.
Wenn dem Datenzeiger ein ungültiger Wert zugewiesen wurde, ist das Verhalten des unären * Operators nicht definiert.
Hinweis: Unter den ungültigen Werten zum Ableiten eines Datenzeigers durch den unären
*Operator ist eine Adresse, die für den Typ, auf den verwiesen wurde, unangemessen ausgerichtet ist (siehe Beispiel in §24.5) und die Adresse einer Variablen nach dem Ende der 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).
24.6.3 Zugriff auf Zeigermitglied
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 Zeigermitgliedszugriff auf das Formular P->IP muss ein Ausdruck eines Datenzeigertyps sein und I ein barrierefreies Element des Typs bezeichnet werden, auf den P die Punkte zugegriffen werden kann. Es handelt sich um einen Kompilierungsfehler, bei P dem typ funcptr_type oder voidptr_type vorhanden sind.
Ein Zeigermitgliedzugriff der Form P->I wird genau wie (*P).Iausgewertet. Eine Beschreibung des Zeiger-Dereferenzierungsoperators (*) finden Sie unter §24.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 OperationP->Igenau der Operation(*P).Ientspricht, hätte die MethodeMaingenauso 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
24.6.4 Zugriff auf Zeigerelemente
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, wenn sowohl die element_access als auch die pointer_element_access (§24.6.4) Alternativen anwendbar sind, wird letzteres gewählt, wenn die eingebettete primary_expression vom Zeigertyp (§24.3) ist.
Bei einem Zeigerelementzugriff auf das Formular P[E]P muss es sich um einen Ausdruck eines anderen Zeigertyps als void*, und E es muss sich um einen Ausdruck handeln, der implizit in int, , uint, nint, nuint, , oder long.ulong
Ein Zeigerelementzugriff der Form P[E] wird genau wie *(P + E)ausgewertet. Eine Beschreibung des Zeiger-Dereferenzierungsoperators (*) finden Sie unter §24.6.2. Eine Beschreibung des Zeigerzugabeoperators (+) finden Sie unter §24.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
forSchleife zu initialisieren. Da die OperationP[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
24.6.5 Die Adresse des Betreibers
Ein -Ausdruck besteht aus einem kaufmännischen Und-Zeichen (&), gefolgt von einem unary_expression.
addressof_expression
: '&' unary_expression
;
unary_expression muss entweder eine Variable oder eine Methodengruppe festlegen. Der Variable Case wird unmittelbar unten beschrieben.
Bei einem Ausdruck E , der einen Typ T aufweist und als feste Variable (§24.4) klassifiziert wird, berechnet das Konstrukt &E die Adresse der variablen, die angegeben Ewird. 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 feste Anweisung (§24.7) verwendet werden, um die Variable vorübergehend zu "fixieren", bevor sie ihre Adresse abrufen.
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 der Operand definitiv zugewiesen wird, aber nach einem & Vorgang wird die Variable, auf die der Operator angewendet wird, in dem Ausführungspfad, in dem der Vorgang auftritt, 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); } }
igilt nach der Operation&i, die zur Initialisierung vonpverwendet wurde, als definitiv zugewiesen. Die Zuweisung an*pbewirkt eine Initialisierung voni, 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 Parameterarray von einer anonymen Funktion (§12.8.24) erfasst wird, wird diese lokale Variable, ein Parameter oder ein Parameterarray nicht mehr als feste Variable (§24.7) betrachtet, sondern stattdessen als verschiebebare Variable betrachtet. 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
Der Fall unary_expression Entwerfen einer Methodengruppe wird unmittelbar unten beschrieben.
In einem unsicheren Kontext ist eine Methode M mit einem funcptr_typeF kompatibel, wenn alle folgenden Bedingungen zutreffen:
-
MundFhaben die gleiche Anzahl von Parametern, und jeder Parameter inMhat die gleichenref,outoderinModifizierer wie der entsprechende Parameter inF. - Für jeden Wertparameter ist eine Identitätskonvertierung, implizite Verweiskonvertierung oder implizite Zeigerkonvertierung vom Parametertyp in
Mden entsprechenden Parametertyp vorhanden inF. - Für jeden By-Reference-Parameter ist der Parametertyp
Midentisch mit dem entsprechenden Parametertyp inF. - Wenn der Rückgabetyp nach Wert ist, ist eine Identität, ein impliziter Verweis oder eine implizite Zeigerkonvertierung vom Rückgabetyp bis
Fzum Rückgabetyp vonMvorhanden. - Wenn der Rückgabetyp referenziert ist, sind der Rückgabetyp und
refdie Modifizierer identisch mit dem Rückgabetyp undrefden ModifizierernFvonM. - Die Aufrufkonvention von
Mentspricht der Aufrufkonvention vonF. -
Mist eine statische Methode.
Eine implizite Konvertierung besteht aus einer unary_expression , deren Ziel eine Methodengruppe Eist, in einen kompatiblen Funktionszeigertyp F , wenn E mindestens eine Methode enthält, die in ihrer normalen Form auf eine Argumentliste angewendet wird, die mithilfe der Parametertypen und Modifizierer von , wie in den folgenden Beispielen Fbeschrieben, erstellt wird:
- Eine einzelne Methode
Mwird ausgewählt, die einem Methodenaufruf des FormularsE(A)mit den folgenden Änderungen entspricht:- Die Argumentliste
Aist eine Liste von Ausdrücken, die jeweils als Variable klassifiziert sind, und mit dem Typ und Modifizierer der entsprechenden funcptr_parameter_list vonF. - Die Kandidatenmethoden sind nur die Methoden, die in ihrer normalen Form anwendbar sind, nicht die, die in ihrer erweiterten Form anwendbar sind.
- Die Kandidatenmethoden sind nur die Methoden, die statisch sind.
- Die Argumentliste
- Wenn der Algorithmus der Überladungsauflösung einen Fehler erzeugt, tritt ein Kompilierungszeitfehler auf. Andernfalls erzeugt der Algorithmus eine einzige beste Methode
Mmit derselben Anzahl von Parametern wieF, und die Konvertierung wird als vorhanden betrachtet. - Die ausgewählte Methode
Mmuss (wie oben definiert) mit dem FunktionszeigertypFkompatibel sein. Andernfalls tritt ein Kompilierungszeitfehler auf. - Das Ergebnis der Konvertierung ist ein Funktionszeiger vom Typ
F.
24.6.6 Zeiger inkrementierung und Dekrementierung
In einem unsicheren Kontext können die ++ Und -- Operatoren (§12.8.16 und §12.9.7) auf Zeigervariablen aller Typen angewendet werden. Es ist ein Kompilierungszeitfehler, damit diese Operatoren auf Variablen vom Typ funcptr_type oder voidptr_type angewendet werden. Daher werden für jeden Datenzeigertyp T*implizit die folgenden Operatoren definiert:
T* operator ++(T* x);
T* operator --(T* x);
Die Operatoren erzeugen die gleichen Ergebnisse wie x+1 bzw x-1. (§24.6.7). Anders ausgedrückt: Bei einer Datenzeigervariable vom Typ T*fügt sizeof(T) der ++ Operator der in der Variablen enthaltenen Adresse hinzu, und der -- Operator subtrahiert sizeof(T) von der in der Variablen enthaltenen Adresse.
Wenn ein Zeigervorgang inkrementiert oder dekrementiert wird, wird die Domäne des Zeigertyps überlaufen, wird das Ergebnis implementierungsdefiniert, und es ist keine Ausnahme erforderlich.
24.6.7 Zeigerarithmetik
In einem unsicheren Kontext kann der + Operator (§12.13.5) und - der Operator (§12.13.6) auf Werte aller Datenzeigertypen angewendet werden. Es handelt sich um einen Kompilierungszeitfehler, der für diese Operatoren auf einen Wert vom Typ funcptr_type oder voidptr_type angewendet werden soll. 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);
Es gibt keine vordefinierten Operatoren für Zeigerzugabe oder Subtraktion mit nativen ganzzahligen Offsets (§8.3.6).
nint Stattdessen werden werte nuint mit zeigerarithmetischen Zeigerarithmetik mithilfe der vordefinierten Operatoren für diese Typen an- bzwulong. heraufgestuftlong.
Ein Ausdruck P eines Datenzeigertyps T* und eines Ausdrucks N vom Typ int, uint, , longoder ulong, die Ausdrücke und N + P berechnen den Zeigerwert des Typs T*P + N, der sich aus dem Hinzufügen N * sizeof(T) zur von .P Ä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 und , eines Datenzeigertyps PT*, berechnet der Ausdruck P – Q den Unterschied zwischen den adressen, die angegeben werdenP, und Q dividiert dann diesen Unterschied durch sizeof(T).Q 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 = 14Ende des Beispiels
Wenn ein zeigerarithmetischer Vorgang die Domäne des Zeigertyps überläuft, wird das Ergebnis in einer implementierungsdefinierten Weise abgeschnitten, und es ist keine Ausnahme erforderlich.
24.6.8 Zeigervergleich
In einem unsicheren Kontext können die ==Operatoren , , !=, <>, <=und >= Operatoren (§12.15) sicher auf Werte aller dataptr_types und auf Werte aller voidptr_types angewendet werden, die Kopien von dataptr_type Werten sind. 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. Das Verhalten beim Vergleichen von Werten von funcptr_types oder void* Kopien davon ist jedoch nicht definiert.
Hinweis: Auf einigen Plattformen ist es möglich, dass sich die Ergebnisse unterscheiden, wenn die Adresse einer bestimmten Methode mehrmals verwendet wird und vergleicht sie unzuverlässig. Hinweisende
24.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.
24.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.
pointer_type darf nicht funcptr_type werden. 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.
Fehler bei der Verwendung einer erfassten lokalen Variablen (§12.22.6.2), Wertparameter oder Parameterarray in einem fixed_pointer_initializer. Ein fixed_pointer_initializer kann einer der folgenden sein:
- Das Token "
&" gefolgt von einem variable_reference (§9.5) zu einer verschiebebaren Variablen (§24.4) eines nicht verwalteten TypsT, vorausgesetzt, der TypT*ist implizit in den in derfixedAnweisung angegebenen Zeigertyp konvertierbar. 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 TypT*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 Anweisungfixedgarantiert an einer festen Adresse. Wenn der Array-Ausdrucknullist oder wenn das Array null Elemente hat, berechnet der Initialisierer eine Adresse gleich Null. - Ein Ausdruck vom Typ
string, sofern der Typchar*implizit in den in der Anweisungfixedangegebenen 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 derfixed-Anweisung an einer festen Adresse. Das Verhalten der Anweisungfixedist implementierungsabhängig, wenn der Zeichenfolgenausdrucknullist. - Ein Ausdruck eines anderen Typs als array_type oder
string, vorausgesetzt, es existiert eine zugängliche Methode oder zugängliche Erweiterungsmethode, die der Signaturref [readonly] T GetPinnableReference()entspricht, wobeiTein unmanaged_typeist undT*implizit in den in derfixed-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 derfixed-Anweisung an einer festen Adresse. EineGetPinnableReference()-Methode kann von derfixed-Anweisung verwendet werden, wenn die Überladungsauflösung (§12.6.4) genau ein Funktionsmitglied erzeugt und dieses Funktionsmitglied die vorhergehenden Bedingungen erfüllt. Die MethodeGetPinnableReferencesollte einen Verweis auf eine Adresse gleich Null zurückgeben, wie z.B. die vonSystem.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 (§24.8.3), und der Puffer mit fester Größe bleibt garantiert bei einer festen Adresse für die Dauer derfixedAnweisung.
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 (da 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-Instanzadie Angabe vona[0]in einerfixed-Anweisung dasselbe wie die einfache Angabe vona.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] = 23Ende 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
Chat eine zugängliche MethodeGetPinnableReferencemit der richtigen Signatur. In der Anweisungfixedwird derref int, der von dieser Methode zurückgegeben wird, wenn sie aufcaufgerufen wird, zur Initialisierung desint*-Zeigerspverwendet. 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
24.8 Puffer mit fester Größe
24.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.
24.8.2 Pufferdeklarationen 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 Strukturdeklarationen zulässig und dürfen nur in unsicheren Kontexten (§24.2) auftreten.
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 einen Satz von Attributen (§23), einen new Modifizierer (§15.3.5), Barrierefreiheitsmodifizierer enthalten, die den deklarierten Zugriffsberechtigungen entsprechen, die für Strukturmitglieder (§16.4.3) und einen unsafe Modifizierer (§24.2) zulässig sind. 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 Pufferelementtyp muss einen der vordefinierten Typen sbyte, , byte, , short, ushortint, uint, ulongnuintnintcharfloatlongdoubleoder .bool
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
24.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.Inicht in einem unsicheren Kontext vorkommt, tritt ein Kompilierfehler auf. - Wenn
Eals Wert klassifiziert wird, tritt ein Kompilierfehler auf. - Andernfalls ist eine
Everschiebebare Variable (§24.4) dann:- Wenn der Ausdruck
E.Iein fixed_pointer_initializer (§24.7) ist, ist das Ergebnis des Ausdrucks ein Zeiger auf das erste Element des PufferelementsImit fester Größe inE. - Andernfalls ist der Ausdruck
E.Iein primary_expression (§12.8.12.1) innerhalb eines element_access (§12.8.12) des FormularsE.I[J], dann ist das ErgebnisE.Iein Zeiger,Pauf das erste Element des PufferelementsIfester Größe inE, und die eingeschlossene element_access wird dann als pointer_element_access (§24.6.4)P[J]ausgewertet. - Andernfalls tritt ein Kompilierfehler auf.
- Wenn der Ausdruck
- Andernfalls verweist
Eauf eine feste Variable und das Ergebnis des Ausdrucks ist ein Zeiger auf das erste Element des PufferelementsImit fester Größe inE. Das Ergebnis ist vom TypS*, wobei S der Elementtyp vonIist, 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
24.8.4 Endgültige Zuordnungsprüfung
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.
24.9 Stapelzuordnung
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 Initialisierungsausdruck einer local_variable_declaration (§13.6.2) auftritt, wobei die local_variable_type entweder ein Zeigertyp (§24.3) oder abgeleitet (var) ist, ist das Ergebnis des stackalloc_expression ein Zeiger vom Typ T*, wobei T die unmanaged_type der stackalloc_expression ist. In diesem Fall ist das Ergebnis ein Zeiger auf den Anfang des zugewiesenen Blocks.
In allen anderen Aspekten folgen die Semantik von local_variable_declarations (§13.6.2) und stackalloc_expression(§12.8.22) in unsicheren Kontexten den für sichere Kontexte definierten.
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 }; // Cannot 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 derIntToString-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
IntToStringim 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.
ECMA C# draft specification