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.
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
unsafe
gekennzeichnet, 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 FelderLeft
undRight
als 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
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
inA
bewirkt einfach, dass die textuelle Ausdehnung vonF
zu einem unsicheren Kontext wird, in dem die unsicheren Funktionen der Sprache verwendet werden können. Bei der Überschreibung vonF
inB
muss der Modifikatorunsafe
nicht neu spezifiziert werden - es sei denn, die MethodeF
inB
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
F
einen 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 inA
der Fall ist, oder indem einunsafe
Modifikator in die Methodendeklaration aufgenommen wird, wie es inB
der 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 void
geschrieben, 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 Typint*
bezeichnet der Ausdruck*P
die Variableint
, die an der inP
enthaltenen Adresse gefunden wird. Ende des Beispiels
Wie eine Objektreferenz kann auch ein Zeiger null
sein. 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
int
s, 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
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.I
resultiert, wobeiV
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 FormP->I
, oder ein pointer_element_access (§23.6.4) der FormP[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
,long
oderulong
zu einem beliebigen pointer_type. - Von einem beliebigen pointer_type zu
sbyte
,byte
,short
,ushort
,int
,uint
,long
, oderulong
.
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 byte
umgewandelt 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, überSystem.Array
oder die von ihm implementierten Schnittstellen auf die Array-Elemente zuzugreifen, kann jedoch zur Laufzeit zu einer Ausnahme führen, da Zeigertypen nicht inobject
konvertierbar 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.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-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 (§23.5) von T
(dem Elementtyp) nach V
gibt, wird ein Fehler erzeugt und es werden keine weiteren Schritte unternommen. Wenn x
den Wert null
hat, 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.NullReferenceException
auslö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->I
muss 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).I
ausgewertet. 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 OperationP->I
genau der Operation(*P).I
entspricht, hätte die MethodeMain
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
, long
oder ulong
umgewandelt 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 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
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 E
gegebenen 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 vonp
verwendet wurde, als definitiv zugewiesen. Die Zuweisung an*p
bewirkt 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 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
, long
oder ulong
berechnen 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 P
gegebenen 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 Q
eines 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 TypsT
, sofern der TypT*
implizit in den in derfixed
-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 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 Anweisungfixed
garantiert an einer festen Adresse. Wenn der Array-Ausdrucknull
ist 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 Anweisungfixed
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 derfixed
-Anweisung an einer festen Adresse. Das Verhalten der Anweisungfixed
ist implementierungsabhängig, wenn der Zeichenfolgenausdrucknull
ist. - 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, wobeiT
ein 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 MethodeGetPinnableReference
sollte 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 (§23.8.3), und der Puffer mit fester Größe bleibt garantiert für die Dauer derfixed
-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-Instanza
die 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 a
erhä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 s
erhä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 MethodeGetPinnableReference
mit der richtigen Signatur. In der Anweisungfixed
wird derref int
, der von dieser Methode zurückgegeben wird, wenn sie aufc
aufgerufen wird, zur Initialisierung desint*
-Zeigersp
verwendet. 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 bool
sein.
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 int
konvertierbar 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ößeI
inE
. - Andernfalls ist der Ausdruck
E.I
ein primary_expression (§12.8.12.1) innerhalb eines element_access (§12.8.12) des FormularsE.I[J]
, dann ist das ErgebnisE.I
ein Zeiger,P
auf das erste Element des PufferelementsI
fester Größe inE
und die eingeschlossene element_access wird dann als pointer_element_access (§23.6.4)P[J]
ausgewertet. - Andernfalls tritt ein Kompilierfehler auf.
- Wenn der Ausdruck
- Andernfalls verweist
E
auf eine feste Variable und das Ergebnis des Ausdrucks ist ein Zeiger auf das erste Element des PufferelementsI
mit fester Größe inE
. Das Ergebnis ist vom TypS*
, wobei S der Elementtyp vonI
ist, 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 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
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.
ECMA C# draft specification