12 Ausdrücke
12.1 Allgemein
Ein Ausdruck ist eine Sequenz von Operatoren und Operanden. Diese Klausel definiert die Syntax, die Reihenfolge der Auswertung von Operanden und Operatoren sowie die Bedeutung von Ausdrücken.
12.2 Klassifizierungen von Ausdrücken
12.2.1 Allgemein
Das Ergebnis eines Ausdrucks wird als eines der folgenden klassifiziert:
- Ein -Wert. Jeder Wert verfügt über einen zugeordneten Typ.
- Eine Variable. Sofern nicht anders angegeben, ist eine Variable explizit typisiert und hat einen zugehörigen Typ, nämlich den deklarierten Typ der Variablen. Eine implizit typisierte Variable hat keinen zugehörigen Typ.
- NULL-Literal Ein Ausdruck mit dieser Klassifizierung kann implizit in einen Referenztyp oder Null-Werttyp konvertiert werden.
- Eine anonyme Funktion. Ein Ausdruck mit dieser Klassifizierung kann implizit in einen kompatiblen Delegattyp oder Typ für die Ausdrucksbaumstruktur umgewandelt werden.
- Ein Tupel Jedes Tupel verfügt über eine feste Anzahl von Elementen, wobei jeder einen Ausdruck und einen optionalen Tupelelementnamen hat.
- Eigenschaftszugriff Jeder Eigenschaftszugriff hat einen entsprechenden Typ, nämlich den Typ der Eigenschaft. Darüber hinaus kann ein Eigenschaftszugriff über einen zugeordneten Instanzausdruck verfügen. Wenn ein Accessor eines Instanzeigenschaftszugriffs aufgerufen wird, wird das Ergebnis der Auswertung des Instanzausdrucks zur Instanz, die durch
this
dargestellt wird (§12.8.14). - Indexerzugriff Jeder Zugriff auf einen Indexer hat einen zugehörigen Typ, nämlich den Elementtyp des Indexers. Darüber hinaus verfügt ein Indexerzugriff über einen zugeordneten Instanzausdruck und eine zugeordnete Argumentliste. Wenn ein Accessor eines Indexerzugriffs aufgerufen wird, wird das Ergebnis der Auswertung des Instanzausdrucks zur Instanz, die durch
this
(§12.8.14) dargestellt wird, und das Ergebnis der Auswertung der Argumentliste wird zur Parameterliste des Aufrufs. - Nichts. Dies ist der Fall, wenn der Ausdruck ein Aufruf einer Methode mit einem Rückgabetyp von
void
ist. Ein Ausdruck, der als „nichts“ klassifiziert ist, ist nur im Kontext eines statement_expression (§13.7) oder als Textkörper eines lambda_expression (§12.19) gültig.
Bei Ausdrücken, die als Unterausdrücke größerer Ausdrücke auftreten, kann das Ergebnis mit den genannten Einschränkungen auch als eines der folgenden klassifiziert werden:
- Ein Namespace. Ein Ausdruck mit dieser Klassifizierung kann nur als linke Seite eines member_access (§12.8.7) auftreten. In jedem anderen Kontext verursacht ein als Namespace klassifizierter Ausdruck einen Kompilierungszeitfehler.
- Ein Typ. Ein Ausdruck mit dieser Klassifizierung kann nur als linke Seite eines member_access (§12.8.7) auftreten. In jedem anderen Kontext verursacht ein Ausdruck, der als Typ klassifiziert wurde, einen Kompilierungsfehler.
- Eine Methodengruppe, bei der es sich um eine Reihe überladener Methoden handelt, die sich aus einer Membersuche ergeben (§12.5). Eine Methodengruppe verfügt möglicherweise über einen zugeordneten Instanzausdruck und eine zugeordnete Typargumentliste. Wenn eine Instanzmethode aufgerufen wird, wird das Ergebnis der Auswertung des Instanzausdrucks zur Instanz, die durch
this
dargestellt wird (§12.8.14). Eine Methodengruppe ist in einem invocation_expression (§12.8.10) oder einem delegate_creation_expression (§12.8.17.6) zulässig und kann implizit in einen kompatiblen Delegattyp konvertiert werden (§10.8). In jedem anderen Kontext verursacht ein als Methodengruppe klassifizierter Ausdruck einen Kompilierungszeitfehler. - Ereigniszugriff Jeder Zugriff auf ein Ereignis hat einen zugehörigen Typ, nämlich den Typ des Ereignisses. Darüber hinaus kann ein Ereigniszugriff über einen zugeordneten Instanzausdruck verfügen. Ein Ereigniszugriff kann als linker Operand der Operatoren
+=
und-=
angezeigt werden (§12.21.5). In jedem anderen Kontext verursacht ein Ausdruck, der als Ereigniszugriff klassifiziert ist, einen Kompilierfehler. Wenn ein Accessor eines Instanzereigniszugriffs aufgerufen wird, wird das Ergebnis der Auswertung des Instanzausdrucks zur Instanz, die durchthis
dargestellt wird (§12.8.14). - Ein throw-Ausdruck, der in verschiedenen Kontexten verwendet werden kann, um eine Ausnahme in einem Ausdruck auszulösen. Ein throw-Ausdruck kann durch eine implizite Umwandlung in jeden beliebigen Typ konvertiert werden.
Ein Zugriff auf eine Eigenschaft oder einen Indexer wird immer als Wert umklassifiziert, indem ein Aufruf des get- oder des set-Accessors ausgeführt wird. Der jeweilige Accessor wird durch den Kontext des Eigenschafts- oder Indexerzugriffs bestimmt: Wenn der Zugriff das Ziel einer Zuweisung ist, wird der set-Accessor aufgerufen, um einen neuen Wert zuzuweisen (§12.21.2). Andernfalls wird der get-Accessor aufgerufen, um den aktuellen Wert zu erhalten (§12.2.2).
Ein Instanzaccessor ist ein Eigenschaftszugriff auf eine Instanz, ein Ereigniszugriff auf eine Instanz oder ein Indexerzugriff.
12.2.2 Werte von Ausdrücken
Die meisten Konstrukte, die einen Ausdruck enthalten, erfordern letztendlich, dass der Ausdruck einen Wertkennzeichnet. Wenn der Ausdruck einen Namespace, einen Typ, eine Methodengruppe oder gar nichts bezeichnet, tritt in solchen Fällen ein Kompilierfehler auf. Wenn der Ausdruck jedoch einen Eigenschaftszugriff, einen Indexerzugriff oder eine Variable bezeichnet, wird der Wert der Eigenschaft, des Indexers oder der Variable implizit ersetzt:
- Der Wert einer Variablen ist einfach der Wert, der aktuell in dem durch die Variable identifizierten Speicherort gespeichert ist. Eine Variable muss als definitiv zugewiesen gelten (§9.4), bevor ihr Wert abgerufen werden kann, andernfalls kommt es zu einem Kompilierfehler.
- Der Wert eines Eigenschaftszugriffsausdrucks wird durch das Aufrufen des get-Accessors der Eigenschaft ermittelt. Wenn die Eigenschaft keinen get-Accessor hat, tritt ein Kompilierzeitfehler auf. Andernfalls wird ein Funktionsmemberaufruf (§12.6.6) ausgeführt, und das Ergebnis des Aufrufs wird zum Wert des Eigenschaftszugriffsausdrucks.
- Der Wert eines Indexerzugriffsausdrucks wird durch den Aufruf des get-Accessors des Indexers abgerufen. Wenn der Indexer keinen get-Accessor hat, tritt ein Kompilierzeitfehler auf. Andernfalls wird ein Funktionsmememberaufruf (§12.6.6) mit der Argumentliste ausgeführt, die dem Indexerzugriffsausdruck zugeordnet ist, wobei das Ergebnis des Aufrufs zum Wert des Indexerzugriffsausdrucks wird.
- Der Wert eines Tupelausdrucks wird durch Anwendung einer impliziten Tupelkonvertierung (§10.2.13) auf den Typ des Tupelausdrucks ermittelt. Beim Abrufen des Werts eines Tupelausdrucks, der keinen Typ aufweist, wird ein Fehler ausgegeben.
12.3 Statische und dynamische Bindung
12.3.1 Allgemein
Bindung ist der Prozess des Festlegens, auf was sich eine Operation bezieht, basierend auf dem Typ oder Wert von Ausdrücken (Argumente, Operanden, Empfänger). Die Bindung eines Methodenaufrufs wird beispielsweise auf der Grundlage des Typs des Empfängers und der Argumente bestimmt. Die Bindung eines Operators wird auf der Grundlage des Typs seiner Operanden bestimmt.
In C# wird die Bindung eines Vorgangs normalerweise zur Kompilierungszeit bestimmt, basierend auf dem Kompilierungszeittyp seiner Unterausdrücke. Wenn ein Ausdruck einen Fehler enthält, wird ebenfalls der Fehler erkannt und zur Kompilierungszeit gemeldet. Dieser Ansatz wird als statische Bindung bezeichnet.
Wenn ein Ausdruck jedoch ein dynamischer Ausdruck ist (d.h. den Typ dynamic
hat), bedeutet dies, dass jede Bindung, an der er teilnimmt, auf seinem Laufzeittyp basieren sollte und nicht auf dem Typ, den er bei der Kompilierung hatte. Die Bindung einer solchen Operation wird daher bis zu dem Zeitpunkt aufgeschoben, an dem die Operation während des Ausführens des Programms ausgeführt werden soll. Dies wird als dynamische Bindung bezeichnet.
Wenn eine Operation dynamisch gebunden ist, wird bei der Kompilierung nur wenig oder gar keine Überprüfung durchgeführt. Stattdessen werden Fehler als Ausnahmen zur Laufzeit gemeldet, wenn die Laufzeitbindung fehlschlägt.
Die folgenden Operationen in C# unterliegen der Bindung:
- Memberzugriff:
e.M
- Methodenaufruf:
e.M(e₁,...,eᵥ)
- Delegataufruf:
e(e₁,...,eᵥ)
- Elementzugriff:
e[e₁,...,eᵥ]
- Objekterstellung: neues
C(e₁,...,eᵥ)
- Überladene unäre Operatoren:
+
,-
,!
(nur logische Negation),~
,++
,--
,true
,false
- Überladene binäre Operatoren:
+
,-
,*
,/
,%
,&
,&&
,|
,||
,??
,^
,<<
,>>
,==
,!=
,>
,<
,>=
,<=
- Zuweisungsoperatoren:
=
,= ref
,+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
,<<=
,>>=
- Implizite und explizite Konvertierungen
Wenn keine dynamischen Ausdrücke involviert sind, verwendet C# standardmäßig die statische Bindung, was bedeutet, dass die bei der Kompilierung festgelegten Typen der Unterausdrücke bei der Auswahl verwendet werden. Wenn jedoch einer der Unterausdrücke in den oben aufgeführten Operationen ein dynamischer Ausdruck ist, wird die Operation stattdessen dynamisch gebunden.
Es ist ein Kompilierfehler, wenn ein Methodenaufruf dynamisch gebunden ist und einer der Parameter, einschließlich des Empfängers, ein Eingabeparameter ist.
12.3.2 Bindezeit
Statische Bindung findet zur Kompilierungszeit statt, während die dynamische Bindung zur Laufzeit erfolgt. In den folgenden Unterabschnitten bezieht sich der Begriff Bindungszeit abhängig davon, wann die Bindung erfolgt, auf Kompilierungszeit oder Laufzeit.
Beispiel: Im Folgenden werden die Begriffe der statischen und dynamischen Bindung und der Bindungszeit erläutert:
object o = 5; dynamic d = 5; Console.WriteLine(5); // static binding to Console.WriteLine(int) Console.WriteLine(o); // static binding to Console.WriteLine(object) Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)
Die ersten beiden Aufrufe sind statisch gebunden: Die Überladung von
Console.WriteLine
wird basierend auf dem Kompilierungszeittyp des Arguments ausgewählt. Daher ist die Bindungszeit Kompilierungszeit.Der dritte Aufruf ist dynamisch gebunden: Die Überladung von
Console.WriteLine
wird basierend auf dem Laufzeittyp des Arguments ausgewählt. Dies geschieht, weil das Argument ein dynamischer Ausdruck ist – sein Typ bei der Kompilierung ist dynamisch. Daher ist die Bindungszeit für den dritten Aufruf Laufzeit.Ende des Beispiels
12.3.3 Dynamische Bindung
Dieser Unterabschnitt ist informativ.
Die dynamische Bindung bietet C#-Programmen die Möglichkeit, mit dynamischen Objekten zu interagieren, d.h. mit Objekten, die nicht den normalen Regeln des C#-Typsystems folgen. Dynamische Objekte können Objekte aus anderen Programmiersprachen mit unterschiedlichen Typensystemen sein, oder es handelt sich um Objekte, die programmgesteuert eingerichtet sind, um ihre eigene Bindungsemantik für verschiedene Vorgänge zu implementieren.
Der Mechanismus, mit dem ein dynamisches Objekt seine eigene Semantik implementiert, ist durch die Implementierung festgelegt. Eine bestimmte Schnittstelle – ebenfalls implementierungsdefiniert – wird von dynamischen Objekten implementiert, um der C#-Runtime zu signalisieren, dass sie eine besondere Semantik besitzen. Wenn also Operationen an einem dynamischen Objekt dynamisch gebunden sind, übernimmt deren eigene Bindungssemantik und nicht die von C#, wie in dieser Spezifikation angegeben, die Kontrolle.
Obwohl das Ziel der dynamischen Bindung darin besteht, die Interaktion mit dynamischen Objekten zu ermöglichen, bietet C# die Möglichkeit der dynamischen Bindung für alle Objekte, unabhängig davon, ob sie dynamisch sind oder nicht. Dies ermöglicht eine reibungslosere Integration dynamischer Objekte, da die Ergebnisse von Vorgängen auf ihnen möglicherweise nicht selbst dynamische Objekte sind, aber immer noch von einem unbekannten Typ sind, der dem Programmierer zur Kompilierungszeit nicht bekannt ist. Außerdem kann die dynamische Bindung dazu beitragen, fehleranfälligen reflektionsbasierten Code zu beseitigen, auch wenn keine Objekte dynamische Objekte sind.
12.3.4 Arten von Unterausdrücken
Wenn eine Operation statisch gebunden ist, wird der Typ eines Unterausdrucks (z. B. ein Empfänger, ein Argument, ein Index oder ein Operand) immer als der Compile-Time-Typ dieses Ausdrucks betrachtet.
Bei der dynamischen Bindung einer Operation wird der Typ eines Unterausdrucks je nach Typ bei der Kompilierung des Unterausdrucks auf unterschiedliche Weise bestimmt:
- Ein Unterausdruck des dynamischen Kompilierungstyps wird als Typ des tatsächlichen Werts betrachtet, für den der Ausdruck zur Laufzeit ausgewertet wird.
- Ein Unterausdruck, dessen Typ bei der Kompilierung ein Typ-Parameter ist, hat den Typ, an den der Typ-Parameter zur Laufzeit gebunden ist
- Andernfalls wird der Unterausdruck als Kompilierungszeittyp betrachtet.
12.4 Operatoren
12.4.1 Allgemein
Ausdrücke bestehen aus Operanden und Operatoren. Die Operatoren eines Ausdrucks geben an, welche Operationen auf die Operanden angewendet werden.
Beispiel: Beispiele für Operatoren sind
+
,-
,*
,/
undnew
. Beispiele für Operanden sind Literale, Felder, lokale Variablen und Ausdrücke. Ende des Beispiels
Es gibt drei Arten von Operatoren:
- Unäre Operatoren. Die unären Operatoren verwenden einen Operanden und nutzen entweder die Präfixnotation (z. B.
–x
) oder die Postfixnotation (z. B.x++
). - Binäre Operatoren Die binären Operatoren nehmen zwei Operanden und verwenden die Infixnotation (z. B.
x + y
). - Ternärer Operator Nur ein ternärer Operator,
?:
, ist vorhanden; er akzeptiert drei Operanden und verwendet die Infix-Notation (c ? x : y
).
Die Reihenfolge der Auswertung von Operatoren in einem Ausdruck wird durch die Rangfolge und Assoziivität der Operatoren bestimmt (§12.4.2).
Die Operanden eines Ausdrucks werden von links nach rechts ausgewertet.
Beispiel: In
F(i) + G(i++) * H(i)
wird MethodeF
mit dem alten Wert voni
aufgerufen, dann wird MethodeG
mit dem alten Wert voni
aufgerufen und schließlich wird MethodeH
mit dem neuen Wert von i aufgerufen. Dies ist unabhängig von und steht in keinem Zusammenhang mit der Rangfolge des Operators. Ende des Beispiels
Bestimmte Operatoren können überladen werden. Das Überladen von Operatoren (§12.4.3) ermöglicht die Angabe benutzerdefinierter Operatorimplementierungen für Vorgänge, in denen einer der Operanden oder beide einer benutzerdefinierten Klasse oder einem benutzerdefinierten Strukturtyp angehören.
12.4.2 Operatorenrangfolge und -assoziativität
Wenn ein Ausdruck mehrere Operatoren enthält, steuert die Priorität der Operatoren die Reihenfolge, in der die einzelnen Operatoren ausgewertet werden.
Anmerkung: Zum Beispiel wird der Ausdruck
x + y * z
alsx + (y * z)
ausgewertet, weil der*
-Operator eine höhere Priorität hat als der binäre+
-Operator. Hinweisende
Die Rangfolge eines Operators wird durch die Definition der zugehörigen Grammatikproduktion festgelegt.
Hinweis: Beispielsweise besteht ein additive_expression aus einer Sequenz von multiplicative_expression, die durch die Operatoren
+
oder-
getrennt sind, wodurch die Operatoren+
und-
eine niedrigere Priorität als die*
,/
und%
genießen. Hinweisende
Hinweis: In der folgenden Tabelle sind alle Operatoren in absteigender Reihenfolge aufgeführt:
Unterklausel Kategorie Operatoren §12.8 Primär x.y
x?.y
f(x)
a[x]
a?[x]
x++
x--
x!
new
typeof
default
checked
unchecked
delegate
stackalloc
§12.9 Unär +
-
!x
~
++x
--x
(T)x
await x
§12.10 Multiplikativ *
/
%
§12.10 Additiv +
-
§12.11 Shift <<
>>
§12.12 Relational und Typtest <
>
<=
>=
is
as
§12.12 Gleichheit ==
!=
§12.13 Logisches AND &
§12.13 Logisches XOR ^
§12.13 Logisches OR \|
§12.14 Bedingtes AND &&
§12.14 Bedingtes OR \|\|
§12.15 und §12.16 Null-Koaleszenz und throw-Ausdruck ??
throw x
§12.18 Bedingt ?:
§12.21 und §12.19 Zuweisungs- und Lambda-Ausdrücke =
= ref
*=
/=
%=
+=
-=
<<=
>>=
&=
^=
\|=
=>
Hinweisende
Wenn ein Operand zwischen zwei Operatoren mit derselben Priorität auftritt, steuert die Assoziativität der Operatoren die Reihenfolge, in der die Operationen ausgeführt werden:
- Mit Ausnahme der Zuweisungsoperatoren und des Null-Koaleszenz-Operators sind alle binären Operatoren links-assoziativ, was bedeutet, dass die Operationen von links nach rechts ausgeführt werden.
Beispiel:
x + y + z
wird als(x + y) + z
bewertet. Ende des Beispiels - Die Zuweisungsoperatoren, die NULL-Sammeloperatoren und der bedingte Operator (
?:
) sind rechtsassoziativ, d. h., die Operationen werden von rechts nach links ausgeführt.Beispiel:
x = y = z
wird alsx = (y = z)
bewertet. Ende des Beispiels
Priorität und Assoziativität können mithilfe von Klammern gesteuert werden.
Beispiel:
x + y * z
multipliziert zuersty
mitz
und addiert dann das Ergebnis zux
, aber(x + y) * z
addiert zuerstx
undy
und multipliziert dann das Ergebnis mitz
. Ende des Beispiels
12.4.3 Operatorüberladung
Alle unären und binären Operatoren haben vordefinierte Implementierungen. Darüber hinaus können benutzerdefinierte Implementierungen durch Einschließen von Operatordeklarationen (§15.10) in Klassen und Strukturen eingeführt werden. Benutzerdefinierte Implementierungen von Operatoren haben immer Priorität vor vordefinierten Implementierungen von Operatoren: Nur wenn keine anwendbaren benutzerdefinierten Operator-Implementierungen existieren, werden die vordefinierten Operator-Implementierungen berücksichtigt, wie in §12.4.4 und §12.4.5 beschrieben.
Die überladbaren unären Operatoren sind:
+ - !
(nur logische Negation) ~ ++ -- true false
Hinweis: Obwohl
true
undfalse
nicht explizit in Ausdrücken verwendet werden (und daher nicht in der Rangfolgentabelle in §12.4.2enthalten sind), gelten diese als Operatoren, da sie in mehreren Ausdruckskontexten aufgerufen werden: Boolesche Ausdrücke (§12.24) und Ausdrücke, die die bedingte (§12.18) und bedingte logische Operatoren verwenden (§12.14). Hinweisende
Hinweis: Der nulltolerante Operator (postfix
!
, §12.8.9) ist kein überladbarer Operator. Hinweisende
Die überladbaren binären Operatoren sind:
+ - * / % & | ^ << >> == != > < <= >=
Nur die oben aufgeführten Operatoren können überladen werden. Insbesondere ist es nicht möglich, den Zugriff auf Member, Methodenaufrufe oder die Operatoren =
, &&
, ||
, ??
, ?:
, =>
, checked
, unchecked
, new
, typeof
, default
, as
und is
zu überladen.
Wenn ein binärer Operator überladen wird, wird der entsprechende zusammengesetzte Zuweisungsoperator, sofern vorhanden, ebenfalls implizit überladen.
Beispiel: Eine Überladung des Operators
*
ist auch eine Überladung des Operators*=
. Dies wird in §12.21 näher beschrieben. Ende des Beispiels
Der Zuweisungsoperator (=)
kann nicht überladen werden. Eine Zuweisung speichert immer einfach einen Wert in einer Variablen (§12.21.2).
Umwandlungsvorgänge wie (T)x
werden durch die Bereitstellung benutzerdefinierter Konvertierungen überladen (§10.5).
Hinweis: Benutzerdefinierte Konvertierungen wirken sich nicht auf das Verhalten der Operatoren
is
oderas
aus. Hinweisende
Elementzugriff, wie beispielsweise a[x]
, wird nicht als überladbarer Operator betrachtet. Stattdessen wird die benutzerdefinierte Indizierung über Indexer unterstützt (§15.9).
In Ausdrücken werden Operatoren durch die Operator-Notation referenziert, in Deklarationen durch die funktionale Notation. Die folgende Tabelle zeigt die Beziehung zwischen Operator- und funktionaler Notation für unäre und binäre Operatoren. Im ersten Eintrag bezeichnet „op” einen überladbaren unären Präfixoperator. Im zweiten Eintrag bezeichnet „op” die unären Postfix-Operatoren ++
und --
. Im dritten Eintrag bezeichnet «op» einen überladbaren binären Operator.
Hinweis: Ein Beispiel für die Überladung der
++
und--
Operatoren finden Sie unter §15.10.2. Hinweisende
Operatornotation | Funktionale Notation |
---|---|
«op» x |
operator «op»(x) |
x «op» |
operator «op»(x) |
x «op» y |
operator «op»(x, y) |
Benutzerdefinierte Operator-Deklarationen erfordern immer, dass mindestens einer der Parameter zu der Klasse oder dem Struct-Typ gehört, der die Operator-Deklaration enthält.
Anmerkung: Es ist also nicht möglich, dass ein benutzerdefinierter Operator die gleiche Signatur hat wie ein vordefinierter Operator. Hinweisende
Benutzerdefinierte Operator-Deklarationen können die Syntax, die Priorität oder die Assoziativität eines Operators nicht verändern.
Beispiel: Der Operator
/
ist immer ein binärer Operator, hat immer die Rangfolgeebene, die in §12.4.2angegeben ist, und ist immer linksassoziativ. Ende des Beispiels
Hinweis: Obwohl es möglich ist, dass ein benutzerdefinierter Operator beliebige Berechnungen durchführt, wird dringend von Implementierungen abgeraten, die andere Ergebnisse als diejenigen erzeugen, die intuitiv erwartet werden. Beispielsweise sollte eine Implementierung des Operators
==
die beiden Operanden auf Gleichheit prüfen und ein entsprechendesbool
-Ergebnis zurückgeben. Hinweisende
Die Beschreibungen der einzelnen Operatoren in §12.9 bis §12.21 geben die vordefinierten Implementierungen der Operatoren und alle zusätzlichen Regeln an, die für jeden Operator gelten. Die Beschreibungen verwenden die Begriffe unäre Operatorüberladungsauflösung, binäre Operatorüberladungsauflösung, numerische Heraufstufung und erweiterte Operatordefinitionen, die in den folgenden Unterabschnitten zu finden sind.
12.4.4 Überladungsauflösung für unäre Operatoren
Eine Operation der Form «op» x
oder x «op»
, wobei „op“ ein überladbarer unärer Operator ist und x
ein Ausdruck vom Typ X
ist, wird wie folgt verarbeitet:
- Die Gruppe der von
X
für den Vorgangoperator «op»(x)
bereitgestellten benutzerdefinierten Operatoren wird gemäß den Regeln von §12.4.6 bestimmt. - Wenn das Set der in Frage kommenden benutzerdefinierten Operatoren nicht leer ist, wird dieses Set zum Set der in Frage kommenden Operatoren für die Operation. Andernfalls werden die vordefinierten binären
operator «op»
-Implementierungen, einschließlich ihrer aufgehobenen Formulare, zum Satz von Kandidatenoperatoren für den Vorgang. Die vordefinierten Implementierungen eines bestimmten Operators werden in der Beschreibung des Operators angegeben. Die vordefinierten Operatoren, die von einem Enumerations- oder Delegattyp bereitgestellt werden, sind nur dann in diesem Satz enthalten, wenn der Typ zum Zeitpunkt der Bindung – oder der zugrunde liegende Typ, wenn es sich um einen Typ mit Nullwert handelt – eines der Operanden der Enumerations- oder Delegattyp ist. - Die Überladungsauflösungsregeln von §12.6.4 werden auf das Set der Kandidatenoperatoren angewendet, um den besten Operator in Bezug auf die Argumentliste
(x)
auszuwählen, und dieser Operator wird das Ergebnis der Überladungsauflösung. Wenn die Überladungsauflösung keinen einzigen besten Operator auswählt, tritt ein Bindungsfehler auf.
12.4.5 Überladungsauflösung für binäre Operatoren
Eine Operation der Form x «op» y
, wobei "op" ein überladbarer binärer Operator, x
ein Ausdruck vom Typ X
und y
ein Ausdruck vom Typ Y
ist, wird wie folgt verarbeitet:
- Der von
X
undY
bereitgestellte Satz benutzerdefinierter Kandidatenoperatoren für den Vorgangoperator «op»(x, y)
ist bestimmt. Die Gruppe besteht aus der Vereinigung der vonX
und vonY
bereitgestellten möglichen Operatoren, die jeweils durch die Regeln in §12.4.6 bestimmt werden. Für den kombinierten Satz werden Kandidaten wie folgt zusammengeführt:- Wenn
X
undY
identitätskonvertierbar sind oderX
undY
von einem gemeinsamen Basistyp abgeleitet werden, treten gemeinsame Kandidatenoperatoren nur einmal in der kombinierten Menge auf. - Wenn es eine Identitätskonvertierung zwischen
X
undY
gibt, weist ein Operator«op»Y
, der vonY
bereitgestellt wird, denselben Rückgabetyp wie ein von«op»X
bereitgestellterX
auf, und die Operandentypen von«op»Y
eine Identitätskonvertierung in die entsprechenden Operandentypen von«op»X
haben, tritt nur«op»X
im Satz auf.
- Wenn
- Wenn das Set der in Frage kommenden benutzerdefinierten Operatoren nicht leer ist, wird dieses Set zum Set der in Frage kommenden Operatoren für die Operation. Andernfalls werden die vordefinierten binären
operator «op»
-Implementierungen, einschließlich ihrer aufgehobenen Formulare, zum Satz von Kandidatenoperatoren für den Vorgang. Die vordefinierten Implementierungen eines bestimmten Operators werden in der Beschreibung des Operators angegeben. Bei vordefinierten Enumerations- und Delegatenoperatoren sind die einzigen Operatoren diejenigen, die von einem Enumerations- oder Delegattyp bereitgestellt werden, der zur Bindungszeit der Typ eines der Operanden ist. - Die Überladungsauflösungsregeln von §12.6.4 werden auf das Set der Kandidatenoperatoren angewendet, um den besten Operator in Bezug auf die Argumentliste
(x, y)
auszuwählen, und dieser Operator wird das Ergebnis der Überladungsauflösung. Wenn die Überladungsauflösung keinen einzigen besten Operator auswählt, tritt ein Bindungsfehler auf.
12.4.6 Mögliche benutzerdefinierte Operatoren
Bei einem Typ T
und einem Vorgang operator «op»(A)
, wobei „op” ein überladbarer Operator und A
eine Argumentliste ist, wird die Gruppe möglicher benutzerdefinierter Operatoren, die von T
für den Operator «op»(A)
bereitstellt, wie folgt bestimmt:
- Bestimmen Sie den Typ
T₀
. WennT
ein nullbarer Werttyp ist, istT₀
sein zugrunde liegender Typ; andernfalls istT₀
gleichT
. - Für alle
operator «op»
-Deklarationen inT₀
und alle aufgehobenen (Lifted) Formen solcher Operatoren besteht die Gruppe möglicher Operatoren aus allen anwendbaren Operatoren in , sofern mindestens ein Operator anwendbar ist (A
) in Bezug auf die ArgumentlisteT₀
. - Andernfalls, wenn es sich bei
T₀
umobject
handelt, ist die Gruppe der möglichen Operatoren leer. - Andernfalls ist der Satz der Kandidatenoperatoren, den
T₀
bereitstellt, entweder der Satz, den die direkte Basisklasse vonT₀
bereitstellt, oder der Satz der effektiven Basisklasse vonT₀
, wennT₀
ein Typparameter ist.
12.4.7 Numerische Heraufstufungen
12.4.7.1 Allgemein
Dieser Unterabschnitt ist informativ.
§12.4.7 und dessen Unterabschnitte sind eine Zusammenfassung der kombinierten Wirkung von:
- Regeln für implizite numerische Konvertierungen (§10.2.3)
- Regeln für eine bessere Konvertierung (§12.6.4.7)
- die verfügbaren arithmetischen (§12.10), relationalen (§12.12) und integralen logischen (§12.13.2) Operatoren.
Die numerische Höherstufung besteht darin, bestimmte implizite Konvertierungen der Operanden der vordefinierten unären und binären numerischen Operatoren automatisch auszuführen. Numerische Höherstufung ist kein eindeutiger Mechanismus, sondern eine Auswirkung der Anwendung der Überladungsauflösung auf die vordefinierten Operatoren. Die numerische Promotion hat keinen Einfluss auf die Auswertung von benutzerdefinierten Operatoren, obwohl benutzerdefinierte Operatoren implementiert werden können, um ähnliche Effekte zu erzielen.
Betrachten Sie als Beispiel für eine numerische Heraufstufung die vordefinierten Implementierungen des binären Operators *
:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);
Wenn Überladungsauflösungsregeln (§12.6.4) auf diesen Operatorsatz angewendet werden, besteht der Effekt darin, den ersten Operator auszuwählen, für den implizite Konvertierungen von den Operandentypen existieren.
Beispiel: Für den Vorgang
b * s
, wobeib
einbyte
ist unds
einshort
ist, wählt die Überladungsauflösungoperator *(int, int)
als besten Operator aus. Die Auswirkung ist daher, dassb
unds
inint
konvertiert werden. Der Typ des Ergebnisses istint
. Ebenso im Falle des Vorgangsi * d
, wobeii
einint
undd
eindouble
ist,overload
wird von der Auflösungoperator *(double, double)
als bester Operator ausgewählt. Ende des Beispiels
Ende des informativen Textes.
12.4.7.2 Unäre numerische Heraufstufungen
Dieser Unterabschnitt ist informativ.
Für die Operanden der vordefinierten unären Operatoren +
, -
und ~
erfolgt eine unäre numerische Heraufstufung. Eine unäre numerische Heraufstufung besteht einfach darin, Operanden vom Typ sbyte
, byte
, short
, ushort
oder char
in den Typ int
zu konvertieren. Darüber hinaus wandelt die unäre numerische Heraufstufung für den unären Operator Operanden vom Typ uint
in den Typ long
um.
Ende des informativen Textes.
12.4.7.3 Binäre numerische Heraufstufungen
Dieser Unterabschnitt ist informativ.
Für die Operanden der vordefinierten unären Operatoren +
, -
, *
, /
, %
, &
, |
, ^
, ==
, !=
, >
, <
, >=
und <=
erfolgt eine binäre numerische Höherstufung. Die binäre numerische Promotion konvertiert implizit beide Operanden in einen gemeinsamen Typ, der im Falle der nicht-relationalen Operatoren auch der Ergebnistyp der Operation wird. Die binäre numerische Promotion besteht aus der Anwendung der folgenden Regeln, in der Reihenfolge, in der sie hier erscheinen:
- Wenn einer der beiden Operanden vom Typ
decimal
ist, wird der andere Operand in den Typdecimal
konvertiert, oder es tritt ein Bindungszeitfehler auf, wenn der andere Operand vom Typfloat
oderdouble
ist. - Andernfalls, wenn einer der beiden Operanden vom Typ
double
ist, wird der andere Operand in den Typdouble
umgewandelt. - Andernfalls, wenn einer der beiden Operanden vom Typ
float
ist, wird der andere Operand in den Typfloat
umgewandelt. - Andernfalls, wenn ein Operand vom Typ
ulong
ist, wird der andere Operand in den Typulong
konvertiert oder ein Bindungszeitfehler tritt auf, falls der andere Operand vom Typtype sbyte
,short
,int
oderlong
ist. - Andernfalls, wenn einer der beiden Operanden vom Typ
long
ist, wird der andere Operand in den Typlong
umgewandelt. - Andernfalls, wenn einer der Operanden vom Typ
uint
ist und der andere Operand den Typsbyte
,short
oderint
hat, werden beide Operanden in den Typlong
konvertiert. - Andernfalls, wenn einer der beiden Operanden vom Typ
uint
ist, wird der andere Operand in den Typuint
umgewandelt. - Andernfalls werden beide Operanden in den Typ
int
konvertiert.
Anmerkung: Die erste Regel verbietet alle Operationen, die den Typ
decimal
mit den Typendouble
undfloat
vermischen. Die Regel ergibt sich aus der Tatsache, dass es keine impliziten Konversionen zwischen dem Typdecimal
und den Typendouble
undfloat
gibt. Hinweisende
Anmerkung: Beachten Sie auch, dass es nicht möglich ist, dass ein Operand vom Typ
ulong
ist, wenn der andere Operand von einem vorzeichenbehafteten Integraltypen ist. Der Grund dafür ist, dass es keinen Integraltypen gibt, der den gesamten Bereich vonulong
sowie die Integraltypen mit Vorzeichen darstellen kann. Hinweisende
In beiden oben genannten Fällen kann ein Umwandlungsausdruck verwendet werden, um einen Operanden explizit in einen Typ zu konvertieren, der mit dem anderen Operanden kompatibel ist.
Beispiel: Im folgenden Code
decimal AddPercent(decimal x, double percent) => x * (1.0 + percent / 100.0);
Ein Bindungszeitfehler tritt auf, da
decimal
nicht mitdouble
multipliziert werden kann. Der Fehler wird behoben, indem der zweite Operand explizit indecimal
umgewandelt wird, wie folgt:decimal AddPercent(decimal x, double percent) => x * (decimal)(1.0 + percent / 100.0);
Ende des Beispiels
Ende des informativen Textes.
12-4.8 „Lifted“ Operatoren
Angehobene Operatoren ermöglichen es, dass vordefinierte und benutzerdefinierte Operatoren, die mit nicht-nullbaren Werttypen arbeiten, auch mit den nullbaren Formen dieser Typen verwendet werden können. Transformierte Operatoren werden aus vordefinierten und benutzerdefinierten Operatoren erstellt, die bestimmte Anforderungen erfüllen, wie in den folgenden Beispielen beschrieben:
- Für die unären Operatoren
+
,++
,-
,--
,!
(logische Negation) und~
existiert eine gehobene Form des Operators, wenn sowohl der Operand als auch die Ergebnistypen nicht-nullbare Werttypen sind. Die transformierte Form wird erstellt, indem dem Operanden und den Ergebnistypen ein einzelner?
-Modifizierer hinzugefügt wird. Der angehobene Operator erzeugt einennull
-Wert, wenn der Operandnull
ist. Andernfalls entpackt der „Lifted” Operator den Operanden, wendet den zugrunde liegenden Operator an und umschließt das Ergebnis. - Für die binären Operatoren
+
,-
,*
,/
,%
,&
,|
,^
,<<
und>>
existiert eine erweiterte Form (lifted) eines Operators, wenn die Operanden- und Ergebnistypen alle nicht-nullbare Wertetypen sind. Das „Lifted” Formular wird erstellt, indem jedem Operanden und den Ergebnistypen ein einzelner?
-Modifizierer hinzugefügt wird. Der „Lifted” Operator erzeugt einennull
-Wert, wenn ein oder beide Operandennull
sind (eine Ausnahme sind die Operatoren&
und|
desbool?
-Typs, wie in §12.13.5 beschrieben). Andernfalls entpackt der „Lifted” Operator die Operanden, wendet den zugrunde liegenden Operator an und umschließt das Ergebnis. - Für die Gleichheitsoperatoren
==
und!=
existiert eine gehobene Form eines Operators, wenn die Operandentypen beide nicht-nullbare Werttypen sind und wenn der Ergebnistypbool
ist. Das „Lifted” Formular wird erstellt, indem jedem Operanden ein einzelner?
-Modifizierer hinzugefügt wird. Der „Lifted” Operator betrachtet zweinull
-Werte als gleich und einennull
-Wert als ungleich zu jedem Wert ohnenull
. Wenn beide Operanden nichtnull
sind, öffnet der „Lifted” Operator die Operanden und wendet den zugrunde liegenden Operator an, um das Ergebnisbool
zu erzeugen. - Für die relationalen Operatoren
<
,>
,<=
und>=
existiert eine erweiterte Form eines Operators, wenn die Operandentypen beide nicht-nullfähige Werttypen sind und der Ergebnistypbool
ist. Das „Lifted” Formular wird erstellt, indem jedem Operanden ein einzelner?
-Modifizierer hinzugefügt wird. Der „Lifted” Operator erzeugt den Wertfalse
, wenn ein oder beide Operandennull
sind. Andernfalls entpackt der transformierte Operator die Operanden und wendet den zugrunde liegenden Operator an, um das Ergebnisbool
zu erzeugen.
12.5 Mitgliedersuche
12.5.1 Allgemein
Eine Membersuche ist der Vorgang, bei dem die Bedeutung eines Namens im Kontext eines Typs bestimmt wird. Eine Membersuche kann bei der Auswertung eines simple_name (§12.8.4) oder eines member_access (§12.8.7) in einem Ausdruck erfolgen. Tritt simple_name oder member_access als primary_expression von invocation_expression (§12.8.10.2) auf, wird der Member aufgerufen.
Wenn ein Member eine Methode oder ein Ereignis oder eine Konstante, ein Feld oder eine Eigenschaft vom Typ „Delegat“ (§20) oder vom Typ dynamic
(§8.2.4) ist, gilt der Member als aufrufbar.
Die Mitgliedersuche berücksichtigt nicht nur den Namen eines Mitglieds, sondern auch die Anzahl der Typparameter, die das Mitglied hat, und ob auf das Mitglied zugegriffen werden kann. Zum Zweck der Membersuche haben generische Methoden und geschachtelte generische Typen die in ihren jeweiligen Deklarationen angegebene Anzahl an Typparametern, während alle anderen Member keinen einzigen Typparameter haben.
Die Membersuche eines Namens N
mit K
-Typargumenten in einem Typ T
wird wie folgt verarbeitet:
- Zunächst wird eine Reihe von zugänglichen Membern mit dem Namen
N
bestimmt:- Wenn
T
ein Typparameter ist, ist der Satz die Vereinigung der Gruppen von zugänglichen Membern namensN
in jedem der Typen, die als primäre Einschränkung oder sekundäre Einschränkung (§15.2.5) fürT
angegeben sind, zusammen mit der Gruppe der zugänglichen Member namensN
inobject
. - Andernfalls besteht der Satz aus allen zugänglichen (§7.5) Membern mit dem Namen
N
inT
, einschließlich geerbter Member und der zugänglichen Member mit dem NamenN
inobject
. WennT
ein konstruierter Typ ist, wird das Set der Mitglieder durch Ersetzen von Typargumenten wie in §15.3.3 beschrieben abgerufen. Member, die einenoverride
-Modifizierer enthalten, werden aus der Gruppe ausgeschlossen.
- Wenn
- Wenn
K
Null ist, werden als nächstes alle eingebetteten Typen, deren Deklarationen Typparameter enthalten, entfernt. WennK
nicht Null ist, werden alle Mitglieder mit einer anderen Anzahl von Typparametern entfernt. WennK
null ist, werden Methoden mit Typparametern nicht entfernt, da der Typableitungsprozess (§12.6.3) möglicherweise in der Lage ist, die Typargumente abzuleiten. - Als Nächstes, wenn der Member aufgerufen wird, werden alle nicht aufrufbaren Member aus der Menge entfernt.
- Im nächsten Schritt werden Member, die von anderen Membern ausgeblendet sind, aus der Gruppe entfernt. Für jeden Member
S.M
in der Gruppe (wobeiS
der Typ ist, in dem der MemberM
deklariert wird) werden die folgenden Regeln angewendet:- Wenn
M
eine Konstante, ein Feld, eine Eigenschaft, ein Ereignis oder ein Aufzählungsmitglied ist, werden alle Mitglieder, die in einem Basistyp vonS
deklariert sind, aus dem Set entfernt. - Wenn
M
eine Typdeklaration ist, werden alle in einem Basistyp vonS
deklarierten Nichttypen aus der Menge entfernt, und alle Typdeklarationen mit derselben Anzahl von Typparametern wieM
in einem Basistyp vonS
werden ebenfalls aus der Menge entfernt. - Wenn
M
eine Methode ist, werden alle Nicht-Methoden-Member, die in einem Basistyp vonS
deklariert sind, aus der Gruppe entfernt.
- Wenn
- Als nächstes werden Schnittstellenmember, die von Klassenmembern verdeckt sind, aus der Gruppe entfernt. Dieser Schritt hat nur Auswirkungen, wenn
T
ein Typparameter ist undT
sowohl eine effektive Basisklasse alsobject
als auch einen nicht leeren effektiven Schnittstellensatz aufweist (§15.2.5). Für jedes ElementS.M
in der Menge, wobeiS
der Typ ist, in dem das ElementM
deklariert wird, werden die folgenden Regeln angewendet, fallsS
eine Klassendeklaration ist, die nichtobject
ist:- Wenn
M
eine Konstante, ein Feld, eine Eigenschaft, ein Ereignis, ein Aufzählungsmitglied oder eine Typdeklaration ist, dann werden alle in einer Schnittstellendeklaration deklarierten Mitglieder aus dem Set entfernt. - Wenn
M
eine Methode ist, werden alle in einer Schnittstellendeklaration deklarierten Nicht-Methodenmitglieder aus der Menge entfernt, und alle Methoden mit derselben Signatur wieM
, die in einer Schnittstellendeklaration deklariert sind, aus der Menge entfernt.
- Wenn
- Schließlich, nachdem ausgeblendete Member entfernt wurden, wird das Ergebnis der Suche bestimmt:
- Besteht das Set aus einem einzigen Mitglied, das keine Methode ist, so ist dieses Mitglied das Ergebnis der Mitgliedersuche.
- Wenn die Gruppe nur Methoden enthält, ist diese Methodengruppe das Ergebnis des Suchvorgangs.
- Andernfalls ist die Suche mehrdeutig, und ein Bindungszeitfehler tritt auf.
Bei Membersuchen in anderen Typen als Typparametern und Schnittstellen sowie bei Membersuchen in Schnittstellen, die eine einfache Vererbung aufweisen (jede Schnittstelle in der Vererbungskette hat genau null oder eine direkte Basisschnittstelle), bewirken die Suchregeln einfach, dass abgeleitete Member Basismember mit demselben Namen oder derselben Signatur verdecken. Solche Einzelvererbungssuchen sind nie mehrdeutig. Die Mehrdeutigkeiten, die möglicherweise aus Membersuchen in Mehrfachvererbungsschnittstellen entstehen können, werden in §18.4.6 beschrieben.
Hinweis: In dieser Phase wird nur eine Art von Mehrdeutigkeit berücksichtigt. Wenn die Mitgliedersuche zu einer Methodengruppe führt, können weitere Verwendungen der Methodengruppe aufgrund von Mehrdeutigkeit fehlschlagen, zum Beispiel wie in §12.6.4.1 und §12.6.6.2 beschrieben. Hinweisende
12.5.2 Basistypen
Für Zwecke der Membersuche wird ein Typ T
als Typ mit den folgenden Basistypen berücksichtigt:
- Wenn
T
object
oderdynamic
ist, dann hatT
keinen Basistyp. - Wenn
T
ein enum_type ist, sind die Basistypen vonT
die KlassentypenSystem.Enum
,System.ValueType
undobject
. - Wenn
T
ein struct_type ist, sind die Basistypen vonT
die KlassentypenSystem.ValueType
undobject
.Hinweis: Ein nullable_value_type ist ein struct_type (§8.3.1). Hinweisende
- Wenn
T
ein class_typeist, sind die Basistypen vonT
die Basisklassen vonT
, einschließlich des Klassentypsobject
. - Wenn
T
ein interface_type-Element ist, sind die Basistypen vonT
die Basisschnittstellen vonT
, und der Klassentyp istobject
. - Wenn
T
ein array_type ist, sind die Basistypen vonT
die KlassentypenSystem.Array
undobject
. - Wenn
T
ein Delegate-Typ ist, dann sind die Basistypen vonT
die KlassentypenSystem.Delegate
undobject
.
12.6 Funktionsmitglieder
12.6.1 Allgemein
Funktionsmember sind Member, die ausführbare Anweisungen enthalten. Funktionsmitglieder sind immer Mitglieder von Typen und können keine Mitglieder von Namespaces sein. C# definiert die folgenden Kategorien von Funktionsmitgliedern:
- Methoden
- Eigenschaften
- Ereignisse
- Indexer
- Benutzerdefinierte Operatoren
- Instance constructors (Instanzkonstruktoren)
- Statische Konstruktoren
- Finalizer
Mit Ausnahme von Finalizern und statischen Konstruktoren (die nicht explizit aufgerufen werden können), werden die Anweisungen, die in den Funktionsmitgliedern enthalten sind, durch Aufrufe von Funktionsmitgliedern ausgeführt. Die tatsächliche Syntax zum Schreiben eines Funktionsmemberaufrufs hängt von der jeweiligen Funktionsmemberkategorie ab.
Die Argumentliste (§12.6.2) eines Funktionsmemberaufrufs liefert aktuelle Werte oder Variablenreferenzen für die Parameter des Funktionsmembers.
Aufrufe generischer Methoden können mit Typinferenz verwendet werden, um die Gruppe von Typargumenten zu bestimmen, die an die Methode übergeben werden sollen. Dieser Prozess wird in §12.6.3 beschrieben.
Aufrufe von Methoden, Indexern, Operatoren und Instanzkonstruktoren verwenden die Überladungsauflösung, um zu bestimmen, welcher Member einer Kandidatengruppe mit Funktionsmembern aufgerufen werden soll. Dieser Prozess wird in §12.6.4 beschrieben.
Sobald ein bestimmter Funktionsmember zur Bindungszeit identifiziert wurde – möglicherweise durch Überladungsauflösung – wird der tatsächliche Laufzeitprozess zum Aufrufen des Funktionsmembers in §12.6.6 beschrieben.
Anmerkung: Die folgende Tabelle fasst die Verarbeitung zusammen, die in Konstrukten stattfindet, die die sechs Kategorien von explizit aufrufbaren Funktionsmitgliedern beinhalten. In der Tabelle bezeichnen
e
,x
,y
undvalue
Ausdrücke, die als Variablen oder Werte klassifiziert sind,T
einen Ausdruck, der als Typ klassifiziert ist,F
ist der einfache Name einer Methode undP
ist der einfache Name einer Eigenschaft.
Konstrukt Beispiel Beschreibung Methodenaufruf F(x, y)
Die Überladungsauflösung wird angewendet, um die beste Methode F
in der enthaltenden Klasse oder Struktur auszuwählen. Die Methode wird mit der Argumentliste(x, y)
aufgerufen. Wenn die Methode nichtstatic
ist, ist der Instanzausdruckthis
.T.F(x, y)
Die Überladungsauflösung wird angewendet, um die beste Methode F
in der enthaltenden Klasse oder StrukturT
auszuwählen. Ein Bindungszeitfehler tritt auf, wenn die Methode nichtstatic
ist. Die Methode wird mit der Argumentliste(x, y)
aufgerufen.e.F(x, y)
Überladungsauflösung wird angewendet, um die beste Methode F
in der Klasse, Struktur oder Schnittstelle auszuwählen, die vom Type
angegeben wird. Ein Bindungszeitfehler tritt auf, wenn die Methodestatic
ist. Die Methode wird mit dem Instanzausdrucke
und der Argumentliste(x, y)
aufgerufen.Eigenschaftenzugriff P
Der get-Accessor der Eigenschaft P
in der enthaltenden Klasse oder Struktur wird aufgerufen. WennP
nur einen Schreibzugriff hat, tritt ein Kompilierungszeitfehler auf. WennP
nichtstatic
ist, ist der Instanzausdruckthis
.P = value
Der set-Accessor der Eigenschaft P
in der enthaltenden Klasse oder Struktur wird mit der Argumentliste(value)
aufgerufen. WennP
schreibgeschützt ist, tritt ein Kompilierungszeitfehler auf. WennP
nichtstatic
ist, ist der Instanzausdruckthis
.T.P
Der get-Accessor der Eigenschaft P
in der enthaltenden Klasse oder StrukturT
wird aufgerufen. Ein Kompilierfehler tritt auf, wennP
nichtstatic
ist oder wennP
schreibgeschützt ist.T.P = value
Der set-Accessor der Eigenschaft P
in der enthaltenden Klasse oder StrukturT
wird mit der Argumentliste(value)
aufgerufen. WennP
nichtstatic
ist oderP
schreibgeschützt ist, tritt ein Kompilierungsfehler auf.e.P
Der Accessor der Eigenschaft P
in der Klasse, Struktur oder Schnittstelle, die vom Typ derE
angegeben wird, wird mit dem Instanzausdrucke
aufgerufen. WennP
static
ist oderP
einen Schreibzugriff hat, tritt ein Bindungszeitfehler auf.e.P = value
Der set-Accessor der Eigenschaft P
in der Klasse, Struktur oder Schnittstelle, die durch den Typ vonE
angegeben ist, wird mit dem Instanz-Ausdrucke
und der Argumentliste(value)
aufgerufen. WennP
static
ist oderP
schreibgeschützt ist, tritt ein Bindungszeitfehler auf.Ereigniszugriff E += value
Der add-Accessor des Ereignisses E
in der enthaltenden Klasse oder Struktur wird aufgerufen. WennE
nichtstatic
ist, ist der Instanzausdruckthis
.E -= value
Der remove-Accessor des Ereignisses E
in der enthaltenden Klasse oder Struktur wird aufgerufen. WennE
nichtstatic
ist, ist der Instanzausdruckthis
.T.E += value
Der add-Accessor des Ereignisses E
in der enthaltenden Klasse oder StrukturT
wird aufgerufen. Ein Bindungszeitfehler tritt auf, wennE
nichtstatic
ist.T.E -= value
Der remove-Accessor des Ereignisses E
in der enthaltenden Klasse oder StrukturT
wird aufgerufen. Ein Bindungszeitfehler tritt auf, wennE
nichtstatic
ist.e.E += value
Der add-Accessor des Ereignisses E
in der Klasse, Struktur oder Schnittstelle, die vom Typ derE
angegeben wird, wird mit dem Instanzausdrucke
aufgerufen. Ein Bindungszeitfehler tritt auf, wennE
static
ist.e.E -= value
Der remove-Accessor des Ereignisses E
in der Klasse, Struktur oder Schnittstelle, die vom Typ derE
angegeben wird, wird mit dem Instanzausdrucke
aufgerufen. Ein Bindungszeitfehler tritt auf, wennE
static
ist.Indexerzugriff e[x, y]
Die Überladungsauflösung wird angewendet, um den besten Indexer in der Klasse, Struktur oder Schnittstelle auszuwählen, die vom Typ e
angegeben wird. Der get-Accessor des Indexers wird mit dem Instanzausdrucke
und der Argumentliste(x, y)
aufgerufen. Wenn der Indexer nur Schreibzugriff aufweist, tritt ein Bindungszeitfehler auf.e[x, y] = value
Die Überladungsauflösung wird angewendet, um den besten Indexer in der Klasse, Struktur oder Schnittstelle auszuwählen, die vom Typ e
angegeben wird. Der set-Accessor des Indexers wird mit dem Instanzausdrucke
und der Argumentliste(x, y, value)
aufgerufen. Wenn der Indexer schreibgeschützt ist, tritt ein Bindungszeitfehler auf.Operatoraufruf -x
Die Überladungsauflösung wird angewendet, um den besten unären Indexer in der Klasse oder Struktur auszuwählen, die vom Typ x
angegeben wird. Der ausgewählte Operator wird mit der Argumentliste(x)
aufgerufen.x + y
Die Überladungsauflösung wird angewendet, um den besten binären Operator anhand der von den Typen x
undy
angegebenen Klassen oder Strukturen auszuwählen. Der ausgewählte Operator wird mit der Argumentliste(x, y)
aufgerufen.Instanzkonstruktoraufruf new T(x, y)
Die Überladungsauflösung wird angewendet, um den besten Instanzkonstruktor in der Klasse oder Struktur T
auszuwählen. Der Instanz-Konstruktor wird mit der Argumentliste(x, y)
aufgerufen.Hinweisende
12.6.2 Argumentlisten
12.6.2.1 Allgemein
Jeder Funktionsmember- und Delegataufruf enthält eine Argumentliste, die tatsächliche Werte oder Variablenverweise für die Parameter des Funktionsmembers bereitstellt. Die Syntax für die Angabe der Argumentliste eines Aufrufs eines Funktionsmitglieds hängt von der Kategorie des Funktionsmitglieds ab:
- Beispielsweise werden die Argumente bei Konstruktoren, Methoden, Indexern und Delegaten, wie unten beschrieben, als argument_list angegeben. Bei Indexern enthält die Argumentliste beim Aufrufen des set-Accessors zusätzlich den Ausdruck, der als rechter Operand des Zuweisungsoperators angegeben ist.
Hinweis: Dieses zusätzliche Argument wird nicht zur Auflösung von Überladungen verwendet, sondern während des Aufrufs des set-Accessors. Hinweisende
- Bei Eigenschaften ist die Argumentliste beim Aufrufen des get-Accessors leer und besteht aus dem Ausdruck, der beim Aufrufen des set-Accessors als der rechte Operand des Zuordnungsoperators angegeben ist.
- Bei Ereignissen besteht die Argumentliste aus dem Ausdruck, der als rechte Operand des
+=
- oder-=
-Operators angegeben ist. - Bei benutzerdefinierten Operatoren besteht die Argumentliste aus dem einzelnen Operanden des unären Operators oder den beiden Operanden des binären Operators.
Die Argumente von Eigenschaften (§15.7) und Ereignissen (§15.8) werden immer als Wertparameter übergeben (§15.6.2.2). Die Argumente von benutzerdefinierten Operatoren (§15.10) werden immer als Wertparameter (§15.6.2.2) oder Eingabeparameter (§9.2.8) übergeben. Die Argumente von Indexern (§15.9) werden immer als Wertparameter (§15.6.2.2), Eingabeparameter (§9.2.8) oder Parameter-Arrays (§15.6.2.4) übergeben. Ausgabe- und Referenzparameter werden für diese Kategorien von Funktionsmitgliedern nicht unterstützt.
Die Argumente eines Instanz-Konstruktors, einer Methode, eines Indexers oder eines Delegatenaufrufs werden als argument_list angegeben:
argument_list
: argument (',' argument)*
;
argument
: argument_name? argument_value
;
argument_name
: identifier ':'
;
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' variable_reference
;
Eine argument_list besteht aus einem oder mehreren Argumenten, die durch Kommas getrennt sind. Jedes Argument besteht aus einem optionalen argument_name gefolgt von einem argument_value. Ein Argument mit einem argument_name wird als benanntes Argument bezeichnet, während ein Argument ohne argument_name ein Positionsargument ist.
Der argument_value kann eine der folgenden Formen annehmen:
- Ein Ausdruck, der angibt, dass das Argument als Wertparameter übergeben oder in einen Eingabeparameter umgewandelt und dann wie durch (§12.6.4.2 und in §12.6.2.3 angegeben wird.
- Das Schlüsselwort
in
, gefolgt von einer Variablenreferenz (§9.5), gibt an, dass das Argument als Eingabeparameter übergeben wird (§15.6.2.3.2). Eine Variable muss definitiv zugewiesen werden (§9.4), bevor sie als Eingabeparameter übergeben werden kann. - Das Schlüsselwort
ref
gefolgt von variable_reference (§9.5), das angibt, dass das Argument als Verweisparameter übergeben wird (§15.6.2.3.3). Eine Variable muss definitiv zugewiesen werden (§9.4), bevor sie als Referenzparameter übergeben werden kann. - Das Schlüsselwort
out
, gefolgt von einem Variablenverweis (§9.5), das angibt, dass das Argument als Ausgabeparameter übergeben wird (§15.6.2.3.4). Eine Variable gilt als definitiv zugewiesen (§9.4), nachdem ein Funktionsmemberaufruf erfolgt ist, bei dem die Variable als Ausgabe-Parameter übergeben wird.
Die Form bestimmt den Parameterübergabemodus des Arguments: Wert, Eingabe, Referenz oder Ausgabe. Wie bereits erwähnt, kann jedoch ein Argument mit dem Wertübergabemodus in ein Argument mit Eingabe-Übergabemodus umgewandelt werden.
Das Übergeben eines volatilen Felds (§15.5.4) als Eingabe-, Ausgabe- oder Referenzparameter führt zu einer Warnung, da das Feld durch die aufgerufene Methode möglicherweise nicht als volatile behandelt wird.
12.6.2.2 Entsprechende Parameter
Für jedes Argument in einer Argumentliste muss ein entsprechender Parameter im Funktionsmember oder Delegat vorhanden sein, der aufgerufen wird.
Die im Folgenden verwendete Parameterliste wird wie folgt bestimmt:
- Bei virtuellen Methoden und Indexern, die in Klassen definiert sind, wird die Parameterliste aus der ersten Deklaration oder Überschreibung des Funktionsmembers ausgewählt, die ausgehend vom statischen Typ des Empfängers und beim Durchsuchen der Basisklassen ermittelt wird.
- Für partielle Methoden wird die Parameterliste der definierenden partiellen Methodendeklaration verwendet.
- Für alle anderen Funktionsmitglieder und Delegaten gibt es nur eine einzige Parameterliste, die verwendet wird.
Die Position eines Arguments oder Parameters ist definiert als die Anzahl der Argumente oder Parameter, die ihm in der Argumentliste oder Parameterliste vorausgehen.
Die entsprechenden Parameter für Argumente von Funktionsmitgliedern werden wie folgt festgelegt:
- Argumente im argument_list-Element von Instanzkonstruktoren, Methoden, Indexern und Delegaten:
- Ein Positionsargument, bei dem ein Parameter an der gleichen Position in der Parameterliste vorkommt, entspricht diesem Parameter, es sei denn, der Parameter ist ein Parameterarray und das Funktionsmitglied wird in seiner erweiterten Form aufgerufen.
- Ein Positionsargument eines Funktionsmembers mit einem Parameterarray, das in seiner erweiterten Form aufgerufen wird, die an oder nach der Position des Parameterarrays in der Parameterliste auftritt, entspricht einem Element im Parameterarray.
- Ein benanntes Argument entspricht dem gleichnamigen Parameter in der Parameterliste.
- Bei Indexern entspricht der Ausdruck, der als rechter Operand des Zuweisungsoperators angegeben ist, dem impliziten
value
-Parameter der set-Accessordeklaration beim Aufrufen des set-Accessors.
- Bei Eigenschaften gibt es beim Aufrufen des get-Accessors keine Argumente. Bei Indexern entspricht der Ausdruck, der als rechter Operand des Zuweisungsoperators angegeben ist, dem impliziten Wertparameter der set-Accessordeklaration beim Aufrufen des set-Accessors.
- Bei benutzerdefinierten unären Operatoren (einschließlich Konvertierungen) entspricht der einzelne Operand dem einzelnen Parameter der Operatordeklaration.
- Bei benutzerdefinierten binären Operatoren entspricht der linke Operand dem ersten Parameter und der rechte Operand dem zweiten Parameter der Operatordeklaration.
- Ein unbenanntes Argument entspricht keinem Parameter, wenn es nach einem benannten Argument, das nicht an der richtigen Stelle steht, oder nach einem benannten Argument steht, das einem Parameterarray entspricht.
Hinweis: Dies verhindert, dass
void M(bool a = true, bool b = true, bool c = true);
vonM(c: false, valueB);
aufgerufen wird. Das erste Argument wird an falscher Stelle verwendet (das Argument wird an erster Stelle verwendet, aber der Parameter mit dem Namenc
steht an dritter Stelle), daher sollten die folgenden Argumente benannt werden. Anders ausgedrückt: Nicht nachgestellte benannte Argumente sind nur zulässig, wenn der Name und die Position dazu führen, denselben entsprechenden Parameter zu finden. Hinweisende
12.6.2.3 Laufzeitauswertung von Argumentlisten
Während der Ausführungslaufzeit eines Funktionsmemberaufrufs (§12.6.6) werden die Ausdrücke oder Verweise auf Variablen einer Argumentliste von links nach rechts in folgender Reihenfolge ausgewertet.
Wenn es sich um ein Wertargument handelt, bei dem der Übergabemodus „Wert” ist
Der Argumentausdruck wird ausgewertet und eine implizite Konvertierung (§10.2) in den entsprechenden Parametertyp wird durchgeführt. Der resultierende Wert wird zum initialen Wert des Parameterwerts im Funktionsmemberaufruf.
andernfalls ist der Übergabemodus des Parameters „Eingabe”. Wenn das Argument eine Variablenreferenz ist und es eine Identitätskonversion (§10.2.2) zwischen dem Typ des Arguments und dem Typ des Parameters gibt, wird der resultierende Speicherort der Speicherort, der durch den Parameter im Aufruf des Funktionsmitglieds repräsentiert wird. Andernfalls wird ein Speicherort erstellt, der den gleichen Typ hat wie der entsprechende Parameter. Der Ausdruck des Arguments wird ausgewertet und eine implizite Konversion (§10.2) in den entsprechenden Parametertyp wird durchgeführt. Der resultierende Wert wird in diesem Speicherort gespeichert. Dieser Speicherort wird durch den Eingabeparameter im Aufruf des Funktionsmitglieds repräsentiert.
Beispiel: Bei folgenden Deklarationen und Methodenaufrufen:
static void M1(in int p1) { ... } int i = 10; M1(i); // i is passed as an input argument M1(i + 5); // transformed to a temporary input argument
Im
M1(i)
Methodenaufruf wirdi
selbst als Eingabeargument übergeben, da es als Variable klassifiziert ist und den gleichen Typint
hat wie der Eingabeparameter. In demM1(i + 5)
-Methodenaufruf wird eine unbenannteint
-Variable erstellt, mit dem Wert des Arguments initialisiert und dann als Eingabeargument übergeben. Siehe §12.6.4.2 und §12.6.4.4.Ende des Beispiels
Bei einem Eingabe-, Ausgabe- oder Verweisargument wird der Variablenverweis ausgewertet, und der resultierende Speicherort wird zum Speicherort, der im Parameter des Funktionsmemberaufrufs dargestellt wird. Bei einem Eingabeargument oder einem Referenzargument muss die Variable zum Zeitpunkt des Methodenaufrufs definitiv zugewiesen werden. Wenn der Variablenverweis als Ausgabeargument übergeben wird oder ein Arrayelement eines reference_type ist, erfolgt eine Laufzeitüberprüfung, um sicherzustellen, dass der Typ des Arrayelements mit dem Parametertyp identisch ist. Wenn diese Überprüfung fehlschlägt, wird ein
System.ArrayTypeMismatchException
ausgelöst.
Hinweis: Diese Laufzeitüberprüfung ist aufgrund der Array-Kovarianz erforderlich (§17.6). Hinweisende
Beispiel: Im folgenden Code
class Test { static void F(ref object x) {...} static void Main() { object[] a = new object[10]; object[] b = new string[10]; F(ref a[0]); // Ok F(ref b[1]); // ArrayTypeMismatchException } }
Der zweite Aufruf von
F
bewirkt, dass eineSystem.ArrayTypeMismatchException
ausgelöst wird, da der tatsächliche Elementtyp vonb
string
ist und nichtobject
.Ende des Beispiels
Methoden, Indexer und Instanzkonstruktoren können den ganz rechten Parameter als Parameterarray deklarieren (§15.6.2.4). Diese Funktionsmitglieder werden entweder in normaler Form oder in ihrer erweiterten Form aufgerufen, je nachdem, welche der beiden anwendbar ist (§12.6.4.2):
- Wenn ein Funktionsmember mit einem Parameterarray in der normaler Form aufgerufen wird, muss das für das Parameterarray angegebene Argument ein einzelner Ausdruck sein, der implizit (§10.2) in den Parameterarraytyp konvertierbar ist. In diesem Fall verhält sich das Parameterarray genau wie ein Wertparameter.
- Wenn ein Funktionsmember mit einem Parameterarray in seiner erweiterten Form aufgerufen wird, gibt der Aufruf null oder mehr Positionsargumente für das Parameterarray an, wobei jedes Argument ein Ausdruck ist, der (§10.2) in den Elementtyp des Parameterarrays implizit konvertierbar ist. In diesem Fall erstellt der Aufruf eine Instanz des Typs Parameterarray mit einer Länge, die der Anzahl der Argumente entspricht, initialisiert die Elemente der Array-Instanz mit den angegebenen Argumentwerten und verwendet die neu erstellte Array-Instanz als das eigentliche Argument.
Die Ausdrücke einer Argumentliste werden immer in textueller Reihenfolge ausgewertet.
Beispiel: So sieht es im Beispiel aus
class Test { static void F(int x, int y = -1, int z = -2) => Console.WriteLine($"x = {x}, y = {y}, z = {z}"); static void Main() { int i = 0; F(i++, i++, i++); F(z: i++, x: i++); } }
erzeugt die Ausgabe
x = 0, y = 1, z = 2 x = 4, y = -1, z = 3
Ende des Beispiels
Wenn ein Funktionsmember mit einem Parameterarray in seiner erweiterten Form mit mindestens einem erweiterten Argument aufgerufen wird, wird der Aufruf so verarbeitet, als ob ein Arrayerstellungsausdruck mit einem Arrayinitialisierer (§12.8.17.5) um die erweiterten Argumente eingefügt wurde. Ein leeres Array wird übergeben, wenn keine Argumente für das Parameterarray vorhanden sind. Es ist nicht angegeben, ob der übergebene Verweis auf ein neu zugewiesenes oder vorhandenes leeres Array verweist.
Beispiel: Aufgrund der Deklaration
void F(int x, int y, params object[] args);
stimmen die folgenden Aufrufe der erweiterten Form der Methode
F(10, 20, 30, 40); F(10, 20, 1, "hello", 3.0);
genau überein
F(10, 20, new object[] { 30, 40 }); F(10, 20, new object[] { 1, "hello", 3.0 });
Ende des Beispiels
Wenn Argumente bei einem Funktionsglied mit entsprechenden optionalen Parametern weggelassen werden, werden die Standardargumente der Funktionsdeklaration implizit übergeben. (Dies kann, wie oben beschrieben, die Erstellung eines Speicherorts beinhalten.)
Anmerkung: Da diese Ausdrücke immer konstant sind, hat ihre Auswertung keinen Einfluss auf die Auswertung der übrigen Argumente. Hinweisende
12.6.3 Typableitung
12.6.3.1 Allgemein
Wenn eine generische Methode ohne Angabe von Typargumenten aufgerufen wird, versucht ein Typableitungsprozess, Typargumente für den Aufruf abzuleiten. Das Vorhandensein der Typableitung lässt eine bequemere Syntax für den Aufruf einer generischen Methode zu und bietet dem Programmierer die Möglichkeit, die Angabe redundanter Typinformationen zu vermeiden.
Beispiel:
class Chooser { static Random rand = new Random(); public static T Choose<T>(T first, T second) => rand.Next(2) == 0 ? first : second; } class A { static void M() { int i = Chooser.Choose(5, 213); // Calls Choose<int> string s = Chooser.Choose("apple", "banana"); // Calls Choose<string> } }
Durch Typinferenz werden die Typargumente
int
undstring
aus den Argumenten der Methode abgeleitet.Ende des Beispiels
Typableitung tritt als Teil der Bindungszeitverarbeitung eines Methodenaufrufs (§12.8.10.2) auf und erfolgt vor dem Schritt der Überladungsauflösung des Aufrufs. Wenn eine bestimmte Methodengruppe in einem Methodenaufruf angegeben wird und keine Typargumente als Element des Methodenaufrufs angegeben werden, wird die Typableitung auf jede generische Methode in der Methodengruppe angewendet. Wenn die Typableitung erfolgreich verläuft, werden die abgeleiteten Typargumente verwendet, um die Typen von Argumenten für die nachfolgende Überladungsauflösung zu bestimmen. Wenn die Überladungsauflösung eine generische Methode zum Aufrufen auswählt, werden die abgeleiteten Typargumente als Typargumente für den Aufruf verwendet. Wenn die Typableitung für eine bestimmte Methode fehlschlägt, nimmt diese Methode nicht am Überladungsauflösungsvorgang teil. Der Fehler der Typableitung selbst verursacht keinen Bindungszeitfehler. Er führt jedoch häufig zu einem Bindungszeitfehler, wenn die Überladungsauflösung dann keine anwendbaren Methoden findet.
Wenn jedes übergebene Argument nicht genau einem Parameter in der Methode entspricht (§12.6.2.2) oder es einen nicht-optionalen Parameter ohne entsprechendes Argument gibt, dann schlägt die Ableitung sofort fehl. Andernfalls nehmen Sie an, dass die generische Methode die folgende Signatur hat:
Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)
Bei einem Methodenaufruf der Form M(E₁ ...Eₓ)
besteht die Aufgabe der Typableitung darin, eindeutige Typ-Argumente S₁...Sᵥ
für jeden Typ-Parameter X₁...Xᵥ
zu finden, sodass der Aufruf M<S₁...Sᵥ>(E₁...Eₓ)
gültig wird.
Der Prozess der Typableitung wird im Folgenden als Algorithmus beschrieben. Ein konformer Compiler kann mit einem alternativen Ansatz implementiert werden, sofern er in allen Fällen zum gleichen Ergebnis kommt.
Bei dem Ableitungsvorgang jedes Typparameters ist Xᵢ
entweder festgelegt auf einen bestimmten Typ Sᵢ
oder nicht festgelegt mit einer zugeordneten Gruppe von Begrenzungen. Jede der Begrenzungen ist ein Typ T
. Zu Beginn ist jede Typvariable Xᵢ
nicht festgelegt mit einer leeren Gruppe von Begrenzungen.
Die Typinferenz findet in Phasen statt. In jeder Phase wird versucht, auf der Grundlage der Ergebnisse der vorherigen Phase Typableitungen für weitere Typvariablen durchzuführen. Die erste Phase zieht anfängliche Schlussfolgerungen über Grenzen, während in der zweiten Phase Typvariablen auf bestimmte Typen festgelegt und weitere Grenzen abgeleitet werden. Die zweite Phase muss möglicherweise mehrmals wiederholt werden.
Hinweis: Der Typrückschluss wird auch in anderen Kontexten verwendet, u. a. für die Konvertierung von Methodengruppen (§12.6.3.14) und die Ermittlung des besten gemeinsamen Typs einer Gruppe von Ausdrücken (§12.6.3.15). Hinweisende
12.6.3.2 Die erste Phase
Für jedes der Methodenargumente Eᵢ
:
- Wenn
Eᵢ
eine anonyme Funktion ist, erfolgt eine explizite Parametertypableitung (§12.6.3.8) vonEᵢ
inTᵢ
- Andernfalls, wenn
Eᵢ
einen TypU
hat und der entsprechende Parameter ein Wertparameter (§15.6.2.2) ist, erfolgt eine Ableitung für die Untergrenze (§12.6.3.10) vonU
zuTᵢ
. - Andernfalls, wenn
Eᵢ
einen TypU
hat und der entsprechende Parameter ein Bezugsparameter (§15.6.2.3.3) oder Ausgabeparameter (§15.6.2 .3.4) ist, erfolgt eine genaue Ableitung (§12.6.3.9) vonU
zuTᵢ
. - Andernfalls, wenn
Eᵢ
einen TypU
hat und der entsprechende Parameter ein Eingabeparameter ist (§15.6.2.3.2) undEᵢ
ein Eingabeargument ist, dann erfolgt eine genaue Ableitung (§12.6.3.9) vonU
zuTᵢ
. - Andernfalls, wenn
Eᵢ
einen TypU
hat und der entsprechende Parameter ein Eingabeparameter (§15.6.2.3.2) ist, erfolgt eine Ableitung für die Untergrenze (§12.6.3.10) vonU
zuTᵢ
. - Andernfalls wird für dieses Argument keine Ableitung durchgeführt.
12.6.3.3 Die zweite Phase
Die zweite Phase läuft wie folgt ab:
- Alle unfixed-Typvariablen
Xᵢ
, die nicht von (§12.6.3.6) abhängen,Xₑ
korrigiert (§12.6.3.12). - Wenn keine solchen Typvariablen vorhanden sind, werden alle unfixed-Typvariablen
Xᵢ
korrigiert, für die alle der folgenden Bedingungen gelten:- Es gibt mindestens eine Typvariable
Xₑ
, die vonXᵢ
abhängt. -
Xᵢ
verfügt über einen nicht leeren Satz von Begrenzungen
- Es gibt mindestens eine Typvariable
- Wenn keine solchen Typ-Variablen existieren und es weiterhin unfixed Typvariablen gibt, schlägt die Typableitung fehl.
- Andernfalls, wenn keine weiteren unfixed behobenen-Typvariablen vorhanden sind, ist die Typableitung erfolgreich.
- Andernfalls enthalten alle Argumente
Eᵢ
mit dem entsprechenden ParametertypTᵢ
, in dem die Ausgabetypen (§12.6.3.5) unfixed-TypvariablenXₑ
, aber die Eingabetypen (§12.6.3.4) enthalten, ein Ausgabetyp (§12.6.3.7) wird vonEᵢ
inTᵢ
erstellt. Dann wird die zweite Phase wiederholt.
12.6.3.4 Eingabetypen
Wenn E
eine Methodengruppe oder implizit eingegebene anonyme Funktion ist und T
ein Delegattyp oder ein Ausdrucksstrukturtyp ist, sind alle Parametertypen von T
die Eingabetypen vonE
mit TypT
.
12.6.3.5 Ausgabetypen
Wenn E
eine Methodengruppe oder eine anonyme Funktion ist und T
ein Delegattyp oder Ausdrucksbaumtyp ist, ist der Rückgabetyp von T
ein Ausgabetyp vonE
mit TypT
.
12.6.3.6 Abhängigkeiten
Eine nicht festgelegte Typvariable Xᵢ
hängt direkt ab von einer nicht festgelegten Typvariable, Xₑ
wenn für ein Argument Eᵥ
mit Typ Tᵥ
Xₑ
in einem Eingabetyp von Eᵥ
mit Typ Tᵥ
und Xᵢ
in einem Ausgabetyp von Eᵥ
mit Typ Tᵥ
auftritt.
Xₑ
hängt vonXᵢ
ab, wenn Xₑ
direkt vonXᵢ
abhängt oder wenn Xᵢ
direkt vonXᵥ
abhängt und Xᵥ
vonXₑ
abhängt. „Abhängig von“ ist also die transitive, aber nicht reflexive Schließung von „direkt abhängig von“.
12.6.3.7 Ausgabetypableitungen
Eine Ausgabetypableitung erfolgt von einem Ausdruck E
in einem Typ T auf die folgende Weise:
- Wenn
E
eine anonyme Funktion mit abgeleitetem RückgabetypU
(§12.6.3.13) ist, undT
ein Delegattyp oder Ausdrucksstrukturtyp mit RückgabetypTₓ
ist, dann wird eine Untergrenzenableitung (§12.6.3.10) vonU
zuTₓ
vorgenommen. - Andernfalls, wenn
E
eine Methodengruppe ist undT
ein Delegattyp oder Ausdrucksstrukturtyp mit ParametertypenT₁...Tᵥ
und RückgabetypTₓ
ist, und die Überladungsauflösung vonE
mit den TypenT₁...Tᵥ
eine einzelne Methode mit RückgabetypU
ergibt, dann wird eine Ableitung für die UntergrenzevonU
zuTₓ
vorgenommen. - Andernfalls, wenn
E
ein Ausdruck vom TypU
ist, wird eine Ableitung für die UntergrenzevonU
zuT
vorgenommen. - Andernfalls werden keine Ableitungen vorgenommen.
12.6.3.8 Explizite Typableitungen für Parameter
Eine explizite Parametertypableitung wird aus einem Ausdruck E
in einen Typ T
auf die folgende Weise vorgenommen:
- Wenn
E
eine explizit eingegebene anonyme Funktion mit ParametertypenU₁...Uᵥ
ist undT
ein Delegattyp oder Ausdrucksstrukturtyp mit ParametertypenV₁...Vᵥ
ist, erfolgt für jedeUᵢ
eine genaue Ableitung (§12.6.3.9) vonUᵢ
in entsprechendenVᵢ
.
12.6.3.9 Exakte Ableitungen
Eine genaue Ableitungaus einem Typ U
in einen Typ V
erfolgt wie folgt:
- Wenn
V
einer der nicht korrigiertenXᵢ
ist, wirdU
dem Satz exakter Grenzen fürXᵢ
hinzugefügt. - Andernfalls werden
V₁...Vₑ
undU₁...Uₑ
bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft:V
ist ein ArraytypV₁[...]
undU
ist ein ArraytypU₁[...]
desselben Rangs.-
V
ist der TypV₁?
undU
ist der TypU₁
-
V
ist ein konstruierter TypC<V₁...Vₑ>
undU
ist ein konstruierter TypC<U₁...Uₑ>
Wenn eines dieser Fälle zutrifft, wird eine genaue Ableitung von jedemUᵢ
auf die entsprechendeVᵢ
vorgenommen.
- Andernfalls werden keine Ableitungen vorgenommen.
12.6.3.10 Untergrenzableitungen
Eine Untergrenzableitung von einem Typ U
in einen Typ V
wird wie folgt vorgenommen:
- Wenn
V
einer der nicht korrigiertenXᵢ
ist, wirdU
dem Satz unterer Grenzen fürXᵢ
hinzugefügt. - Andernfalls, wenn
V
der TypV₁?
undU
der TypU₁?
sind, wird eine Untergrenzenableitung vonU₁
inV₁
vorgenommen. - Andernfalls werden
U₁...Uₑ
undV₁...Vₑ
bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft:V
ist ein ArraytypV₁[...]
, undU
ist ein ArraytypU₁[...]
desselben Rangs.-
V
ist einer vonIEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
oderIList<V₁>
undU
ist ein eindimensionales Array vom TypU₁[]
-
V
ist ein konstruierterclass
-,struct
-,interface
- oderdelegate
-TypC<V₁...Vₑ>
, und es gibt einen eindeutigen TypC<U₁...Uₑ>
, sodass für dasU
-Element (oder, wennU
von Typparameter
ist, seine effektive Basisklasse oder ein Element seines effektiven Schnittstellensatzes) Folgendes gilt: Es ist identisch mit, erbt (inherits
) (direkt oder indirekt) von oder implementiert (direkt oder indirekt)C<U₁...Uₑ>
. - (Die Einschränkung der „Eindeutigkeit“ bedeutet, dass im Fall der Schnittstelle
C<T>{} class U: C<X>, C<Y>{}
keine Ableitung vonU
nachC<T>
vorgenommen wird, daU₁
auchX
oderY
sein könnte).
Wenn einer dieser Fälle zutrifft, wird wie folgt ein Rückschluss von jedemUᵢ
-Element in das entsprechendeVᵢ
-Element durchgeführt: - Wenn
Uᵢ
nicht als Referenztyp bekannt ist, wird eine exakte Ableitung vorgenommen. - Andernfalls, wenn
U
ein Arraytyp ist, wird eine Untergrenzenableitung vorgenommen. - Andernfalls, wenn
V
C<V₁...Vₑ>
ist, hängt die Ableitung von demi-th
-Typparameter vonC
ab:- Wenn der Typparameter kovariant ist, erfolgt eine Untergrenzenableitung.
- Wenn er kontravariant ist, wird eine Obergrenzenableitung vorgenommen.
- Wenn er invariant ist, wird eine genaue Ableitung vorgenommen.
- Andernfalls werden keine Ableitungen vorgenommen.
12.6.3.11 Obergrenzenableitung
Eine Obergrenzenableitung von einem Typ U
in einen Typ V
wird wie folgt vorgenommen:
- Wenn
V
einer der unbestimmtenXᵢ
ist, wirdU
der Menge der oberen Begrenzungen fürXᵢ
hinzugefügt. - Andernfalls werden
V₁...Vₑ
undU₁...Uₑ
bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft:U
ist ein ArraytypU₁[...]
, undV
ist ein ArraytypV₁[...]
desselben Rangs.-
U
ist einer vonIEnumerable<Uₑ>
,ICollection<Uₑ>
,IReadOnlyList<Uₑ>
,IReadOnlyCollection<Uₑ>
oderIList<Uₑ>
undV
ist ein eindimensionales Array vom TypVₑ[]
-
U
ist der TypU1?
undV
ist der TypV1?
-
U
ist ein Konstruktor für die Klasse, Struktur, Schnittstelle oder DelegattypC<U₁...Uₑ>
, undV
ist ein Typclass, struct, interface
oderdelegate
, der mitidentical
zu,inherits
von (direkt oder indirekt) verwandt ist oder (direkt oder indirekt) einen eindeutigen TypC<V₁...Vₑ>
implementiert. - (Die Einschränkung „Eindeutigkeit“ bedeutet, dass bei einer Schnittstelle
C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}
kein Rückschluss vonC<U₁>
aufV<Q>
erfolgt. Es werden keine Rückschlüsse vonU₁
aufX<Q>
oderY<Q>
gezogen.)
Wenn einer dieser Fälle zutrifft, wird wie folgt ein Rückschluss von jedemUᵢ
-Element in das entsprechendeVᵢ
-Element durchgeführt: - Wenn
Uᵢ
nicht als Referenztyp bekannt ist, wird eine exakte Ableitung vorgenommen. - Andernfalls, wenn
V
ein Arraytyp ist, wird eine Obergrenzenableitung vorgenommen. - Andernfalls, wenn
U
C<U₁...Uₑ>
ist, hängt die Ableitung von demi-th
-Typparameter vonC
ab:- Wenn er kovariant ist, wird eine Obergrenzenableitung vorgenommen.
- Wenn der Typparameter kontravariant ist, erfolgt eine Untergrenzenableitung.
- Wenn er invariant ist, wird eine genaue Ableitung vorgenommen.
- Andernfalls werden keine Ableitungen vorgenommen.
12.6.3.12 Korrektur
Eine nicht korrigierte Typvariable Xᵢ
mit einer Menge von Begrenzungen wird wie folgt korrigiert:
- Die Menge von Kandidatentypen
Uₑ
beginnt als Menge aller Typen in der Begrenzungsgruppe fürXᵢ
. - Jede Bindung für
Xᵢ
wird wiederum untersucht: Für jede genaue Grenze U vonXᵢ
werden alle TypenUₑ
, die nicht mitU
identisch sind, aus der Kandidatenmenge entfernt. Für jede untere GrenzeU
vonXᵢ
werden alle TypenUₑ
, in die keine implizite Konvertierung ausU
vorhanden ist, aus der Kandidatenmenge entfernt. Für jede obere Grenze U vonXᵢ
werden alle TypenUₑ
, in die keine implizite Konvertierung inU
vorhanden ist, aus der Kandidatenmenge entfernt. - Wenn es unter den verbleibenden Kandidatentypen
Uₑ
einen eindeutigen TypV
gibt, für den es eine implizite Konvertierung von allen anderen Kandidatentypen gibt, wirdXᵢ
aufV
festgelegt. - Andernfalls funktioniert die Typableitung nicht.
12.6.3.13 Abgeleiteter Rückgabetyp
Der abgeleitete Rückgabetyp einer anonymen Funktion F
wird während der Typableitung und Überladungsauflösung verwendet. Der abgeleitete Rückgabetyp kann nur für eine anonyme Funktion bestimmt werden, bei der alle Parametertypen bekannt sind, entweder weil sie explizit angegeben werden, über eine anonyme Funktionskonvertierung bereitgestellt oder während der Typableitung für einen eingeschlossenen generischen Methodenaufruf abgeleitet werden.
Der abgeleitete effektive Rückgabetyp wird wie folgt bestimmt:
- Wenn der Textkörper von
F
ein Ausdruck mit einem Typ ist, dann ist der abgeleitete effektive Rückgabetyp vonF
der Typ dieses Ausdrucks. - Wenn der Textkörper von
F
ein Block ist und der Satz von Ausdrücken in denreturn
-Anweisungen des Blocks einen am häufigsten verwendeten TypT
(§12.6.3.15) aufweist, ist der abgeleitete effektive RückgabetypF
T
. - Andernfalls kann kein effektiver Rückgabetyp für
F
abgeleitet werden.
Der abgeleitete Rückgabetyp wird wie folgt bestimmt:
- Wenn
F
asynchron ist und der Textkörper vonF
entweder ein Ausdruck ist, der als nichts klassifiziert ist (§12.2), oder ein Block, bei dem keinereturn
-Anweisungen Ausdrücke aufweisen, ist der abgeleitete Rückgabetyp«TaskType»
(§15.15.1). - Wenn
F
asynchron ist und einen abgeleiteten effektiven RückgabetypT
hat, ist der abgeleitete Rückgabetyp«TaskType»<T>»
(§15.15.1). - Wenn
F
nicht asynchron ist und einen abgeleiteten effektiven RückgabetypT
hat, ist der abgeleitete RückgabetypT
. - Andernfalls kann ein Rückgabetyp für
F
nicht abgeleitet werden.
Beispiel: Betrachten Sie als Beispiel für die Typableitung anonymer Funktionen die
Select
-Erweiterungsmethode, die in derSystem.Linq.Enumerable
-Klasse deklariert ist:namespace System.Linq { public static class Enumerable { public static IEnumerable<TResult> Select<TSource,TResult>( this IEnumerable<TSource> source, Func<TSource,TResult> selector) { foreach (TSource element in source) { yield return selector(element); } } } }
Angenommen, der
System.Linq
-Namespace wurde mit einerusing namespace
-Anweisung importiert, und es ist eine KlasseCustomer
mit derName
-Eigenschaft vom Typstring
vorhanden, kann dieSelect
-Methode zum Auswählen der Namen einer Kundenliste verwendet werden.List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);
Der Aufruf der Erweiterungsmethode (§12.8.10.3) von
Select
wird verarbeitet, indem der Aufruf in einen statischen Methodenaufruf umgeschrieben wird.IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);
Da die Typargumente nicht explizit angegeben wurden, wird die Typableitung verwendet, um auf die Typargumente zu schließen. Zunächst wird das Kundenargument mit dem Quellparameter in Beziehung gesetzt, wobei von
TSource
aufCustomer
abgeleitet wird. Dann wird mit Hilfe des oben beschriebenen anonymen Funktionstyp-Ableitungsprozessesc
der TypCustomer
gegeben, und der Ausdruckc.Name
wird mit dem Rückgabetyp des Selektorparameters in Beziehung gesetzt, wasTResult
zustring
macht. Daher ist der Aufruf gleichbedeutend mitSequence.Select<Customer,string>(customers, (Customer c) => c.Name)
und das Ergebnis ist vom Typ
IEnumerable<string>
.Das folgende Beispiel zeigt, wie die Typableitung einer anonymen Funktion die Möglichkeit bietet, Flows zwischen den Argumenten eines generischen Methodenaufrufs zu erzeugen. Angesichts der folgenden Methode und des Aufrufs:
class A { static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2) { return f2(f1(value)); } static void M() { double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours); } }
Die Typableitung für den Aufruf läuft wie folgt ab: Zunächst wird das Argument "1:15:30" mit dem Parameter value in Beziehung gesetzt, wobei von
X
aufstring
geschlossen wird. Dann erhält der Parameter der ersten anonymen Funktion,s
, den abgeleiteten Typstring
, und der AusdruckTimeSpan.Parse(s)
wird mit dem Rückgabetyp vonf1
in Beziehung gesetzt, wodurchY
zuSystem.TimeSpan
wird. Schließlich wird dem Parameter der zweiten anonymen Funktion,t
, der abgeleitete TypSystem.TimeSpan
zugewiesen. Der Ausdruckt.TotalHours
steht in Beziehung zum Rückgabetyp vonf2
, wodurchZ
alsdouble
abgeleitet wird. Daher ist das Ergebnis des Aufrufs vom Typdouble
.Ende des Beispiels
12.6.3.14 Typableitung für die Konvertierung von Methodengruppen
Ähnlich wie bei Aufrufen generischer Methoden wird auch Typinferenz angewendet, wenn eine Methodengruppe M
, die eine generische Methode enthält, in einen bestimmten Delegattyp D
konvertiert wird (§10.8). Angenommen, eine Methode
Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)
und die Methodengruppe M
dem Delegattyp D
zugewiesen wird, besteht die Aufgabe der Typableitung darin, Typargumente S₁...Sᵥ
zu finden, sodass der Ausdruck:
M<S₁...Sᵥ>
kompatibel (§20.2) mit D
wird.
Im Gegensatz zum Typableitungsalgorithmus für generische Methodenaufrufe gibt es in diesem Fall nur Argument--Typen, während es keine Argument--Ausdrücke gibt. Insbesondere gibt es keine anonymen Funktionen und daher keine Notwendigkeit für mehrere Phasen der Ableitung.
Stattdessen werden alle Xᵢ
als considered nicht korrigiert betrachtet und es erfolgt eine Untergrenzenableitung von jedem Argumenttyp Uₑ
von D
in den entsprechenden Parametertyp Tₑ
von M
. Wenn für einen der Xᵢ
keine Grenzen gefunden wurden, schlägt die Typableitung fehl. Andernfalls werden alle Xᵢ
korrigiert in die entsprechenden Sᵢ
, die das Ergebnis der Typableitung sind.
12.6.3.15 Den besten gemeinsamen Typ für ein Set von Ausdrücken finden
In manchen Fällen ist es erforderlich, einen gemeinsamen Typ für ein Set von Ausdrücken festzulegen. Insbesondere werden die Elementtypen implizit typisierter Arrays und die Rückgabetypen anonymer Funktionen mit block-Textkörpern auf diese Weise gefunden.
Der beste gemeinsame Typ für ein Set von Ausdrücken E₁...Eᵥ
wird wie folgt festgelegt:
- Es wird eine neue nicht korrigierte Typvariable
X
eingeführt. - Für jeden Ausdruck
Ei
wird eine Ausgabetypableitung (§12.6.3.7) von ihm inX
vorgenommen. X
wird korrigiert (§12.6.3.12), falls möglich, und der resultierende Typ ist der beste gemeinsame Typ.- Andernfalls schlägt die Ableitung fehl.
Hinweis: Intuitiv entspricht diese Ableitung dem Aufrufen einer Methode
void M<X>(X x₁ ... X xᵥ)
mit demEᵢ
als Argumente und dem Ableiten vonX
. Hinweisende
12.6.24 Überladungsauflösung
12.6.4.1 Allgemein
Die Überladungsauflösung ist ein Bindungszeitmechanismus zum Auswählen des besten Funktionsmembers, der aus einer Argumentenliste und einer Reihe von Kandidatenfunktionsmembern ausgewählt werden soll. Die Überladungsauflösung wählt den Funktionsmember aus, der in den folgenden unterschiedlichen Kontexten in C# aufgerufen werden soll:
- Aufruf einer in einer invocation_expression benannten Methode (§12.8.10).
- Aufruf eines in einem object_creation_expression benannten Instanzkonstruktors(§12.8.17.2).
- Aufruf eines Indexer-Accessors über einen element_access (§12.8.12).
- Aufruf eines vordefinierten oder benutzerdefinierten Operators, auf den in einem Ausdruck verwiesen wird (§12.4.4 und §12.4.5).
Jeder dieser Kontexte definiert die Menge von Kandidatenfunktionsmembern und die Liste der Argumente auf eigene eindeutige Weise. Beispielsweise enthält die Kandidatenmenge für einen Methodenaufruf keine Methoden, die als Überschreibung gekennzeichnet sind (§12.5), und Methoden in einer Basisklasse sind keine Kandidaten, wenn eine Methode in einer abgeleiteten Klasse anwendbar ist (§12.8.10.2).
Sobald die Kandidatenfunktionsmitglieder und die Argumentliste identifiziert wurden, ist die Auswahl des besten Funktionsmitglieds in allen Fällen identisch:
- Zunächst wird die Menge der in Frage kommenden Funktionsmitglieder auf die Funktionsmitglieder reduziert, die in Bezug auf die gegebene Argumentliste anwendbar sind (§ 12.6.4.2). Wenn dieses reduzierte Set leer ist, tritt ein Kompilierfehler auf.
- Dann wird das beste Mitglied aus der Menge der in Frage kommenden Funktionsmitglieder festgelegt. Wenn das Set nur ein Funktionsmitglied enthält, dann ist dieses Funktionsmitglied das beste Funktionsmitglied. Andernfalls ist das beste Funktionsmitglied dasjenige Funktionsmitglied, das in Bezug auf die angegebene Argumentliste besser als alle anderen Funktionsmitglieder ist, vorausgesetzt, dass jedes Funktionsmitglied mit allen anderen Funktionsmitgliedern verglichen wird, wobei die Regeln in §12.6.4.3verwendet werden. Wenn es nicht genau ein Funktionsmitglied gibt, das besser ist als alle anderen Funktionsmitglieder, dann ist der Aufruf des Funktionsmitglieds zweideutig und es kommt zu einem Bindungszeitfehler.
Die folgenden Absätze definieren die genauen Bedeutungen der Begriffe anwendbarer Funktionsmember und besserer Funktionsmember.
12.6.4.2 Anwendbarer Funktionsmember
Ein Funktionsmember gilt als anwendbarer Funktionsmember in Bezug auf eine Argumentliste A
, wenn alle folgenden Aussagen zutreffen:
- Jedes Argument in
A
entspricht einem Parameter in der Deklaration des Funktionsmitglieds, wie in §12.6.2.2 beschrieben. Jedem Parameter entspricht höchstens ein Argument, und jeder Parameter, dem kein Argument entspricht, ist ein optionaler Parameter. - Für jedes Argument in
A
ist der Modus der Parameterübergabe des Arguments identisch mit dem Modus der Parameterübergabe des entsprechenden Parameters und- Für einen Wertparameter oder ein Parameterarray besteht eine implizite Konvertierung (§10.2) vom Argumentausdruck in den Typ des entsprechenden Parameters. Oder:
- Bei einem Verweis- oder Ausgabeparameter gibt es eine Identitätsumwandlung zwischen dem Typ des Argumentausdrucks, wenn dieser vorhanden ist, und dem Typ des entsprechenden Parameters, oder
- für einen Eingabeparameter, wenn das entsprechende Argument den Modifizierer
in
aufweist, gibt es eine Identitätskonvertierung zwischen dem Typ des Argumentausdrucks (falls vorhanden) und dem Typ des entsprechenden Parameters oder - Für einen Eingabeparameter, wenn das entsprechende Argument den
in
-Modifizierer auslässt, existiert eine implizite Konvertierung (§10.2) vom Argumentausdruck in den Typ des entsprechenden Parameters.
Für einen Funktionsmember, der ein Parameterarray enthält, gilt, dass falls er nach den oben genannten Regeln anwendbar ist, in seiner Normalform anwendbar ist. Wenn ein Funktionsmitglied, das ein Parameterarray enthält, nicht in seiner normalen Form anwendbar ist, kann das Funktionsmitglied stattdessen in seiner erweiterten Form anwendbar sein:
- Das erweiterte Formular wird erstellt, indem das Parameterarray in der Funktionsmemberdeklaration durch null oder mehr Wertparameter des Elementtyps des Parameterarrays ersetzt wird, sodass die Anzahl der Argumente in der Argumentliste
A
der Gesamtzahl der Parameter entspricht. WennA
weniger Argumente als die Anzahl der festen Parameter in der Funktionsmitglieddeklaration aufweist, kann die erweiterte Form des Funktionsmitglieds nicht erstellt werden und ist somit nicht anwendbar. - Andernfalls ist die erweiterte Form anwendbar, wenn für jedes Argument in
A
eine der folgenden Bedingungen erfüllt ist:- Der Parameterübergabemodus des Arguments ist identisch mit dem Parameterübergabemodus des entsprechenden Parameters und:
- für einen festen Wertparameter oder einen wertparameter, der durch die Erweiterung erstellt wird, ist eine implizite Konvertierung (§10.2) vom Argumentausdruck in den Typ des entsprechenden Parameters vorhanden; oder
- für einen By-Reference-Parameter ist der Typ des Argumentausdrucks mit dem Typ des entsprechenden Parameters identisch.
- der Parameterübergabemodus des Arguments ist Wert und der Parameterübergabemodus des entsprechenden Parameters ist Eingabe und es existiert eine implizite Konversion (§10.2) vom Ausdruck des Arguments zum Typ des entsprechenden Parameters.
- Der Parameterübergabemodus des Arguments ist identisch mit dem Parameterübergabemodus des entsprechenden Parameters und:
Wenn die implizite Konversion vom Eingabeargumenttyp zum Parametertyp eines Eingabeparameters eine dynamische implizite Konversion ist (§10.2.10), sind die Ergebnisse undefiniert.
Beispiel: Bei folgenden Deklarationen und Methodenaufrufen:
public static void M1(int p1) { ... } public static void M1(in int p1) { ... } public static void M2(in int p1) { ... } public static void Test() { int i = 10; uint ui = 34U; M1(in i); // M1(in int) is applicable M1(in ui); // no exact type match, so M1(in int) is not applicable M1(i); // M1(int) and M1(in int) are applicable M1(i + 5); // M1(int) and M1(in int) are applicable M1(100u); // no implicit conversion exists, so M1(int) is not applicable M2(in i); // M2(in int) is applicable M2(i); // M2(in int) is applicable M2(i + 5); // M2(in int) is applicable }
Ende des Beispiels
- Eine statische Methode gilt nur, wenn die Methodengruppe aus einem simple_name oder einem member_access über einen Typ resultiert.
- Eine Instanzmethode ist nur anwendbar, wenn die Methodengruppe aus einem simple_name, einem member_access über eine Variable oder einen Wert oder einem base_access resultiert.
- Wenn die Methodengruppe aus einem simple_nameresultiert, ist eine Instanzmethode nur anwendbar, wenn
this
Zugriff §12.8.14 zulässig ist.
- Wenn die Methodengruppe aus einem simple_nameresultiert, ist eine Instanzmethode nur anwendbar, wenn
- Wenn die Methodengruppe aus einem member_access resultiert, der entweder über eine Instanz oder einen Typ erfolgen kann, wie in §12.8.7.2 beschrieben, sind sowohl Instanz- als auch statische Methoden anwendbar.
- Eine generische Methode, deren Typargumente (explizit angegeben oder abgeleitet) nicht alle ihre Einschränkungen erfüllen, gilt nicht.
- Im Kontext einer Methodengruppenkonvertierung muss eine Identitätskonvertierung (§10.2.2) oder eine implizite Verweiskonvertierung (§10.2.8) vom Rückgabetyp der Methode zum Rückgabetyp des Delegaten vorhanden sein. Andernfalls ist das Kandidatenverfahren nicht anwendbar.
12.6.4.3 Besserer Funktionsmember
Um das bessere Funktionsmitglied zu bestimmen, wird eine reduzierte Argumentliste A
erstellt, die nur die Argumentausdrücke selbst in der Reihenfolge enthält, in der sie in der ursprünglichen Argumentliste erscheinen, und bei der alle out
- oder ref
-Argumente weggelassen werden.
Parameterlisten für die einzelnen Kandidatenfunktionsmember werden wie folgt erstellt:
- Die erweiterte Form wird verwendet, wenn das Funktionsmitglied nur in der erweiterten Form anwendbar war.
- Optionale Parameter ohne entsprechende Argumente werden aus der Parameterliste entfernt.
- Referenz- und Ausgabeparameter werden aus der Parameterliste entfernt.
- Die Parameter werden neu geordnet, sodass sie an der gleichen Position wie das entsprechende Argument in der Argumentliste stehen.
Angesichts einer Argumentliste A
mit einer Reihe von Argumentausdrücken {E₁, E₂, ..., Eᵥ}
und zwei anwendbaren Funktionselementen Mᵥ
und Mₓ
mit Parametertypen {P₁, P₂, ..., Pᵥ}
und {Q₁, Q₂, ..., Qᵥ}
wird Mᵥ
als ein besseres Funktionselement als Mₓ
definiert, wenn
- für jedes Argument ist die implizite Konversion von
Eᵥ
nachQᵥ
nicht besser als die implizite Konversion vonEᵥ
nachPᵥ
, und - für mindestens ein Argument ist die Konversion von
Eᵥ
nachPᵥ
besser als die Konversion vonEᵥ
nachQᵥ
.
Falls die Parametertypsequenzen {P₁, P₂, ..., Pᵥ}
und {Q₁, Q₂, ..., Qᵥ}
gleichwertig sind (d. h., jede Pᵢ
verfügt über eine Identitätskonvertierung in die entsprechende Qᵢ
), werden die folgenden Bindestrichregeln angewendet, um den besseren Funktionsmember zu ermitteln.
- Wenn
Mᵢ
eine nicht-generische Methode ist undMₑ
eine generische Methode, dann istMᵢ
besser alsMₑ
. - Andernfalls, wenn
Mᵢ
in seiner normalen Form anwendbar ist undMₑ
ein Parameterarray hat und nur in seiner erweiterten Form anwendbar ist, dann istMᵢ
besser alsMₑ
. - Andernfalls, wenn beide Methoden params-Arrays haben und nur in ihrer erweiterten Form anwendbar sind, und wenn das params-Array von
Mᵢ
weniger Elemente hat als das params-Array vonMₑ
, dann istMᵢ
besser alsMₑ
. - Andernfalls, wenn
Mᵥ
spezifischere Parametertypen hat alsMₓ
, dann istMᵥ
besser alsMₓ
. Lassen Sie{R1, R2, ..., Rn}
und{S1, S2, ..., Sn}
die nicht instanziierten und nicht erweiterten Parametertypen vonMᵥ
undMₓ
darstellen. Die Parametertypen vonMᵥ
sind spezifischer als die vonMₓ
, wenn für jeden ParameterRx
nicht weniger spezifisch ist alsSx
und für mindestens einen ParameterRx
spezifischer ist alsSx
:- Ein Typ-Parameter ist weniger spezifisch als ein Nicht-Typ-Parameter.
- Rekursiv ist ein konstruierter Typ spezifischer als ein anderer konstruierter Typ (mit der gleichen Anzahl von Typargumenten), wenn mindestens ein Typargument spezifischer ist und kein Typargument weniger spezifisch ist als das entsprechende Typargument des anderen.
- Ein Array-Typ ist spezifischer als ein anderer Array-Typ (mit der gleichen Anzahl von Dimensionen), wenn der Elementtyp des ersten spezifischer ist als der Elementtyp des zweiten.
- Wenn ein einzelnes Element kein Non-Lifted Operator ist und das andere ein Lifted Operator ist, ist der Non-Lifted Operator besser.
- Wenn kein Funktionsmitglied als besser befunden wurde und alle Parameter von
Mᵥ
ein entsprechendes Argument haben, während für mindestens einen optionalen Parameter inMₓ
Standardargumente eingesetzt werden müssen, istMᵥ
besser alsMₓ
. - Wenn für mindestens einen Parameter
Mᵥ
die die bessere Wahl für die Parameterübergabe (§12.6.4.4) als der entsprechende Parameter inMₓ
verwendet und keine der Parameter inMₓ
bessere Wahl für die Parameterübergabe alsMᵥ
verwenden, istMᵥ
besser alsMₓ
. - Andernfalls ist kein Funktionsmitglied besser.
12.6.4.4 Besserer Modus für die Parameterübergabe
Es ist zulässig, dass entsprechende Parameter in zwei überladenen Methoden nur durch den Parameterübergabemodus abweichen, sofern einer der beiden Parameter den Wertübergabemodus aufweist, wie folgt:
public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
Bei int i = 10;
führen gemäß §12.6.4.2 die Aufrufe M1(i)
und M1(i + 5)
dazu, dass beide Überladungen anwendbar sind. In solchen Fällen ist die Methode mit dem Parameterübergabemodus die bessere Wahl für die Parameterübergabe.
Hinweis: Für Eingabe-, Ausgabe- oder Verweisübergabemodi muss keine solche Auswahl existieren, da diese Argumente nur mit genau denselben Parameterübergabemodi übereinstimmen. Hinweisende
12.6.4.5 Bessere Konvertierung eines Ausdrucks
Aufgrund einer impliziten Konvertierung C₁
, die einen Ausdruck E
in einen Typ T₁
umwandelt, und einer impliziten Konvertierung C₂
, die einen Ausdruck E
in einen Typ T₂
umwandelt, ist C₁
eine bessere Konvertierung als C₂
, wenn eine der folgenden Bedingungen zutrifft:
-
E
entspricht genauT₁
undE
entspricht nicht genauT₂
(§12.6.4.6) E
stimmt genau mit den beiden ElementenT₁
undT₂
oder keinem dieser Elemente überein, undT₁
ist ein besseres Konvertierungsziel alsT₂
(§12.6.4.7).E
ist eine Methodengruppe (§12.2),T₁
ist kompatibel (§20.4) mit der einzigen besten Methode aus der Methodengruppe zur KonvertierungC₁
, undT₂
ist nicht kompatibel mit der einzigen besten Methode aus der Methodengruppe zur KonvertierungC₂
12.6.4.6 Genau übereinstimmender Ausdruck
Bei einem Ausdruck E
und einem Typ T
, liegt bei E
eine genaue ÜbereinstimmungT
vor, wenn eine der folgenden Bedingungen erfüllt ist:
-
E
weist einen TypS
auf, und eine Identitätskonvertierung vonS
inT
ist vorhanden. E
ist eine anonyme Funktion,T
ist entweder ein DelegattypD
oder ein AusdrucksbaumtypExpression<D>
und einer der folgenden Faktoren trifft zu:- Es gibt einen abgeleiteten Rückgabetyp
X
fürE
im Kontext der Parameterliste vonD
(§12.6.3.12), und es ist eine Identitätsumwandlung vonX
zu dem Rückgabetyp vonD
vorhanden. E
ist eineasync
-Lambda ohne Rückgabewert undD
hat einen Rückgabetyp, der ein nicht generischer«TaskType»
ist- Entweder ist
E
nicht asynchron undD
hat einen RückgabetypY
, oderE
ist asynchron undD
hat einen Rückgabetyp«TaskType»<Y>
(§15.15.1), und es gilt eine der folgenden Bedingungen:- Der Textkörper von
E
ist ein Ausdruck, der genau mitY
übereinstimmt. - Der Textkörper von
E
ist ein Block, in dem jede Rückgabeanweisung einen Ausdruck zurückgibt, der genau mitY
übereinstimmt.
- Der Textkörper von
- Es gibt einen abgeleiteten Rückgabetyp
12.6.4.7 Besseres Konvertierungsziel
Bei den beiden Typen T₁
und T₂
ist T₁
ein besseres Konvertierungsziel als T₂
, wenn eine der folgenden Bedingungen erfüllt ist:
- Eine implizite Konversion von
T₁
nachT₂
existiert und keine implizite Konversion vonT₂
nachT₁
existiert -
T₁
ist«TaskType»<S₁>
(§15.15.1),T₂
ist«TaskType»<S₂>
, undS₁
ist ein besseres Konvertierungsziel alsS₂
. -
T₁
ist«TaskType»<S₁>
(§15.15.1),T₂
ist«TaskType»<S₂>
, undT₁
ist spezieller alsT₂
-
T₁
istS₁
oderS₁?
, wobeiS₁
ein Integraltypen mit Vorzeichen ist, undT₂
istS₂
oderS₂?
, wobeiS₂
ein Integraltypen ohne Vorzeichen ist. Konkret:S₁
istsbyte
undS₂
istbyte
,ushort
,uint
oderulong
S₁
istshort
undS₂
istushort
,uint
oderulong
S₁
istint
undS₂
istuint
oderulong
S₁
istlong
, undS₂
istulong
.
12.6.4.8 Überladung in generischen Klassen
Anmerkung: Obwohl die deklarierten Signaturen eindeutig sein müssen (§8.6), ist es möglich, dass die Ersetzung von Typargumenten zu identischen Signaturen führt. In einer solchen Situation wählt die Überladungsauflösung die spezifischste (§12.6.4.3) der ursprünglichen Signaturen (vor der Ersetzung von Typargumenten) aus, sofern vorhanden, und andernfalls wird ein Fehler gemeldet. Hinweisende
Beispiel: In den folgenden Beispielen werden Überladungen gezeigt, die gemäß dieser Regel gültig und ungültig sind:
public interface I1<T> { ... } public interface I2<T> { ... } public abstract class G1<U> { public abstract int F1(U u); // Overload resolution for G<int>.F1 public abstract int F1(int i); // will pick non-generic public abstract void F2(I1<U> a); // Valid overload public abstract void F2(I2<U> a); } abstract class G2<U,V> { public abstract void F3(U u, V v); // Valid, but overload resolution for public abstract void F3(V v, U u); // G2<int,int>.F3 will fail public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail public abstract void F5(U u1, I1<V> v2); // Valid overload public abstract void F5(V v1, U u2); public abstract void F6(ref U u); // Valid overload public abstract void F6(out V v); }
Ende des Beispiels
12.6.5 Kompilierungszeitüberprüfung des Aufrufs dynamischer Member
Obwohl die Auflösung von Überladungen eines dynamisch gebundenen Vorgangs zur Laufzeit geschieht, kann es manchmal zur Kompilierzeit möglich sein, die Liste der Funktionsmember zu kennen, aus denen eine Überladung ausgewählt wird.
- Bei einem Delegataufruf (§12.8.10.4) ist die Liste ein einzelner Funktionsmember mit derselben Parameterliste wie der delegate_type des Aufrufs.
- Bei einem Methodenaufruf (§12.8.10.2) bei einem Typ oder einem Wert, dessen statischer Typ nicht dynamisch ist, ist der Satz von zugänglichen Methoden in der Methodengruppe zur Kompilierungszeit bekannt.
- Für einen Objekterstellungsausdruck (§12.8.17.2) ist die Menge zugänglicher Konstruktoren im Typ zur Kompilierungszeit bekannt.
- Für einen Indexerzugriff (§12.8.12.3) ist die Menge der zugänglichen Indexer im Empfänger zur Kompilierungszeit bekannt.
In diesen Fällen wird eine begrenzte Kompilierungszeitprüfung für jeden Member in der bekannten Menge von Funktionsmitgliedern durchgeführt, um festzustellen, ob mit Sicherheit bekannt ist, dass es zur Laufzeit nie aufgerufen wird. Für jedes Funktionselement F
wird eine geänderte Parameter- und Argumentliste erstellt:
- Wenn
F
eine generische Methode ist und Typargumente übergeben wurden, werden diese zunächst durch die Typparameter in der Parameterliste ersetzt. Wenn jedoch keine Typargumente angegeben wurden, findet keine solche Ersetzung statt. - Anschließend wird jeder Parameter, dessen Typ offen ist (d. h. einen Typparameter enthält; siehe §8.4.3), zusammen mit den dazugehörigen Parametern entfernt.
Damit F
die Prüfung bestehen kann, müssen alle folgenden Bedingungen erfüllt sein:
- Die geänderte Parameterliste für
F
gilt für die geänderte Argumentliste in Bezug auf §12.6.4.2. - Alle konstruierten Typen in der geänderten Parameterliste genügen ihren Beschränkungen (§8.4.5).
- Wenn die Typparameter von
F
im obigen Schritt ersetzt wurden, sind ihre Beschränkungen erfüllt. - Wenn
F
eine statische Methode ist, darf die Methodengruppe nicht aus einem member_access resultieren, dessen Empfänger zur Kompilierungszeit als eine Variable oder ein Wert bekannt ist. - Wenn
F
eine Instanzmethode ist, darf die Methodengruppe nicht aus einem member_access resultieren, dessen Empfänger zur Kompilierungszeit als ein Typ erkannt wird.
Wenn kein Kandidat diesen Test besteht, tritt ein Kompilierfehler auf.
12.6.6 Funktionsmemberaufruf
12.6.6.1 Allgemein
In diesem Unterabschnitt wird der Prozess beschrieben, der zur Laufzeit ausgeführt wird, um einen bestimmten Funktionsmember aufzurufen. Es wird davon ausgegangen, dass ein Bindungszeitprozess bereits den zu aufrufenden Member bestimmt hat, möglicherweise durch Anwenden der Überladungsauflösung auf eine Reihe von Kandidatenfunktionsmembern.
Zum Zwecke der Beschreibung des Aufrufprozesses werden Funktionsmitglieder in zwei Kategorien unterteilt.
- Statische Funktionsmember Dies sind statische Methoden, statische Property-Accessoren und benutzerdefinierte Operatoren. Statische Funktionsmember sind immer nicht-virtuell.
- Instanzfunktionsmember Hierbei handelt es sich um Instanzmethoden, Instanzkonstruktoren, Eigenschaftsaccessoren und Indexzugriffsaccessoren. Instanzfunktionsmember sind entweder nicht-virtuell oder virtuell und werden immer auf eine bestimmte Instanz angewendet. Die Instanz wird durch einen Instanzausdruck berechnet und steht innerhalb des Funktionsmembers als
this
(§12.8.14) zur Verfügung. Bei einem Instanzkonstruktor wird der Instanzausdruck als das neu zugewiesene Objekt verwendet.
Die Laufzeitverarbeitung eines Funktionsmembersaufrufs besteht aus den folgenden Schritten, wobei M
der Funktionsmember ist und, wenn M
ein Instanzmember ist, E
der Instanzausdruck ist:
Wenn
M
ein statisches Funktionsmitglied ist:- Die Argumentliste wird wie in §12.6.2 beschrieben ausgewertet.
-
M
wird aufgerufen.
Andernfalls, wenn der Typ von
E
ein WerttypV
ist undM
inV
deklariert oder außer Kraft gesetzt wird:-
E
wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte ausgeführt. Bei einem Instanzkonstruktor besteht diese Auswertung aus dem Zuordnen des Speichers (in der Regel aus einem Ausführungsstapel) für das neue Objekt. In diesem Fall wirdE
als Variable klassifiziert. - Wenn
E
nicht als Variable eingestuft ist oderV
kein readonly-Strukturtyp ist (§16.2.2) undE
einer von:- ein Eingabeparameter (§15.6.2.3.2) oder
- ein
readonly
-Feld (§15.5.3) oder - eine
readonly
Referenzvariable oder Rückgabe (§9.7),
dann wird eine temporäre lokale Variable vom Typ
E
erstellt und der Wert vonE
wird dieser Variablen zugewiesen.E
wird dann als Referenz auf diese temporäre lokale Variable neu klassifiziert. Auf die temporäre Variable kann alsthis
innerhalb vonM
zugegriffen werden, jedoch nicht auf andere Weise. Nur wennE
geschrieben werden kann, kann der Aufrufer die Änderungen beobachten, dieM
anthis
vornimmt.- Die Argumentliste wird wie in §12.6.2 beschrieben ausgewertet.
-
M
wird aufgerufen. Die Variable, auf dieE
verweist, wird zu der Variable, auf diethis
verweist.
-
Andernfalls:
-
E
wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte ausgeführt. - Die Argumentliste wird wie in §12.6.2 beschrieben ausgewertet.
- Wenn der Typ von
E
ein value_type ist, wird eine Boxing-Konvertierung (§10.2.9) durchgeführt, umE
in einen class_type zu konvertieren, undE
wird in den folgenden Schritten als dieser class_type betrachtet. Wenn die value_type ein enum_type ist, ist der class_type andernfallsSystem.Enum;
, ist erSystem.ValueType
. - Der Wert von
E
wird auf seine Gültigkeit geprüft. Wenn der Wert vonE
Null ist, wird einSystem.NullReferenceException
ausgelöst und es werden keine weiteren Schritte ausgeführt. - Die aufzurufende Funktionsmemberimplementierung wird bestimmt:
- Wenn der Bindungszeittyp von
E
eine Schnittstelle ist, ist der aufrufende Funktionsmember die Implementierung vonM
, die vom Laufzeittyp der Instanz bereitgestellt wird, auf die vonE
verwiesen wird. Dieses Funktionsmitglied wird durch Anwendung der Regeln für die Zuordnung von Schnittstellen (§18.6.5) bestimmt, um die Implementierung vonM
zu ermitteln, die durch den Laufzeittyp der vonE
referenzierten Instanz bereitgestellt wird. - Andernfalls, wenn
M
ein virtuelles Funktionsmember ist, ist das aufzurufende Funktionsmember die Implementierung vonM
, die vom Laufzeittyp der durchE
referenzierten Instanz bereitgestellt wird. Dieses Funktionsmitglied wird durch Anwendung der Regeln zur Bestimmung der am weitesten abgeleiteten Implementierung (§15.6.4) vonM
in Bezug auf den Laufzeittyp der Instanz, die vonE
referenziert wird, bestimmt. - Andernfalls ist
M
ein nicht-virtuelles Funktionsmitglied, und das aufzurufende Funktionsmitglied istM
selbst.
- Wenn der Bindungszeittyp von
- Die im obigen Schritt ermittelte Implementierung des Funktionsmembers wird aufgerufen. Das Objekt, auf das von
E
verwiesen wird, wird zu dem Objekt, auf das durch dieses verwiesen wird.
-
Das Ergebnis des Aufrufs eines Instanz-Konstruktors (§12.8.17.2) ist der erstellte Wert. Das Ergebnis des Aufrufs eines anderen Funktionsmebers ist der Wert, der falls vorhanden, aus seinem Textkörper zurückgegeben wird (§13.10.5).
12.6.6.2 Aufrufe für geschachtelte Instanzen
Ein in einem value_type implementierter Funktionsmember kann in den folgenden Situationen über eine Boxinstanz dieses value_type aufgerufen werden:
- Wenn es sich bei dem Funktionsmember um eine Außerkraftsetzung einer vom Typ class_type geerbten Methode handelt und über einen Instanzausdruck dieses class_type aufgerufen wird.
Hinweis: Der class_type ist immer einer von
System.Object
,System.ValueType
oderSystem.Enum
. Hinweisende - Wenn der Funktionsmember eine Implementierung eines Schnittstellenfunktionsmembers ist und über einen Instanzausdruck eines interface_type aufgerufen wird.
- Wenn der Funktionsmember über einen Delegaten aufgerufen wird.
In diesen Situationen wird die geschachtelte Instanz als Variable des value_type betrachtet, und diese Variable wird zur Variablen, auf die innerhalb des Funktionsmemberaufrufs verwiesen wird.
Hinweis: Dies bedeutet insbesondere, dass es möglich ist, den Wert in der Boxinstanz zu ändern, wenn ein Funktionsmember in einer geschachtelten Instanz aufgerufen wird. Hinweisende
12.7 Dekonstruktion
Dekonstruktion ist ein Prozess, bei dem ein Ausdruck in ein Tupel einzelner Ausdrücke umgewandelt wird. Die Dekonstruction wird verwendet, wenn das Ziel einer einfachen Zuordnung ein Tupelausdruck ist, um Werte abzurufen, die jedem der Elemente dieses Tupels zugewiesen werden sollen.
Ein Ausdruck E
wird wie folgt in einen Tupelausdruck mit n
Elementen dekonstruiert:
- Wenn
E
ein Tupelausdruck mitn
-Elementen ist, ist das Ergebnis der Dekonstruktion der AusdruckE
selbst. - Andernfalls, wenn
E
einen Tupeltyp(T1, ..., Tn)
mitn
-Elementen aufweist, wirdE
in eine temporäre Variable__v
ausgewertet, und das Ergebnis der Dekonstruktion ist der Ausdruck(__v.Item1, ..., __v.Itemn)
. - Andernfalls, wenn der Ausdruck
E.Deconstruct(out var __v1, ..., out var __vn)
während der Kompilierungszeit zu einer eindeutigen Instanz oder Erweiterungsmethode aufgelöst wird, wird dieser Ausdruck ausgewertet, und das Ergebnis der Dekonstruktion ist der Ausdruck(__v1, ..., __vn)
. Eine solche Methode wird als Dekonstruktor bezeichnet. - Andernfalls kann
E
nicht dekonstruiert werden.
Hier beziehen sich __v
und __v1, ..., __vn
auf ansonsten unsichtbare und unzugängliche temporäre Variablen.
Anmerkung: Ein Ausdruck des Typs
dynamic
kann nicht dekonstruiert werden. Hinweisende
12.8 Primärausdrücke
12.8.1 Allgemein
Primäre Ausdrücke umfassen die einfachsten Formen von Ausdrücken.
primary_expression
: primary_no_array_creation_expression
| array_creation_expression
;
primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
| tuple_expression
| member_access
| null_conditional_member_access
| invocation_expression
| element_access
| null_conditional_element_access
| this_access
| base_access
| post_increment_expression
| post_decrement_expression
| null_forgiving_expression
| object_creation_expression
| delegate_creation_expression
| anonymous_object_creation_expression
| typeof_expression
| sizeof_expression
| checked_expression
| unchecked_expression
| default_value_expression
| nameof_expression
| anonymous_method_expression
| pointer_member_access // unsafe code support
| pointer_element_access // unsafe code support
| stackalloc_expression
;
Hinweis: Diese Grammatikregeln können nicht mit ANTLR verwendet werden, da sie Teil von gegenseitig linksrekursiven Regeln (
primary_expression
,primary_no_array_creation_expression
,member_access
,invocation_expression
,element_access
,post_increment_expression
,post_decrement_expression
,null_forgiving_expression
,pointer_member_access
undpointer_element_access
) sind, die von ANTLR nicht verarbeitet werden. Standardtechniken können verwendet werden, um die Grammatik zu transformieren und die gegenseitige Linksrekursion zu entfernen. Dies wurde nicht getan, da nicht alle Analysestrategien dies erfordern (z. B. ein LALR-Parser tut dies nicht) und somit die Struktur und Beschreibung verschleiert würden. Hinweisende
pointer_member_access (§23.6.3) und pointer_element_access (§23.6.4) sind nur in unsicherem Code verfügbar (§23).
Primäre Ausdrücke werden zwischen array_creation_expressions und primary_no_array_creation_expressions unterschieden. Eine solche Behandlung von array_creation_expression, anstatt ihn zusammen mit den anderen einfachen Ausdrucksformen aufzulisten, ermöglicht es der Grammatik, potenziell verwirrenden Code, wie zum Beispiel „...” zu vermeiden.
object o = new int[3][1];
der andernfalls interpretiert werden würde als
object o = (new int[3])[1];
12.8.2 Literale
Ein primary_expression, der aus einem Literal (§6.4.5) besteht, wird als Wert klassifiziert.
12.8.3 Interpolierte Zeichenfolgenausdrücke
Ein interpolated_string_expression besteht aus $
, $@
oder @$
, unmittelbar gefolgt von Text innerhalb von "
Zeichen. Innerhalb des zitierten Texts sind null oder mehr Interpolationen durch {
und }
Zeichen getrennt, von denen jeder einen Ausdruck und optionale Formatierungsspezifikationen einschließt.
Interpolierte Zeichenfolgenausdrücke gibt es in zwei Formen: regelmäßig (interpolated_regular_string_expression) und wörtlich (interpolated_verbatim_string_expression). Diese sind lexikalisch ähnlich, unterscheiden sich jedoch semantisch von den beiden Formen der Zeichenfolgenliterale (§6.4.5.6).
interpolated_string_expression
: interpolated_regular_string_expression
| interpolated_verbatim_string_expression
;
// interpolated regular string expressions
interpolated_regular_string_expression
: Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
Interpolated_Regular_String_End
;
regular_interpolation
: expression (',' interpolation_minimum_width)?
Regular_Interpolation_Format?
;
interpolation_minimum_width
: constant_expression
;
Interpolated_Regular_String_Start
: '$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Regular_String_Mid
: Interpolated_Regular_String_Element+
;
Regular_Interpolation_Format
: ':' Interpolated_Regular_String_Element+
;
Interpolated_Regular_String_End
: '"'
;
fragment Interpolated_Regular_String_Element
: Interpolated_Regular_String_Character
| Simple_Escape_Sequence
| Hexadecimal_Escape_Sequence
| Unicode_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Regular_String_Character
// Any character except " (U+0022), \\ (U+005C),
// { (U+007B), } (U+007D), and New_Line_Character.
: ~["\\{}\u000D\u000A\u0085\u2028\u2029]
;
// interpolated verbatim string expressions
interpolated_verbatim_string_expression
: Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
Interpolated_Verbatim_String_End
;
verbatim_interpolation
: expression (',' interpolation_minimum_width)?
Verbatim_Interpolation_Format?
;
Interpolated_Verbatim_String_Start
: '$@"'
| '@$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Verbatim_String_Mid
: Interpolated_Verbatim_String_Element+
;
Verbatim_Interpolation_Format
: ':' Interpolated_Verbatim_String_Element+
;
Interpolated_Verbatim_String_End
: '"'
;
fragment Interpolated_Verbatim_String_Element
: Interpolated_Verbatim_String_Character
| Quote_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Verbatim_String_Character
: ~["{}] // Any character except " (U+0022), { (U+007B) and } (U+007D)
;
// lexical fragments used by both regular and verbatim interpolated strings
fragment Open_Brace_Escape_Sequence
: '{{'
;
fragment Close_Brace_Escape_Sequence
: '}}'
;
Sechs der oben definierten lexikalischen Regeln sind kontextsensitiv.
Regel | Kontextuelle Anforderungen |
---|---|
Interpolated_Regular_String_Mid | Wird nur nach einer Interpolated_Regular_String_Start, zwischen sämtlichen folgenden Interpolationen und vor dem entsprechenden Interpolated_Regular_String_End erkannt. |
Regular_Interpolation_Format | Wird nur innerhalb einer regular_interpolation, und wenn das anfangende Kolon (:) nicht innerhalb einer Klammer (runde Klammern/geschweifte Klammern/eckige Klammern) geschachtelt ist, erkannt. |
Interpolated_Regular_String_End | Wird nur nach einer Interpolated_Regular_String_Start erkannt und nur dann, wenn die dazwischenliegenden Token entweder Interpolated_Regular_String_Mids sind oder Token, die Teil von regular_interpolation sein können, einschließlich der Token für alle in diesen Interpolationen enthaltenen interpolated_regular_string_expression. |
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End | Die Erkennung dieser drei Regeln erfolgt nach denselben Prinzipien wie bei den entsprechenden Regeln oben, wobei jede erwähnte regelmäßige Grammatikregel durch die entsprechende wörtliche Regel ersetzt wird. |
Anmerkung: Die obigen Regeln sind vertraulich, da sich ihre Definitionen mit denen anderer Token in der Sprache überschneiden. Hinweisende
Hinweis: Die oben genannte Grammatik ist aufgrund der kontextabhängigen lexikalischen Regeln nicht für ANTLR geeignet. Wie bei anderen Lexergeneratoren unterstützt ANTLR kontextabhängige lexikalische Regeln, z. B. durch die Verwendung seiner lexikalischen Modi . Dies ist jedoch ein Implementierungsdetail und daher nicht Teil dieser Spezifikation. Hinweisende
Ein interpolated_string_expression wird als Wert klassifiziert. Wenn er sofort in System.IFormattable
oder System.FormattableString
mit einer impliziten interpolierten Zeichenfolgenkonvertierung (§10.2.5) konvertiert wird, hat der Ausdruck für die interpolierte Zeichenfolge diesen Typ. Andernfalls hat er den Typ string
.
Hinweis: Die Unterschiede zwischen den möglichen Typen eines interpolierten Zeichenfolgenausdrucks können aus der Dokumentation für
System.String
(§C.2) undSystem.FormattableString
(§C.3) ermittelt werden. Hinweisende
Die Bedeutung einer Interpolation, sowohl regular_interpolation als auch verbatim_interpolation, besteht darin, den Wert des Ausdrucks als string
entsprechend dem Format zu formatieren, das durch das Regular_Interpolation_Format oder Verbatim_Interpolation_Formatangegeben wird, oder nach einem Standardformat für den Typ des Ausdrucks. Die formatierte Zeichenfolge wird dann von der interpolation_minimum_width( falls vorhanden ) geändert, um die endgültige string
zu erzeugen, die in den interpolated_string_expression interpoliert werden soll.
Anmerkung: Wie das Standardformat für einen Typ bestimmt wird, ist in der Dokumentation für
System.String
(§C.2) undSystem.FormattableString
(§C.3) ausführlich beschrieben. Beschreibungen von Standardformaten, die für Regular_Interpolation_Format und Verbatim_Interpolation_Format identisch sind, finden Sie in der Dokumentation fürSystem.IFormattable
(§C.4) und in anderen Typen in der Standardbibliothek (§C). Hinweisende
In einer interpolation_minimum_width muss der constant_expression über eine implizite Umwandlung in int
verfügen. Lassen Sie die Feldbreite den absoluten Wert dieses konstanten Ausdrucks darstellen und die Ausrichtung das Vorzeichen (positiv oder negativ) des Werts dieses konstanten Ausdrucks sein:
- Wenn der Wert von field width kleiner oder gleich der Länge der formatierten Zeichenfolge ist, wird die formatierte Zeichenfolge nicht verändert.
- Andernfalls wird die formatierte Zeichenfolge mit Leerzeichen aufgefüllt, damit ihre Länge der Feldbreite entspricht:
- Wenn die Ausrichtung positiv ist, wird die formatierte Zeichenfolge durch vorangestellte Leerzeichen rechtsbündig ausgerichtet.
- Andernfalls wird sie durch Hinzufügen von Leerzeichen linksbündig ausgerichtet.
Die allgemeine Bedeutung eines interpolated_string_expression, einschließlich der obigen Formatierung und Auffüllung von Interpolationen, wird durch eine Konvertierung des Ausdrucks in einen Methodenaufruf definiert: Wenn der Typ des Ausdrucks System.IFormattable
oder System.FormattableString
ist, wird die Methode System.Runtime.CompilerServices.FormattableStringFactory.Create
(§C.3) aufgerufen, die einen Wert vom Typ System.FormattableString
zurückgibt; andernfalls muss der Typ string
sein und die Methode string.Format
(§C.2) wird aufgerufen, die einen Wert vom Typ string
zurückgibt.
In beiden Fällen besteht die Argumentliste des Aufrufs aus einem Zeichenfolgenliteral mit Formatspezifikationen für jede Interpolation und einem Argument für jeden Ausdruck, der den Formatspezifikationen entspricht.
Das Zeichenfolgenliteral-Format wird wie folgt erstellt, wobei N
die Anzahl der Interpolationen im interpolated_string_expression ist. Das Zeichenfolgenliteral-Format besteht aus diesen Elementen in der folgenden Reihenfolge:
- Die Zeichen von Interpolated_Regular_String_Start oder Interpolated_Verbatim_String_Start
- Die Zeichen von Interpolated_Regular_String_Mid oder Interpolated_Verbatim_String_Mid, falls vorhanden
- Wenn
N ≥ 1
für jede ZahlI
von0
bisN-1
:- Eine Platzhalterspezifikation:
- Ein linkes Klammerzeichen (
{
) - Die Dezimaldarstellung von
I
- Wenn dann die entsprechende regular_interpolation oder verbatim_interpolation einen interpolation_minimum_width aufweist, folgt ein Komma (
,
) gefolgt von der Dezimaldarstellung des Werts des constant_expression - Die Zeichen des Regular_Interpolation_Format oder des Verbatim_Interpolation_Format, falls vorhanden, der entsprechenden Regular_Interpolation oder Verbatim_Interpolation
- Ein rechtes geschweiftes Klammerzeichen (
}
)
- Ein linkes Klammerzeichen (
- Die Zeichen der Interpolated_Regular_String_Mid oder Interpolated_Verbatim_String_Mid unmittelbar nach der entsprechenden Interpolation, falls vorhanden
- Eine Platzhalterspezifikation:
- Schließlich die Zeichen von Interpolated_Regular_String_End oder Interpolated_Verbatim_String_End.
Die nachfolgenden Argumente sind die Ausdrücke aus den Interpolationen, falls vorhanden, in der Reihenfolge.
Wenn ein interpolated_string_expression mehrere Interpolationen enthält, werden die Ausdrücke in diesen Interpolationen in der Reihenfolge des Textes von links nach rechts ausgewertet.
Beispiel:
In diesem Beispiel werden die folgenden Merkmale der Formatspezifikation verwendet.
- die
X
-Formatspezifikation, die ganze Zahlen als Hexadezimalzahlen in Großbuchstaben formatiert, - das Standardformat für einen
string
Wert ist der Wert selbst, - positive Ausrichtungswerte, die innerhalb der angegebenen Mindestfeldbreite rechtsbündig sind,
- negative Ausrichtungswerte, die innerhalb der angegebenen Mindestfeldbreite linksbündig sind,
- definierte Konstanten für die interpolation_minimum_width und
- dass
{{
und}}
als{
bzw.}
formatiert werden.
Gegeben:
string text = "red";
int number = 14;
const int width = -4;
Führen Sie dann folgende Schritte aus:
Interpolierter Zeichenfolgenausdruck | Entspricht der Bedeutung von string |
Wert |
---|---|---|
$"{text}" |
string.Format("{0}", text) |
"red" |
$"{{text}}" |
string.Format("{{text}}) |
"{text}" |
$"{ text , 4 }" |
string.Format("{0,4}", text) |
" red" |
$"{ text , width }" |
string.Format("{0,-4}", text) |
"red " |
$"{number:X}" |
string.Format("{0:X}", number) |
"E" |
$"{text + '?'} {number % 3}" |
string.Format("{0} {1}", text + '?', number % 3) |
"red? 2" |
$"{text + $"[{number}]"}" |
string.Format("{0}", text + string.Format("[{0}]", number)) |
"red[14]" |
$"{(number==0?"Zero":"Non-zero")}" |
string.Format("{0}", (number==0?"Zero":"Non-zero")) |
"Non-zero" |
Ende des Beispiels
12.8.4 Einfache Namen
Ein simple_name besteht aus einem Bezeichner, der optional von einer Typargumentliste gefolgt wird.
simple_name
: identifier type_argument_list?
;
A einfacher_name ist entweder von der Form I
oder in der Form I<A₁, ..., Aₑ>
, wobei I
ist ein einzelner Bezeichner und I<A₁, ..., Aₑ>
ist eine optionale typ_argument_liste. Wenn keine typ_argument_liste angegeben ist, berücksichtigen e
auf Null gesetzt werden. Die einfacher_name wird wie folgt bewertet und klassifiziert:
- Wenn
e
Null ist und der simple_name innerhalb eines lokalen Variablen-Deklarationsbereichs (§7.3) erscheint, der direkt eine lokale Variable, einen Parameter oder eine Konstante mit dem NamenI
enthält, dann bezieht sich der simple_name auf diese lokale Variable, den Parameter oder die Konstante und wird als Variable oder Wert klassifiziert. - Wenn
e
Null ist und der simple_name in einer generischen Methodendeklaration angezeigt wird, aber außerhalb der Attribute seiner method_declaration, und wenn diese Deklaration einen Typparameter mit dem NamenI
enthält, bezieht sich der simple_name auf diesen Typparameter. - Andernfalls gilt für jeden Instanztyp
T
(§15.3.2), vom Instanztyp der unmittelbar umschließenden Typdeklaration bis zum Instanztyp jeder umschließenden Klasse oder Strukturdeklaration (wenn zutreffend):- Wenn
e
Null ist und die Deklaration vonT
einen Typparameter mit dem NamenI
enthält, dann bezieht sich simple_name auf diesen Typparameter. - Andernfalls, wenn eine Membersuche (§12.5) von
I
inT
mite
-Typargumenten eine Übereinstimmung ergibt:- Wenn
T
der Instanztyp der unmittelbar einschließenden Klasse oder Struktur ist und die Suche eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methodengruppe mit einem zugeordneten Instanzausdruck vonthis
. Wenn eine Liste von Typargumenten angegeben wurde, wird sie beim Aufruf einer generischen Methode verwendet (§12.8.10.2). - Andernfalls, wenn
T
der Instanztyp der unmittelbar umschließenden Klasse oder des Struct-Typs ist, falls die Suche ein Instanzmitglied identifiziert und wenn der Verweis innerhalb des Blocks eines Instanzkonstruktors, einer Instanzmethode oder eines Instanzaccessors (§12.2.1) erfolgt, ist das Ergebnis dasselbe wie ein Memberzugriff (§12.8.7) der Formthis.I
. Dies kann nur geschehen, wenne
Null ist. - Andernfalls entspricht das Ergebnis einem Memberzugriff (§12.8.7) im Format
T.I
oderT.I<A₁, ..., Aₑ>
.
- Wenn
- Wenn
- Andernfalls werden für jeden Namespace
N
ab dem Namespace, in dem simple_name enthalten ist, über jeden umschließenden Namespace (wenn zutreffend) bis zum globalen Namespace die folgenden Schritte ausgewertet, bis eine Entität gefunden wird:- Wenn
e
Null ist undI
der Name eines Namespace inN
ist, dann:- Wenn der Speicherort, an dem der simple_name auftritt, durch eine Namespacedeklaration für
N
umschlossen wird und die Namespacedeklaration eine extern_alias_directive oder using_alias_directive enthält, die den NamenI
einem Namespace oder Typ zuordnet, ist die simple_name mehrdeutig und ein Kompilierungszeitfehler tritt auf. - Andernfalls bezieht sich der simple_name auf den Namespace namens
I
inN
.
- Wenn der Speicherort, an dem der simple_name auftritt, durch eine Namespacedeklaration für
- Andernfalls, wenn
N
einen zugänglichen Typ mit NamenI
unde
-Typparametern enthält, dann:- Wenn
e
Null ist und der Speicherort, an dem simple_name auftritt, durch eine Namespacedeklaration fürN
umschlossen wird und die Namespacedeklaration ein extern_alias_directive- oder using_alias_directive-Element enthält, das den NamenI
einem Namespace oder Typ zuordnet, ist simple_name mehrdeutig, und bei der Kompilierung tritt ein Fehler auf. - Andernfalls verweist namespace_or_type_name auf den mit den angegebenen Typargumenten erstellten Typ.
- Wenn
- Andernfalls, wenn der Ort, an dem der simple_name auftritt, von einer Namespacedeklaration für
N
eingeschlossen wird:- Wenn
e
Null ist und die Namespace-Deklaration eine extern_alias_directive oder using_alias_directive enthält, die den NamenI
einem importierten Namespace oder Typ zuordnet, bezieht sich der simple_name auf diesen Namespace oder diesen Typ. - Andernfalls, wenn die von den using_namespace_directiveder Namespace-Deklaration importierten Namespaces genau einen Typ mit dem Namen
I
unde
-Typparametern enthalten, dann bezieht sich der simple_name auf diesen Typ, der mit den angegebenen Typargumenten erstellt wurde. - Andernfalls, wenn die durch die importierten Namespaces derusing_namespace_directive der Namespacedeklaration mehr als einen Typ mit den Namen
I
und Typparameterne
enthalten, wird der simple_name mehrdeutig und es tritt ein Kompilierfehler auf.
- Wenn
Hinweis: Dieser gesamte Schritt ist genau parallel zu dem entsprechenden Schritt bei der Verarbeitung eines namespace_or_type_name (§7.8). Hinweisende
- Wenn
- Andernfalls, wenn
e
Null ist undI
der Bezeichner_
lautet, ist der simple_name ein einfacher Ausschuss, der eine Form eines Deklarationsausdrucks darstellt (§12.17). - Andernfalls ist simple_name undefiniert und es tritt ein Kompilierfehler auf.
12.8.5 Geklammerte Ausdrücke
Ein parenthesized_expression besteht aus einem Ausdruck, der in Klammern eingeschlossen ist.
parenthesized_expression
: '(' expression ')'
;
Ein parenthesized_expression wird ausgewertet, indem der Ausdruck innerhalb der Klammern ausgewertet wird. Wenn der Ausdruck innerhalb der Klammern einen Namespace oder Typ angibt, tritt ein Kompilierungszeitfehler auf. Andernfalls ist das Ergebnis des parenthesized_expression das Ergebnis der Auswertung des enthaltenen Ausdrucks.
12.8.6 Tupelausdrücke
Ein tuple_expression stellt ein Tupel dar und besteht aus zwei oder mehr kommagetrennten und optional benannten Ausdrücken, die in Klammern eingeschlossen sind. Ein deconstruction_expression ist eine Kurzschreibweise für ein Tupel, das implizit typisierte Deklarationsausdrücke enthält.
tuple_expression
: '(' tuple_element (',' tuple_element)+ ')'
| deconstruction_expression
;
tuple_element
: (identifier ':')? expression
;
deconstruction_expression
: 'var' deconstruction_tuple
;
deconstruction_tuple
: '(' deconstruction_element (',' deconstruction_element)+ ')'
;
deconstruction_element
: deconstruction_tuple
| identifier
;
Ein tuple_expression wird als Tupel klassifiziert.
Ein deconstruction_expressionvar (e1, ..., en)
ist eine Kurzform des tuple_expression(var e1, ..., var en)
und zeigt dasselbe Verhalten. Dies gilt rekursiv für alle geschachtelten deconstruction_tuple im deconstruction_expression. Jeder in einem deconstruction_expression verschachtelte Bezeichner führt somit einen Deklarationsausdruck ein (§12.17). Daher kann ein deconstruction_expression nur auf der linken Seite einer einfachen Zuordnung auftreten.
Ein Tupelausdruck hat einen Typ genau dann, wenn jeder Ausdruck seiner Elemente Ei
einen Typ Ti
hat. Der Typ soll ein Tupeltyp mit der gleichen Arität wie der Tupelausdruck sein, wobei jedes Element durch Folgendes definiert wird:
- Wenn das Tupelelement an der entsprechenden Position einen Namen
Ni
hat, dann muss das Element des TupeltypsTi Ni
sein. - Andernfalls ist
Ei
der FormNi
oderE.Ni
oderE?.Ni
, dann muss das TupeltypelementTi Ni
sein, es sei denn, erfüllt eine der folgenden Bedingungen:- Ein anderes Element des Tupelausdrucks hat den Namen
Ni
oder - Ein weiteres Tupelelement ohne Namen weist einen Tupelelementausdruck der Form
Ni
,E.Ni
oderE?.Ni
auf. Ni
hat die FormItemX
, wobeiX
eine Sequenz von nicht mit0
beginnenden Dezimalziffern ist, die möglicherweise die Position eines Tupelelements darstellen, und wobeiX
die Position des Elements nicht darstellt.
- Ein anderes Element des Tupelausdrucks hat den Namen
- Andernfalls soll das Tupelelement
Ti
sein.
Ein Tupelausdruck wird ausgewertet, indem seine Elementausdrücke der Reihe nach von links nach rechts ausgewertet werden.
Ein Tupelwert kann aus einem Tupelausdruck abgerufen werden, indem er in einen Tupeltyp konvertiert wird (§10.2.13), indem er als Wert neu klassifiziert wird (§12.2.2) oder indem er als Ziel einer Dekonstruktionszuweisung festgelegt wird (§12.21.2).
Beispiel:
(int i, string) t1 = (i: 1, "One"); (long l, string) t2 = (l: 2, null); var t3 = (i: 3, "Three"); // (int i, string) var t4 = (i: 4, null); // Error: no type
In diesem Beispiel sind alle vier Tupelausdrücke gültig. Die ersten beiden,
t1
undt2
, verwenden nicht den Typ des Tupelausdrucks, sondern wenden stattdessen eine implizite Tupelkonvertierung an. Beit2
basiert die implizite Tupelkonvertierung auf den impliziten Konvertierungen von2
zulong
und vonnull
zustring
. Der dritte Tupelausdruck weist einen Typ(int i, string)
auf und kann daher als Wert dieses Typs neu klassifiziert werden. Die Deklaration vont4
ist dagegen ein Fehler: Der Tupelausdruck hat keinen Typ, da das zweite Element keinen Typ aufweist.if ((x, y).Equals((1, 2))) { ... };
Dieses Beispiel zeigt, dass Tupel manchmal zu mehreren Ebenen von Klammern führen können, insbesondere wenn der Tupelausdruck das einzige Argument für einen Methodenaufruf ist.
Ende des Beispiels
12.8.7 Elementzugriff
12.8.7.1 Allgemein
Ein member_access besteht aus einem primary_expression, einem predefined_type oder einem qualified_alias_member, gefolgt von einem „.
”-Token, gefolgt von einem identifier, optional gefolgt von einer type_argument_list.
member_access
: primary_expression '.' identifier type_argument_list?
| predefined_type '.' identifier type_argument_list?
| qualified_alias_member '.' identifier type_argument_list?
;
predefined_type
: 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
| 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
| 'ushort'
;
Die qualified_alias_member-Produktion ist in §14.8 definiert.
Ein member_access hat entweder die Form E.I
oder die Form E.I<A₁, ..., Aₑ>
, wobei gilt, dass E
ein primary_expression, ein predefined_type oder ein qualified_alias_member ist,I
als einzelner Bezeichner auftritt und <A₁, ..., Aₑ>
eine optionale type_argument_list ist. Wenn keine typ_argument_liste angegeben ist, berücksichtigen e
auf Null gesetzt werden.
Ein member_access mit einem primary_expression des Typs dynamic
ist dynamisch gebunden (§12.3.3). In diesem Fall klassifiziert der Compiler den Memberzugriff als Eigenschaftszugriff vom Typ dynamic
. Die folgenden Regeln, um die Bedeutung des member_access zu bestimmen, werden dann zur Laufzeit mithilfe des Laufzeittyps anstelle des Kompilierungszeittyps des primary_expression angewendet. Führt diese Laufzeitklassifizierung zu einer Methodengruppe, so ist der Memberzugriff der primary_expression eines invocation_expression.
Der member_access wird wie folgt ausgewertet und klassifiziert:
- Wenn
e
null ist undE
ein Namespace ist undE
einen geschachtelten Namespace mit dem NamenI
enthält, dann ist das Ergebnis dieser Namespace. - Andernfalls, wenn
E
ein Namespace ist undE
einen zugänglichen Typ mit dem NamenI
und denK
-Typparametern enthält, ist das Ergebnis dieser Typ, der mit den angegebenen Typargumenten erstellt wurde. - Wenn
E
als Typ klassifiziert wird, wennE
kein Typparameter ist und eine Membersuche (§12.5) vonI
inE
mit denK
-Typparametern eine Übereinstimmung ergibt, wirdE.I
ausgewertet und wie folgt klassifiziert:Anmerkung: Wenn das Ergebnis einer solchen Mitgliedersuche eine Methodengruppe ist und
K
gleich Null ist, kann die Methodengruppe Methoden mit Typparametern enthalten. Auf diese Weise können solche Methoden für die Typargumentableitung in Betracht gezogen werden. Hinweisende- Wenn
I
einen Typ identifiziert, dann ist das Ergebnis dieser Typ, erstellt mit gegebenen Typargumenten. - Wenn
I
eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methodengruppe ohne zugeordneten Instanzausdruck. - Wenn
I
eine statische Eigenschaft identifiziert, ist das Ergebnis ein Eigenschaftszugriff ohne dazugehörigen Instanzausdruck. - Wenn
I
ein statisches Feld identifiziert:- Wenn das Feld schreibgeschützt ist und der Verweis außerhalb des statischen Konstruktors der Klasse oder Struktur auftritt, in der das Feld deklariert wird, ist das Ergebnis ein Wert, nämlich der Wert des statischen Felds
I
inE
. - Andernfalls ist das Ergebnis eine Variable, nämlich das statische Feld
I
inE
.
- Wenn das Feld schreibgeschützt ist und der Verweis außerhalb des statischen Konstruktors der Klasse oder Struktur auftritt, in der das Feld deklariert wird, ist das Ergebnis ein Wert, nämlich der Wert des statischen Felds
- Wenn
I
ein statisches Ereignis identifiziert:- Wenn der Verweis innerhalb der Klasse oder Struktur auftritt, in der das Ereignis deklariert wird, und das Ereignis ohne event_accessor_declarations (§15.8.1) deklariert wurde, wird
E.I
genauso verarbeitet, als wäreI
ein statisches Feld. - Andernfalls ist das Ergebnis ein Ereigniszugriff ohne zugeordneten Instanzausdruck.
- Wenn der Verweis innerhalb der Klasse oder Struktur auftritt, in der das Ereignis deklariert wird, und das Ereignis ohne event_accessor_declarations (§15.8.1) deklariert wurde, wird
- Wenn
I
eine Konstante identifiziert, dann ist das Ergebnis ein Wert, nämlich der Wert dieser Konstante. - Wenn
I
ein Aufzählungsmitglied identifiziert, dann ist das Ergebnis ein Wert, nämlich der Wert dieses Aufzählungsmitglieds. - Andernfalls ist
E.I
ein ungültiger Memberverweis, und ein Kompilierungszeitfehler tritt auf.
- Wenn
- Wenn
E
ein Eigenschaftszugriff, ein Indexerzugriff, eine Variable oder ein Wert ist, dessen TypT
ist, und eine Membersuche (§12.5) vonI
inT
mitK
Typargumenten übereinstimmt, dann wirdE.I
ausgewertet und wie folgt klassifiziert:- Ist
E
eine Eigenschaft oder ein Indexer-Zugriff, dann wird zunächst der Wert der Eigenschaft oder des Indexer-Zugriffs abgerufen (§12.2.2) und E wird in einen Wert umklassifiziert. - Wenn
I
eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methodengruppe mit einem zugeordneten Instanzausdruck vonE
. - Wenn
I
eine Instanzeigenschaft identifiziert, dann ist das Ergebnis ein Eigenschaftszugriff mit einem verbundenen Instanzausdruck vonE
und einem verbundenen Typ, der dem Typ der Eigenschaft entspricht. WennT
ein Klassentyp ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Eigenschaft gewählt, die gefunden wird, indem beiT
begonnen und die Basisklassen durchsucht werden. - Wenn
T
ein -Klassentyp ist undI
ein Instanzfeld dieses -Klassentypsidentifiziert:- Wenn der Wert von
E
null
ist, wird einSystem.NullReferenceException
ausgegeben. - Andernfalls, wenn das Feld schreibgeschützt ist und der Verweis außerhalb eines Instanzkonstruktors der Klasse auftritt, in der das Feld deklariert wird, ist das Ergebnis ein Wert, nämlich der Wert des Felds
I
im Objekt, auf das vonE
verwiesen wird. - Andernfalls ist das Ergebnis eine Variable, nämlich das Feld
I
in dem vonE
referenzierten Objekt.
- Wenn der Wert von
- Wenn
T
ein Strukturtyp ist undI
ein Instanzfeld dieses Struct-Typs identifiziert:- Wenn
E
ein Wert ist oder wenn das Feld schreibgeschützt ist und der Verweis außerhalb eines Instanzkonstruktors der Struktur auftritt, in der das Feld deklariert wird, ist das Ergebnis ein Wert, nämlich der Wert des FeldsI
in der Strukturinstanz, die durchE
angegeben wird. - Andernfalls ist das Ergebnis eine Variable, nämlich das Feld
I
in der Strukturinstanz, die vonE
angegeben wird.
- Wenn
- Wenn
I
ein Instanz-Ereignis identifiziert:- Wenn der Verweis innerhalb der Klasse oder Struktur auftritt, in der das Ereignis deklariert wird und das Ereignis ohne event_accessor_declarations (§15.8.1) deklariert wurde und der Verweis nicht als linke Seite
a +=
oder-=
Operator deklariert wurde, wirdE.I
genau so verarbeitet, als wäreI
ein Instanzfeld. - Andernfalls ist das Ergebnis ein Ereigniszugriff mit einem zugeordneten Instanzausdruck von
E
.
- Wenn der Verweis innerhalb der Klasse oder Struktur auftritt, in der das Ereignis deklariert wird und das Ereignis ohne event_accessor_declarations (§15.8.1) deklariert wurde und der Verweis nicht als linke Seite
- Ist
- Andernfalls wird versucht,
E.I
als Erweiterungsmethodenaufruf zu verarbeiten (§12.8.10.3). Wenn dies fehlschlägt, istE.I
ein ungültiger Memberverweis und ein Bindungszeitfehler tritt auf.
12.8.7.2 Identische einfache Namen und Typnamen
In einem Memberzugriff in der Form E.I
, wenn E
ein einzelner Bezeichner ist und wenn die Bedeutung von E
als simple_name (§12.8.4) eine Konstante, ein Feld, eine Eigenschaft, eine lokale Variable oder ein Parameter mit demselben Typ wie die Bedeutung von E
als type_name (§7.8.1) ist, dann sind beide möglichen Bedeutungen von E
zulässig. Die Membersuche von E.I
ist nie zweideutig, da I
in beiden Fällen zwingend ein Mitglied des Typs E
sein muss. Mit anderen Worten, die Regel erlaubt einfach den Zugriff auf die statischen Member und geschachtelten Typen von E
, bei denen andernfalls ein Kompilierungsfehler aufgetreten wäre.
Beispiel:
struct Color { public static readonly Color White = new Color(...); public static readonly Color Black = new Color(...); public Color Complement() => new Color(...); } class A { public «Color» Color; // Field Color of type Color void F() { Color = «Color».Black; // Refers to Color.Black static member Color = Color.Complement(); // Invokes Complement() on Color field } static void G() { «Color» c = «Color».White; // Refers to Color.White static member } }
Nur zu erklärenden Zwecken werden innerhalb der Klasse
A
die Vorkommen des BezeichnersColor
, die auf den TypColor
verweisen, durch«...»
begrenzt, während diejenigen, die auf das FeldColor
verweisen, nicht begrenzt werden.Ende des Beispiels
12.8.8 NULL-bedingter Memberzugriff
Ein null_conditional_member_access ist eine bedingte Version von member_access (§12.8.7) und es handelt sich um einen Bindungszeitfehler, wenn der Ergebnistyp void
ist. Für einen Null-bedingten Ausdruck, bei dem der Ergebnistyp möglicherweise void
ist, siehe (§12.8.11).
Ein null_conditional_member_access besteht aus einem primary_expression gefolgt von den beiden Token „?
“ und „.
“, gefolgt von einem Bezeichner mit einer optionalen type_argument_list, gefolgt von keiner oder mehreren dependent_access, denen ein null_forgiving_operator vorangestellt werden kann.
null_conditional_member_access
: primary_expression '?' '.' identifier type_argument_list?
(null_forgiving_operator? dependent_access)*
;
dependent_access
: '.' identifier type_argument_list? // member access
| '[' argument_list ']' // element access
| '(' argument_list? ')' // invocation
;
null_conditional_projection_initializer
: primary_expression '?' '.' identifier type_argument_list?
;
Ein null_conditional_member_access-Ausdruck E
hat die Form P?.A
. Die Bedeutung von E
wird wie folgt bestimmt:
Wenn der Typ von
P
ein Nullwerttyp ist:Lassen Sie
T
den Typ vonP.Value.A
sein.Wenn
T
ein Typparameter ist, der weder als Referenztyp noch als nicht-nullbarer Werttyp bekannt ist, kommt es zu einem Kompilierungsfehler.Wenn
T
ein nicht-nullbarer Werttyp ist, dann ist der Typ vonE
T?
und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? (T?)null : P.Value.A
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.Andernfalls ist der Typ von
E
T
, und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? (T)null : P.Value.A
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.
Andernfalls:
Lassen Sie
T
den Typ des AusdrucksP.A
sein.Wenn
T
ein Typparameter ist, der weder als Referenztyp noch als nicht-nullbarer Werttyp bekannt ist, kommt es zu einem Kompilierungsfehler.Wenn
T
ein nicht-nullbarer Werttyp ist, dann ist der Typ vonE
T?
und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? (T?)null : P.A
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.Andernfalls ist der Typ von
E
T
, und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? (T)null : P.A
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.
Hinweis: In einem Ausdruck der Form:
P?.A₀?.A₁
Wenn
P
zunull
ausgewertet wird, werden wederA₀
nochA₁
ausgewertet. Dasselbe gilt, wenn ein Ausdruck eine Sequenz der Vorgänge null_conditional_member_access oder null_conditional_element_access§12.8.13 ist.Hinweisende
Ein null_conditional_projection_initializer ist eine Einschränkung von null_conditional_member_access und hat die gleiche Semantik. Es tritt nur als Projektionsinitialisierer in einem anonymen Objekterstellungsausdruck auf (§12.8.17.7).
12.8.9 Nulltolerante Ausdrücke
12.8.9.1 Allgemein
Der Wert, Typ, Klassifizierung (§12.2) und der sichere Kontext (§16.4.12) sind der Wert, Typ, Klassifizierung und sichere Kontext des nulltoleranten Ausdrucks .
null_forgiving_expression
: primary_expression null_forgiving_operator
;
null_forgiving_operator
: '!'
;
Hinweis: Die Operatoren Postfix, nulltolerant und Präfix, logische Negation (§12.9.4), obwohl sie durch dasselbe lexikalische Token (!
) dargestellt werden, unterscheiden sich. Nur Letztere können überschrieben werden (§15.10), die Definition des nulltoleranten Operators ist fest. Hinweisende
Es ist ein Kompilierungsfehler, den nulltoleranten Operator mehr als einmal auf denselben Ausdruck anzuwenden, selbst wenn er in Klammern gesetzt ist.
Beispiel: Die folgenden sind alle ungültig:
var p = q!!; // error: applying null_forgiving_operator more than once var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)
Ende des Beispiels
Der Rest dieser Unterklausel und die folgenden Geschwister-Unterklauseln sind bedingt normativ.
Ein Compiler, der eine statische Nullstatus-Analyse (§8.9.5) durchführt, muss der folgenden Spezifikation entsprechen.
Der nulltolerante Operator ist ein Kompilierzeit-Pseudovorgang, der verwendet wird, um die statische Nullzustandsanalyse des Compilers zu unterstützen. Er hat zwei Verwendungsmöglichkeiten: Außerkraftsetzung der Bestimmung eines Compilers, dass ein Ausdruck möglicherweise nullist; und Außerkraftsetzung der Ausgabe einer vom Compiler ausgegebenen Warnung bezüglich der Nullwerteigenschaft.
Die Anwendung des Null-Vergabe-Operators auf einen Ausdruck, für den die statische Nullstatus-Analyse des Compilers keine Warnungen erzeugt, ist kein Fehler.
12.8.9.2 Überschreiben einer „möglicherweise null”-Bestimmung
Unter bestimmten Umständen kann die statische Nullzstandsanalyse eines Compilers feststellen, dass ein Ausdruck den NULL-Zustand möglicherweise null aufweist, und gibt eine Diagnosewarnung aus, wenn andere Informationen darauf hinweisen, dass der Ausdruck nicht null sein kann. Das Anwenden des nulltoleranten Operators auf einen solchen Ausdruck informiert die statische Nullzustandsanalyse des Compilers darüber, dass sich der Null-Zustand in nicht null befindet; dies verhindert die Diagnosewarnung und kann die laufende Analyse unterstützen.
Beispiel: Betrachten Sie das Folgende:
#nullable enable public static void M() { Person? p = Find("John"); // returns Person? if (IsValid(p)) { Console.WriteLine($"Found {p!.Name}"); // p can't be null } } public static bool IsValid(Person? person) => person != null && person.Name != null;
Wenn
IsValid
true
zurückgibt, kannp
sicher dereferenziert werden, um auf seineName
-Eigenschaft zuzugreifen, und die Warnung „Dereferenzierung eines möglicherweise Nullwerts” kann mithilfe von!
unterdrückt werden.Ende des Beispiels
Beispiel: Der Null-Forgiving-Operator sollte mit Vorsicht verwendet werden, beachten Sie:
#nullable enable int B(int? x) { int y = (int)x!; // quash warning, throw at runtime if x is null return y; }
Hier wird der nulltolerante Operator auf einen Werttyp angewendet und jede Warnung für
x
wird aufgehoben. Wennx
jedochnull
ist, wird zur Laufzeit eine Ausnahme ausgelöst, danull
nicht inint
umgewandelt werden kann.Ende des Beispiels
12.8.9.3 Außerkraftsetzung anderer Nullanalysewarnungen
Neben der Möglichkeit, möglicherweise null-Bestimmungen wie oben außer Kraft zu setzen, kann es auch andere Umstände geben, unter denen die statische Nullzustandsanalyse eines Compilers dahingehend außer Kraft gesetzt werden soll, dass ein Ausdruck eine oder mehrere Warnungen erfordert. Anwenden des nulltoleranten Operators auf einen solchen Ausdruck erfordert, dass ein Compiler keine Warnungen für den Ausdruck ausgibt. Als Reaktion darauf kann ein Compiler beschließen, keine Warnungen auszugeben und kann auch seine weitere Analyse ändern.
Beispiel: Betrachten Sie das Folgende:
#nullable enable public static void Assign(out string? lv, string? rv) { lv = rv; } public string M(string? t) { string s; Assign(out s!, t ?? "«argument was null»"); return s; }
Die Typen der Parameter der Methode
Assign
,lv
&rv
, sindstring?
, wobeilv
ein Ausgabeparameter ist und eine einfache Zuordnung durchführt.Die Methode
M
übergibt die Variables
vom Typstring
als Ausgabewert vonAssign
. Der verwendete Compiler gibt eine Warnung aus, das
keine Nullwertvariable ist. Da das zweite Argument vonAssign
nicht null sein kann, wird der nulltolerante Operator verwendet, um die Warnung zu unterdrücken.Ende des Beispiels
Ende des bedingt normativen Textes.
12.8.10 Aufrufausdrücke
12.8.10.1 Allgemein
Ein invocation_expression wird verwendet, um eine Methode aufzurufen.
invocation_expression
: primary_expression '(' argument_list? ')'
;
Der primary_expression kann ein null_forgiving_expression sein, wenn und nur, wenn er über einen delegate_type verfügt.
Ein invocation_expression ist dynamisch gebunden (§12.3.3), wenn mindestens eine der folgenden Bedingungen erfüllt ist:
- Der primary_expression hat den Kompilierungszeittyp
dynamic
. - Mindestens ein Argument der optionalen argument_list weist den Kompilierungszeittyp
dynamic
auf.
In diesem Fall klassifiziert der Compiler den invocation_expression als einen Wert vom Typ dynamic
. Die folgenden Regeln zur Bestimmung der Bedeutung von invocation_expression werden dann zur Laufzeit angewendet, wobei der Laufzeittyp anstelle des Compile-Time-Typs der Ausdrücke primary_no_array_creation_expression und Argumenten, die den Compile-Time-Typ dynamic
haben. Wenn der primary_expression keinen Kompiliertyp dynamic
hat, unterliegt der Methodenaufruf einer eingeschränkten Kompilierungszeitprüfung, wie in §12.6.5 beschrieben.
Der primary_expression eines invocation_expression muss eine Methodengruppe oder ein Wert eines delegate_type sein. Ist der primary_expression eine Methodengruppe, ist der invocation_expression ein Methodenaufruf (§12.8.10.2). Wenn der primary_expression ein Wert eines delegate_type ist, ist der invocation_expression ein Delegataufruf (§12.8.10.4). Wenn der primary_expression weder eine Methodengruppe noch ein Wert eines delegate_type ist, tritt ein Bindungszeitfehler auf.
Die optionale argument_list (§12.6.2) stellt Werte oder Variablenverweise für die Parameter der Methode bereit.
Das Ergebnis der Auswertung eines invocation_expression wird wie folgt klassifiziert:
- Wenn der invocation_expression eine returns-no-value-Methode (§15.6.1) oder einen returns-no-value-Delegaten aufruft, führt das zu keinem Ergebnis. Ein Ausdruck, der als nichts klassifiziert wird, ist nur im Kontext eines statement_expression zulässig (§13.7) oder als Textkörper eines lambda_expression (§12.19). Andernfalls tritt ein Bindungszeitfehler auf.
- Andernfalls, wenn der invocation_expression eine returns-by-ref-Methode (§15.6.1) oder einen returns-by-ref-Delegaten aufruft, ist das Ergebnis eine Variable mit einem zugeordneten Typ, der dem Rückgabewert der Methode oder des Delegaten entspricht. Wenn es sich bei dem Aufruf um eine Instanzmethode handelt und der Empfänger vom Klassentyp
T
ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Methode ausgewählt, die beginnend mitT
gefunden wird, während die Basisklassen durchsucht werden. - Andernfalls, wenn der invocation_expression eine returns-by-value-Methode (§15.6.1) oder einen returns-by-value-Delegaten aufruft, ist das Ergebnis ein Wert mit einem zugeordneten Typ, der dem Rückgabewert der Methode oder des Delegaten entspricht. Wenn es sich bei dem Aufruf um eine Instanzmethode handelt und der Empfänger vom Klassentyp
T
ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Methode ausgewählt, die beginnend mitT
gefunden wird, während die Basisklassen durchsucht werden.
12.8.10.2 Methodenaufrufe
Bei einem Methodenaufruf muss der primary_expression des invocation_expression eine Methodengruppe sein. Die Methodengruppe identifiziert entweder eine Methode, die aufgerufen werden soll, oder die Menge der überladenen Methoden, aus denen eine spezifische Methode ausgewählt wird. Im letzteren Fall wird die spezifische aufzurufende Methode basierend auf dem Kontext der Typen der Argumente in der argument_list bestimmt.
Die Bindungszeitverarbeitung eines Methodenaufrufs in der Form M(A)
, wobei M
eine Methodengruppe (möglicherweise einschließlich einer type_argument_list) ist und A
eine optionale argument_list ist, erfolgt folgendermaßen:
- Die Sammlung von Kandidatenmethoden für den Methodenaufruf wird erstellt. Für jede Methode
F
, die der MethodengruppeM
zugeordnet ist:- Wenn
F
nicht generisch ist, istF
ein Kandidat, wenn:-
M
keine Typargumentliste hat, und -
F
ist in Bezug aufA
anwendbar (§12.6.4.2).
-
- Wenn
F
generisch ist undM
keine Liste mit Typargumenten hat, istF
ein Kandidat, wenn:- Die Typinferenz (§12.6.3) ist erfolgreich und leitet eine Liste von Typargumenten für den Aufruf ab, und
- Sobald die abgeleiteten Typargumente durch die entsprechenden Methodentypparameter ersetzt werden, entsprechen alle konstruierten Typen in der Parameterliste von
F
ihren Einschränkungen (§8.4.5), und die Parameterliste vonF
ist in Bezug aufA
anwendbar (§12.6.4.2).
- Wenn
F
generisch ist undM
eine Typargumentliste enthält, istF
ein Kandidat, wenn:-
F
hat die gleiche Anzahl von Methodentypparametern wie in der Typargumentliste geliefert wurden, und - Sobald die Typargumente durch die entsprechenden Methodentypparameter ersetzt werden, erfüllen alle konstruierten Typen in der Parameterliste von
F
ihre Einschränkungen (§8.4.5), und die Parameterliste vonF
gilt mit Bezug aufA
(§12.6.4.2).
-
- Wenn
- Die Menge der Kandidatenmethoden wird reduziert, um nur Methoden aus den am meisten abgeleiteten Typen zu enthalten: Für jede Methode
C.F
in der Menge, wobeiC
der Typ ist, in dem die MethodeF
deklariert wird, werden alle Methoden, die in einem Basistyp vonC
deklariert sind, aus der Menge entfernt. Außerdem werden, wennC
ein anderer Klassentyp alsobject
ist, alle Methoden, die in einem Schnittstellentyp deklariert sind, aus dem Set entfernt.Hinweis: Diese letztgenannte Regel hat nur Auswirkungen, wenn die Methodengruppe das Ergebnis einer Mitgliederabfrage eines Typparameters ist, der eine effektive Basisklasse hat, die nicht
object
ist, und eine nicht leere effektive Schnittstellenmenge aufweist. Hinweisende - Wenn der resultierende Satz von Kandidatenmethoden leer ist, wird die Weiterverarbeitung nach den folgenden Schritten abgebrochen, und stattdessen wird versucht, den Aufruf als Erweiterungsmethodenaufruf zu verarbeiten (§12.8.10.3). Wenn dies fehlschlägt, gibt es keine anwendbaren Methoden, und es tritt ein Bindungszeitfehler auf.
- Die beste Methode der Menge von Kandidatenmethoden wird mithilfe der Überladungsauflösungsregeln von §12.6.4 identifiziert. Wenn eine einzelne beste Methode nicht identifiziert werden kann, ist der Methodenaufruf zweideutig und es tritt ein Bindungszeitfehler auf. Bei der Überladungsauflösung werden die Parameter einer generischen Methode berücksichtigt, nachdem die Typargumente (angegeben oder abgeleitet) für die entsprechenden Methodentypparameter ersetzt wurden.
Sobald eine Methode zum Bindungszeitpunkt durch die obigen Schritte ausgewählt und validiert wurde, wird der tatsächliche Laufzeitaufruf gemäß den Regeln des Funktionsmemberaufrufs verarbeitet, wie in §12.6.6 beschrieben.
Anmerkung: Die oben beschriebenen Auflösungsregeln wirken sich intuitiv wie folgt aus: Um die bestimmte Methode zu finden, die durch einen Methodenaufruf aufgerufen wird, beginnen Sie mit dem Typ, der durch den Methodenaufruf angegeben wird, und gehen die Vererbungskette hinauf, bis Sie mindestens eine anwendbare, zugreifbare, nicht überschreibende Methodendeklaration finden. Führen Sie dann die Typableitung und Überladungsauflösung für die Menge anwendbarer, zugänglicher, nicht-überschreibbare Methoden aus, die in diesem Typ deklariert sind, und rufen Sie die somit ausgewählte Methode auf. Wenn keine Methode gefunden wurde, versuchen Sie stattdessen, den Aufruf als Aufruf der Erweiterungsmethode zu verarbeiten. Hinweisende
12.8.10.3 Erweiterungsmethode-Aufrufe
Bei einem Methodenaufruf (§12.6.6.2) einer der Formen
«expr» . «identifier» ( )
«expr» . «identifier» ( «args» )
«expr» . «identifier» < «typeargs» > ( )
«expr» . «identifier» < «typeargs» > ( «args» )
Wenn bei der normalen Verarbeitung des Aufrufs keine Methoden gefunden werden, die anwendbar sind, wird versucht, das Konstrukt als Aufruf der Erweiterungsmethode zu verarbeiten. Wenn „expr“ oder eines der „args“ den Compile-Time-Typ dynamic
hat, werden Erweiterungsmethoden nicht angewendet.
Ziel ist es, den besten type_nameC
zu finden, damit der entsprechende statische Methodenaufruf stattfinden kann:
C . «identifier» ( «expr» )
C . «identifier» ( «expr» , «args» )
C . «identifier» < «typeargs» > ( «expr» )
C . «identifier» < «typeargs» > ( «expr» , «args» )
Eine Erweiterungsmethode Cᵢ.Mₑ
ist berechtigt, wenn:
-
Cᵢ
ist eine nicht generische, nicht geschachtelte Klasse - Der Name von
Mₑ
lautet identifier -
Mₑ
ist zugriffsfrei und anwendbar, wenn es auf die Argumente als statische Methode angewendet wird, wie oben gezeigt. - Eine implizite Identitäts-, Verweis- oder Boxing-Konversion ist von expr zum Typ des ersten Parameters von
Mₑ
vorhanden.
Die Suche für C
wird wie folgt ausgeführt:
- Beginnend mit der nächstgelegenen umschließenden Namespace-Deklaration, fortlaufend mit jeder umschlossenen Namespace-Deklaration und endend mit der einschließenden Kompilationseinheit, werden aufeinanderfolgende Versuche unternommen, eine Kandidatenmenge von Erweiterungsmethoden zu finden.
- Wenn der angegebene Namespace oder die Kompilierungseinheit direkt nicht-generische Typdeklarationen
Cᵢ
mit zulässigen ErweiterungsmethodenMₑ
enthält, dann ist die Menge dieser Erweiterungsmethoden die Kandidatenmenge. - Wenn Namespaces, die mithilfe von Namespacedirektiven im angegebenen Namespace oder Kompilierungseinheit importiert werden, direkt nicht generische Typdeklarationen
Cᵢ
mit berechtigten ErweiterungsmethodenMₑ
enthalten, wird die Menge dieser Erweiterungsmethoden zur Kandidatenmenge.
- Wenn der angegebene Namespace oder die Kompilierungseinheit direkt nicht-generische Typdeklarationen
- Wenn in einer umschließenden Namespace-Deklaration oder Kompilierungseinheit kein Kandidaten-Set gefunden wird, tritt ein Kompilierfehler auf.
- Andernfalls wird die Überladungsauflösung auf die Kandidatenmenge angewendet, wie in §12.6.4 beschrieben. Wenn keine einzelne beste Methode gefunden wird, tritt ein Kompilierfehler auf.
-
C
ist der Typ, innerhalb dessen die beste Methode als Erweiterungsmethode deklariert ist.
Mithilfe von C
als Ziel wird der Methodenaufruf dann als statischer Methodenaufruf verarbeitet (§12.6.6).
Hinweis: Im Gegensatz zu einem Instanzmethodenaufruf wird keine Ausnahme ausgelöst, wenn expr zu einem Nullverweis ausgewertet wird. Stattdessen wird dieser
null
-Wert an die Erweiterungsmethode übergeben, wie es bei einem regulären statischen Methodenaufruf der Fall wäre. Es liegt an der Implementierung der Erweiterungsmethode zu entscheiden, wie auf einen solchen Aufruf reagiert werden soll. Hinweisende
Die vorstehenden Regeln bedeuten, dass Instanzmethoden Vorrang vor Erweiterungsmethoden haben, dass Erweiterungsmethoden, die in inneren Namespace-Deklarationen verfügbar sind, Vorrang vor Erweiterungsmethoden haben, die in äußeren Namespace-Deklarationen verfügbar sind, und dass Erweiterungsmethoden, die direkt in einem Namespace deklariert sind, Vorrang vor Erweiterungsmethoden haben, die in denselben Namespace mit einer Direktive using namespace importiert wurden.
Beispiel:
public static class E { public static void F(this object obj, int i) { } public static void F(this object obj, string s) { } } class A { } class B { public void F(int i) { } } class C { public void F(object obj) { } } class X { static void Test(A a, B b, C c) { a.F(1); // E.F(object, int) a.F("hello"); // E.F(object, string) b.F(1); // B.F(int) b.F("hello"); // E.F(object, string) c.F(1); // C.F(object) c.F("hello"); // C.F(object) } }
Im Beispiel hat die Methode von
B
Vorrang vor der ersten Erweiterungsmethode, und die Methode vonC
hat Vorrang vor beiden Erweiterungsmethoden.public static class C { public static void F(this int i) => Console.WriteLine($"C.F({i})"); public static void G(this int i) => Console.WriteLine($"C.G({i})"); public static void H(this int i) => Console.WriteLine($"C.H({i})"); } namespace N1 { public static class D { public static void F(this int i) => Console.WriteLine($"D.F({i})"); public static void G(this int i) => Console.WriteLine($"D.G({i})"); } } namespace N2 { using N1; public static class E { public static void F(this int i) => Console.WriteLine($"E.F({i})"); } class Test { static void Main(string[] args) { 1.F(); 2.G(); 3.H(); } } }
Die Ausgabe dieses Beispiels lautet:
E.F(1) D.G(2) C.H(3)
D.G
hat Vorrang vorC.G
, undE.F
hat Vorrang vorD.F
undC.F
.Ende des Beispiels
12.8.10.4 Delegataufrufe
Für einen Delegataufruf muss der primary_expression des invocation_expression ein Wert eines delegate_type sein. Unter der Annahme, dass der delegate_type ein Funktionsmember mit derselben Parameterliste wie der delegate_type ist, soll der delegate_type angewendet werden (§12.6.4.2) in Bezug auf die argument_list des invocation_expression.
Die Laufzeitverarbeitung eines Delegataufrufs in der Form D(A)
, wobei D
ein Primärausdruck eines Delegattyps ist und A
eine optionale Argumentliste ist, besteht aus den folgenden Schritten:
-
D
wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte ausgeführt. - Die Argumentliste
A
wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte ausgeführt. - Der Wert von
D
wird auf seine Gültigkeit geprüft. Wenn der Wert vonD
null
ist, wird einSystem.NullReferenceException
ausgegeben, und es werden keine weiteren Schritte ausgeführt. - Ansonsten ist
D
ein Verweis auf eine Delegatinstanz. Funktionsmemberaufrufe (§12.6.6) werden für jede aufrufbare Entität in der Aufrufliste des Delegaten ausgeführt. Bei aufrufbaren Entitäten, die aus einer Instanz und einer Instanzmethode bestehen, ist die Instanz für den Aufruf die Instanz, die in der aufrufbaren Entität enthalten ist.
Siehe §20.6 für Details zu mehrfachen Aufruflisten ohne Parameter.
12.8.11 Null-Ausdruck für bedingten Aufruf
Ein null_conditional_invocation_expression ist syntaktisch entweder ein null_conditional_member_access (§12.8.8) oder null_conditional_element_access (§12.8.13), wobei der letzte dependent_access ein Aufrufausdruck ist (§12.8.10).
Ein null_conditional_invocation_expression tritt im Kontext eines statement_expression (§13.7), anonymous_function_body (§12.19.1) oder method_body (§15.6.1) auf.
Im Gegensatz zu den syntaktisch gleichwertigen null_conditional_member_access oder null_conditional_element_access kann ein null_conditional_invocation_expression als „nichts” klassifiziert werden.
null_conditional_invocation_expression
: null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
| null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
;
Der optionale null_forgiving_operator darf nur einbezogen werden, wenn die null_conditional_member_access oder null_conditional_element_access über einen delegate_type verfügt.
Ein null_conditional_invocation_expression Ausdruck E
hat die Form P?A
; wo A
der Rest des syntaktisch gleichwertigen null_conditional_member_access oder null_conditional_element_access ist, beginnt A
deshalb mit .
oder [
. Lassen Sie PA
die Verkettung von P
und A
kennzeichnen.
Wenn E
als statement_expression auftritt, ist die Bedeutung von E
die gleiche wie die Bedeutung von statement:
if ((object)P != null) PA
mit der Ausnahme, dass P
nur einmal ausgewertet wird.
Wenn E
als anonymous_function_body oder method_body auftritt, hängt die Bedeutung von E
von ihrer Klassifizierung ab:
Wenn
E
als nichts klassifiziert wird, ist seine Bedeutung dieselbe wie die von Block:{ if ((object)P != null) PA; }
mit der Ausnahme, dass
P
nur einmal ausgewertet wird.Andernfalls ist die Bedeutung von
E
die gleiche wie die Bedeutung des Blocks:{ return E; }
und wiederum die Bedeutung dieses Blocks hängt davon ab, ob
E
syntaktisch einem null_conditional_member_access entspricht (§12.8.8) oder null_conditional_element_access (§12.8.13).
12.8.12 Elementzugriff
12.8.12.1 Allgemein
Ein Elementzugriff besteht aus einem primary_no_array_creation_expression, gefolgt von einem"[
"-Token, gefolgt von einer argument_list, gefolgt von einem"]
"-Token. Die argument_list besteht aus einem oder mehreren Argumenten, die durch Kommas getrennt sind.
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
Die argument_list eines element_access darf keine out
- oder ref
-Argumente enthalten.
Ein element_access ist dynamisch gebunden (§12.3.3), wenn mindestens eine der folgenden Bedingungen erfüllt ist:
- Der primary_no_array_creation_expression hat den Compile-Time-Typ
dynamic
. - Mindestens ein Ausdruck der argument_list weist den Kompilierungszeittyp
dynamic
auf, und der primary_no_array_creation_expression weist keinen Arraytyp auf.
In diesem Fall klassifiziert der Compiler den element_access als einen Wert vom Typ dynamic
. Die folgenden Regeln zur Bestimmung der Bedeutung von element_access werden dann zur Laufzeit angewendet, wobei der Laufzeittyp anstelle des Compile-Time-Typs der Ausdrücke primary_no_array_creation_expression und argument_list verwendet wird, die den Compile-Time-Typ dynamic
haben. Wenn der primary_no_array_creation_expression nicht den Compile-Time-Typ dynamic
hat, dann wird der Zugriff auf das Element einer begrenzten Compile-Time-Prüfung unterzogen, wie in §12.6.5 beschrieben.
Wenn der primary_no_array_creation_expression eines element_access ein Wert eines array_type ist, ist der element_access ein Array-Zugriff (§12.8.12.2). Andernfalls ist der primary_no_array_creation_expression eine Variable oder ein Wert eines Class-, Struct- oder Interface-Typs, der ein oder mehrere Indexer-Mitglieder hat. In diesem Fall ist der element_access ein Indexer-Zugriff (§12.8.12.3).
12.8.12.2 Arrayzugriff
Für einen Arrayzugriff muss der primary_no_array_creation_expression des element_access ein Wert eines array_type sein. Außerdem darf die argument_list eines Array-Zugriffs keine benannten Argumente enthalten. Die Anzahl der Ausdrücke in der argument_list muss dem Rang des array_type entsprechen und jeder Ausdruck muss vom Typ int
, uint
, long
oder ulong,
sein oder implizit in einen oder mehrere dieser Typen konvertierbar sein.
Das Ergebnis der Auswertung eines Arrayzugriffs ist eine Variable des Elementtyps des Arrays, nämlich das Arrayelement, das durch die Werte der Ausdrücke in der argument_list ausgewählt wurde.
[...] Die Laufzeitverarbeitung eines Arrayzugriffs der Form P[A]
, wobei P
ein primary_no_array_creation_expression eines array_type ist und A
eine argument_list ist, umfasst die folgenden Schritte:
-
P
wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte ausgeführt. - Die Indexausdrücke der argument_list werden in der Reihenfolge von links nach rechts ausgewertet. Nach der Auswertung jedes Indexausdrucks wird eine implizite Konvertierung (§10.2) zu einem der folgenden Typen durchgeführt:
int
,uint
,long
,ulong
. Der erste Typ in dieser Liste, für den eine implizite Konversion existiert, wird ausgewählt. Wenn der Indexausdruck beispielsweise vom Typshort
ist, wird eine implizite Konversion nachint
durchgeführt, da implizite Konversionen vonshort
nachint
und vonshort
nachlong
möglich sind. Wenn die Auswertung eines Indexausdrucks oder die anschließende implizite Konversion eine Ausnahme verursacht, werden keine weiteren Indexausdrücke ausgewertet und keine weiteren Schritte ausgeführt. - Der Wert von
P
wird auf seine Gültigkeit geprüft. Wenn der Wert vonP
null
ist, wird einSystem.NullReferenceException
ausgegeben, und es werden keine weiteren Schritte ausgeführt. - Der Wert jedes Ausdrucks in der argument_list wird anhand der tatsächlichen Grenzen jeder Dimension der Arrayinstanz überprüft, auf die durch
P
verwiesen wird. Wenn ein oder mehrere Werte außerhalb des Bereichs liegen, wird einSystem.IndexOutOfRangeException
ausgelöst und es werden keine weiteren Schritte ausgeführt. - Die Position des Arrayelements, das von den Indexausdrücken angegeben wird, wird berechnet, und diese Position wird zum Ergebnis des Arrayzugriffs.
12.8.12.3 Zugriff auf den Indexer
Für einen Indexerzugriff muss der primary_no_array_creation_expression des element_access eine Variable oder ein Wert eines Klassen-, Struktur- oder Schnittstellentyps sein, und dieser Typ muss einen oder mehrere Indexer implementieren, die in Bezug auf die argument_list des element_access anwendbar sind.
Die Bindungszeitverarbeitung eines Indizierungszugriffs des Formulars P[A]
, wobei P
ein primary_no_array_creation_expression einer Klasse, Struktur oder eines Schnittstellentyps T
ist und A
eine argument_list ist, besteht aus den folgenden Schritten:
- Die von
T
bereitgestellte Indexergruppe wird konstruiert. Der Satz besteht aus allen inT
deklarierten Indexern oder einem Basistyp vonT
, die keine Deklarationen überschreiben und im aktuellen Kontext zugänglich sind (§7.5). - Die Menge wird auf diejenigen Indexer reduziert, die anwendbar sind und nicht von anderen Indexern ausgeblendet werden. Die folgenden Regeln werden auf jeden Indexer
S.I
im Set angewendet, wobeiS
der Typ ist, in dem der IndexerI
deklariert ist:- Falls
I
nicht aufA
zutrifft (§12.6.4.2), wirdI
aus der Menge entfernt. - Falls
I
in Bezug aufA
(§12.6.4.2) anwendbar ist, werden alle in einem Basistyp vonS
deklarierten Indexer aus der Gruppe entfernt. - Wenn
I
in Bezug aufA
(§12.6.4.2) gilt undS
ein anderer Klassentyp alsobject
ist, werden alle in einer Schnittstelle deklarierten Indexer aus der Menge entfernt.
- Falls
- Wenn das resultierende Set von Indexer-Kandidaten leer ist, dann gibt es keine anwendbaren Indexer und es tritt ein Bindungszeitfehler auf.
- Der beste Indexer aus der Menge von Kandidatenindexern wird mithilfe der Überladungsauflösungsregeln von §12.6.4 identifiziert. Wenn ein einzelner bester Indexer nicht identifiziert werden kann, ist der Zugriff auf den Indexer mehrdeutig und es tritt ein Bindungszeitfehler auf.
- Die Indexausdrücke der argument_list werden in der Reihenfolge von links nach rechts ausgewertet. Das Ergebnis der Verarbeitung des Indexerzugriffs ist ein Ausdruck, der als Indexerzugriff klassifiziert ist. Der Indexerzugriffsausdruck verweist auf den im obigen Schritt ermittelten Indexer und hat einen zugehörigen Instanzausdruck von
P
, eine zugehörige Argumentliste vonA
sowie einen zugehörigen Typ, der dem Typ des Indexers entspricht. WennT
ein Klassentyp ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung des Indexers gewählt, der gefunden wird, indem beiT
begonnen und die Basisklassen durchsucht werden.
Abhängig vom Kontext, in dem er verwendet wird, führt ein Zugriff auf einen Indexer zu einem Aufruf der get- oder set-Methode des Indexers. Wenn der Indexerzugriff das Ziel einer Zuweisung ist, wird der set-Accessor aufgerufen, um einen neuen Wert zuzuweisen (§12.21.2). In allen anderen Fällen wird der get-Accessor aufgerufen, um den aktuellen Wert zu erhalten (§12.2.2).
12.8.13 Nullbedingter Elementzugriff
Ein null_conditional_element_access besteht aus einem primary_no_array_creation_expression gefolgt von den beiden Token ?
und [
, gefolgt von einer argument_list, gefolgt von einem ]
-Token, gefolgt von Null oder mehreren dependent_access, denen ein null_forgiving_operator vorangestellt werden kann.
null_conditional_element_access
: primary_no_array_creation_expression '?' '[' argument_list ']'
(null_forgiving_operator? dependent_access)*
;
Ein v ist eine bedingte Version von element_access (§12.8.12) und es handelt sich um einen Bindungszeitfehler, wenn der Ergebnistyp void
ist. Für einen Null-bedingten Ausdruck, bei dem der Ergebnistyp möglicherweise void
ist, siehe (§12.8.11).
Ein null_conditional_element_access-Ausdruck E
hat die Form P?[A]B
; wobei B
mehrere dependent_access sind, falls vorhanden. Die Bedeutung von E
wird wie folgt bestimmt:
Wenn der Typ von
P
ein Nullwerttyp ist:Legen Sie
T
als Typ des AusdrucksP.Value[A]B
fest.Wenn
T
ein Typparameter ist, der weder als Referenztyp noch als nicht-nullbarer Werttyp bekannt ist, kommt es zu einem Kompilierungsfehler.Wenn
T
ein nicht-nullbarer Werttyp ist, dann ist der Typ vonE
T?
und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? (T?)null : P.Value[A]B
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.Andernfalls ist der Typ von
E
T
, und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? null : P.Value[A]B
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.
Andernfalls:
Lassen Sie
T
den Typ des AusdrucksP[A]B
sein.Wenn
T
ein Typparameter ist, der weder als Referenztyp noch als nicht-nullbarer Werttyp bekannt ist, kommt es zu einem Kompilierungsfehler.Wenn
T
ein nicht-nullbarer Werttyp ist, dann ist der Typ vonE
T?
und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? (T?)null : P[A]B
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.Andernfalls ist der Typ von
E
T
, und die Bedeutung vonE
ist die gleiche wie die Bedeutung von:((object)P == null) ? null : P[A]B
Mit der Ausnahme, dass
P
nur einmal ausgewertet wird.
Hinweis: In einem Ausdruck der Form:
P?[A₀]?[A₁]
Wenn
P
zunull
ausgewertet wird, werden wederA₀
nochA₁
ausgewertet. Dasselbe gilt, wenn ein Ausdruck eine Sequenz der Vorgänge null_conditional_element_access oder null_conditional_member_access§12.8.8 ist.Hinweisende
12.8.14 This-Zugriff
Ein this_access besteht aus dem Schlüsselwort this
.
this_access
: 'this'
;
Ein this_access ist nur im Block eines Instanzkonstruktors, einer Instanzmethode, einem Instanz-Accessor (§12.2.1) oder einem Finalizer zulässig. Er hat eine der folgenden Bedeutungen:
- Wenn
this
in einem primary_expression innerhalb eines Instanzkonstruktors einer Klasse verwendet wird, wird er als Wert klassifiziert. Der Typ des Wertes ist der Instanztyp (§15.3.2) der Klasse, in der die Verwendung stattfindet, und der Wert ist ein Verweis auf das zu konstruierende Objekt. - Wenn
this
in einem primary_expression innerhalb einer Instanzmethode oder einem Instanz-Accessor einer Klasse verwendet wird, wird er als Wert klassifiziert. Der Typ des Wertes ist der Instanztyp (§15.3.2) der Klasse, innerhalb derer die Verwendung stattfindet, und der Wert ist eine Referenz auf das Objekt, für das die Methode oder der Accessor aufgerufen wurde. - Wenn
this
in einem primary_expression innerhalb eines Instanzkonstruktors einer Struktur verwendet wird, wird er als Variable klassifiziert. Der Typ der Variablen ist der Instanztyp (§15.3.2) der Struct, innerhalb derer die Verwendung stattfindet, und die Variable repräsentiert die Struct, die konstruiert wird.- Wenn die Konstruktor-Deklaration keinen Konstruktor-Initialisierer hat, verhält sich die
this
-Variable genau so wie ein Ausgabeparameter vom Typ struct. Das bedeutet insbesondere, dass die Variable in jedem Ausführungspfad des Instanz-Konstruktors definitiv zugewiesen werden muss. - Andernfalls verhält sich die
this
-Variable genau wie einref
-Parameter des Struct-Typs. Dies bedeutet insbesondere, dass die Variable anfänglich zugewiesen ist.
- Wenn die Konstruktor-Deklaration keinen Konstruktor-Initialisierer hat, verhält sich die
- Wenn
this
in einem primary_expression innerhalb einer Instanzmethode oder einem Instanz-Accessor einer Struktur verwendet wird, wird er als Variable klassifiziert. Der Typ der Variablen ist der Instanztyp (§15.3.2) des Structs, innerhalb dessen die Verwendung erfolgt.- Wenn es sich bei der Methode oder dem Accessor nicht um einen Iterator (§15.14) oder eine asynchrone Funktion (§15.15) handelt, steht die
this
-Variable für die Struct, für die die Methode oder der Accessor aufgerufen wurde.- Wenn es sich bei der Struktur um eine
readonly struct
handelt, verhält sich diethis
-Variable genau wie ein Eingabeparameter des Struct-Typs. - Andernfalls verhält sich die
this
-Variable genau wie einref
-Parameter des Struct-Typs.
- Wenn es sich bei der Struktur um eine
- Wenn die Methode oder der Zugriff ein Iterator oder eine asynchrone Funktion ist, repräsentiert die
this
-Variable eine Kopie der Struct, für die die Methode oder der Zugriff aufgerufen wurde, und verhält sich genau wie ein Wert-Parameter vom Typ Struct.
- Wenn es sich bei der Methode oder dem Accessor nicht um einen Iterator (§15.14) oder eine asynchrone Funktion (§15.15) handelt, steht die
Die Verwendung von this
in einem primary_expression in einem anderen Kontext als den oben aufgeführten ist ein Kompilierungsfehler. Insbesondere ist es nicht möglich, auf this
in einer statischen Methode, einem statischen Eigenschaftsaccessor oder in einem variable_initializer einer Felddeklaration zu verweisen.
12.8.15 Basiszugriff
Ein base_access besteht aus dem Schlüsselwort base
, gefolgt entweder von einem „.
“-Token und einem Bezeichner und optional type_argument_list oder einer argument_list, die in eckigen Klammern eingeschlossen sind:
base_access
: 'base' '.' identifier type_argument_list?
| 'base' '[' argument_list ']'
;
Ein base_access wird verwendet, um auf Member der Basisklasse zuzugreifen, die von ähnlich benannten Membern in der aktuellen Klasse oder Struktur versteckt werden. Ein base_access ist nur im Textkörper eines Instanzkonstruktors, einer Instanzmethode, einem Instanz-Accessor (§12.2.1) oder einem (§12.2.1) oder einem Finalizer zulässig. Wenn base.I
in einer Klasse oder Struktur auftritt, muss I
ein Mitglied der Basisklasse dieser Klasse oder Struktur kennzeichnen. Wenn base[E]
in einer Klasse vorkommt, muss ein entsprechender Indexer in der Basisklasse vorhanden sein.
Zur Bindungszeit werden base_access-Ausdrücke der Form base.I
und base[E]
genau so ausgewertet, als ob sie ((B)this).I
und ((B)this)[E]
geschrieben wurden, wobei B
die Basisklasse der Klasse oder Struktur ist, in der das Konstrukt auftritt. Daher entsprechen base.I
und base[E]
this.I
und this[E]
, mit der Ausnahme, dass this
als Instanz der Basisklasse betrachtet wird.
Wenn ein base_access auf einen virtuellen Funktionsmember (eine Methode, Eigenschaft oder einen Indexer) verweist, ändert sich die Bestimmung, welches Funktionselement zur Laufzeit aufgerufen wird (§12.6.6). Der aufgerufene Funktionsmember wird bestimmt, indem die am meisten abgeleitete Implementierung (§15.6.4) des Funktionsmembers im Hinblick auf B
ermittelt wird (anstelle des Laufzeittyps von this
, wie in einem nicht basisbasierten Zugriff üblich). Daher kann innerhalb einer Außerkraftsetzung eines virtuellen Funktionsmembers ein base_access verwendet werden, um die geerbte Implementierung des Funktionsmembers aufzurufen. Wenn das Funktionsmitglied, auf das ein base_access verweist, abstrakt ist, tritt ein Bindungszeitfehler auf.
Hinweis: Im Gegensatz zu
this
istbase
kein eigenständiger Ausdruck. Es handelt sich um ein Schlüsselwort, das nur im Kontext eines base_access oder eines constructor_initializer verwendet wird (§15.11.2). Hinweisende
12.8.16 Inkrementierungs- und Dekrementierungsoperatoren in Postfixnotation
post_increment_expression
: primary_expression '++'
;
post_decrement_expression
: primary_expression '--'
;
Der Operand eines Postfix-Inkrement- oder Dekrementvorgangs muss ein Ausdruck sein, der als Variable, Eigenschaftszugriff oder Indexerzugriff klassifiziert ist. Das Ergebnis der Operation ist ein Wert vom gleichen Typ wie der Operand.
Wenn der primary_expression den Kompilierungszeittyp dynamic
hat, dann ist der Operator dynamisch gebunden (§12.3.3). Der post_increment_expression oder der post_decrement_expression hat ebenfalls den Kompilierungszeittyp dynamic
, und die folgenden Regeln werden zur Laufzeit mithilfe des Laufzeittyps des primary_expression angewendet.
Wenn der Operand eines Postfix-Inkrement- oder Dekrementvorgangs eine Eigenschaft oder ein Indexerzugriff ist, muss die Eigenschaft oder der Indexer sowohl über einen get- als auch einen set-Accessor verfügen. Wenn dies nicht der Fall ist, tritt ein Bindungszeitfehler auf.
Die unäre Operatorüberladungsauflösung (§12.4.4) wird angewendet, um eine bestimmte Operatorimplementierung auszuwählen. Es sind vordefinierte ++
und --
-Operatoren für die folgenden Typen vorhanden: sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
und alle Enumerationstypen. Die vordefinierten ++
-Operatoren geben den Wert zurück, der durch das Hinzufügen von 1
zum Operanden entsteht, und die vordefinierten --
-Operatoren geben den Wert zurück, der durch das Subtrahieren von 1
vom Operanden entsteht. In einem geprüften Kontext, wenn sich das Ergebnis dieser Addition oder Subtraktion außerhalb des Bereichs des Ergebnistyps befindet und der Ergebnistyp ein integraler Typ oder ein Enumerationstyp ist, wird ein System.OverflowException
ausgegeben.
Es muss eine implizite Konvertierung vom Rückgabetyp des ausgewählten unären Operators in den Typ des primary_expression geben, andernfalls tritt ein Kompilierungszeitfehler auf.
Die Laufzeitverarbeitung eines Postfix-Inkrement- oder Dekrementvorgangs des Formulars x++
oder x--
besteht aus den folgenden Schritten:
- Wenn
x
als Variable klassifiziert wird:-
x
wird ausgewertet, um die Variable zu erzeugen. - Der Wert von
x
wird gespeichert. - Der gespeicherte Wert von
x
wird in den Operandentyp des ausgewählten Operators umgewandelt und der Operator wird mit diesem Wert als Argument aufgerufen. - Der vom Operator zurückgegebene Wert wird in den Typ von
x
konvertiert und an der durch die vorherige Auswertung vonx
angegebenen Stelle gespeichert. - Der gespeicherte Wert von
x
wird das Ergebnis der Operation.
-
- Wenn
x
als Zugriff auf eine Eigenschaft oder einen Indexer klassifiziert wird:- Der Instanzausdruck (wenn
x
nichtstatic
ist) und die Argumentliste (wennx
ein Indexerzugriff ist), die mitx
verknüpft sind, werden ausgewertet, und die Ergebnisse werden bei den nachfolgenden get- und set-Accessor-Aufrufen verwendet. - Der Get-Accessor von
x
wird aufgerufen und der zurückgegebene Wert wird gespeichert. - Der gespeicherte Wert von
x
wird in den Operandentyp des ausgewählten Operators umgewandelt und der Operator wird mit diesem Wert als Argument aufgerufen. - Der vom Operator zurückgegebene Wert wird in den Typ
x
konvertiert, und der set-Accessor vonx
wird mit diesem Wert als Wertargument aufgerufen. - Der gespeicherte Wert von
x
wird das Ergebnis der Operation.
- Der Instanzausdruck (wenn
Die Operatoren ++
und --
unterstützen auch Präfixnotation (§12.9.6). Das Ergebnis von x++
oder x--
ist der Wert von x
vor der Operation, während das Ergebnis von ++x
oder --x
dagegen der Wert von x
nach der Operation ist. In beiden Fällen hat x
selbst nach der Operation den gleichen Wert.
Eine Implementierung des Operators ++
oder --
kann entweder in der Postfix- oder Präfix-Notation aufgerufen werden. Es ist nicht möglich, separate Implementierungen der Operatoren für die beiden Notationen zu haben.
12.8.17 Der neue Operator
12.8.17.1 Allgemein
Der new
-Operator wird verwendet, um neue Instanzen von Typen zu erstellen.
Es gibt drei Formen von neuen Ausdrücken:
- Ausdrücke zur Objekterstellung und anonyme Ausdrücke zur Objekterstellung werden verwendet, um neue Instanzen von Klassentypen und Wertetypen zu erstellen.
- Ausdrücke zur Erstellung von Arrays werden verwendet, um neue Instanzen von Arraytypen zu erzeugen.
- Delegaterstellungsausdrücke werden verwendet, um Instanzen von Delegattypen abzurufen.
Der new
-Operator impliziert die Erstellung einer Instanz eines Typs, bedeutet jedoch nicht unbedingt eine Speicherzuweisung. Insbesondere Instanzen von Werttypen benötigen keinen zusätzlichen Speicher über die Variablen hinaus, in denen sie sich befinden, und es finden keine Zuweisungen statt, wenn new
verwendet wird, um Instanzen von Werttypen zu erstellen.
Hinweis: Delegaterstellungsausdrücke erstellen nicht immer neue Instanzen. Wenn der Ausdruck auf die gleiche Weise verarbeitet wird wie die Konversion einer Methodengruppe (§10.8) oder einer anonymen Funktion (§10.7), kann dies dazu führen, dass eine vorhandene Delegaten-Instanz wiederverwendet wird. Hinweisende
12.8.17.2 Objekterstellungsausdrücke
Mit einem object_creation_expression wird eine neue Instanz eines class_type oder eines value_type erstellt.
object_creation_expression
: 'new' type '(' argument_list? ')' object_or_collection_initializer?
| 'new' type object_or_collection_initializer
;
object_or_collection_initializer
: object_initializer
| collection_initializer
;
Der Typ eines object_creation_expression muss ein class_type, ein value_type oder ein type_parameter sein. Der Typ kann kein tuple_type oder ein abstrakter oder statischer class_type sein.
Die optionale argument_list (§12.6.2) ist nur zulässig, wenn der Typ ein class_type oder ein struct_type ist.
Ein Objekterstellungsausdruck kann die Konstruktorargumentliste und die umschließenden Klammern weglassen, sofern er einen Objektinitialisierungs- oder Auflistungsinitialisierer enthält. Wenn Sie die Argumentliste des Konstruktors weglassen und in Klammern setzen, entspricht dies der Angabe einer leeren Argumentliste.
Die Verarbeitung eines Objekterstellungsausdrucks, der einen Objekt- oder Auflistungsinitialisierer enthält, besteht darin, zuerst den Instanzkonstruktor zu verarbeiten und dann die vom Objektinitialisierer angegebenen Member- oder Elementinitialisierungen (§12.8.17.3) oder Auflistungsinitialisierungen (§12.8.17.4) zu verarbeiten.
Wenn eines der Argumente in der optionalen argument_list den Typ dynamic
bei der Kompilierung hat, wird der object_creation_expressio dynamisch gebunden (§12.3.3) und die folgenden Regeln werden zur Laufzeit unter Verwendung des Laufzeittyps derjenigen Argumente der argument_list ausgeführt, die den Typ dynamic
bei der Kompilierung haben. Die Objekterstellung wird jedoch einer begrenzten Kompilierungszeitprüfung unterzogen, wie in §12.6.5 beschrieben.
Die Verarbeitung eines object_creation_expression der Form new T(A)
, wobei T
ein class_type oder ein value_type und A
eine optionale argument_list ist, zur Bindungszeit besteht aus den folgenden Schritten:
- Wenn
T
ein value_type ist undA
nicht vorhanden ist:- Der object_creation_expression ist ein Standardkonstruktoraufruf. Das Ergebnis der object_creation_expression ist ein Wert vom Typ
T
, nämlich der Standardwert fürT
, wie in §8.3.3definiert.
- Der object_creation_expression ist ein Standardkonstruktoraufruf. Das Ergebnis der object_creation_expression ist ein Wert vom Typ
- Andernfalls, wenn
T
ein type_parameter ist undA
fehlt:- Wenn für
T
keine Werttyp-Beschränkung oder Konstruktor-Beschränkung (§15.2.5) angegeben wurde, tritt ein Bindungszeitfehler auf. - Das Ergebnis des object_creation_expression ist ein Wert des Laufzeittyps, an den der Typparameter gebunden wurde, nämlich das Ergebnis des Aufrufs des Standardkonstruktors dieses Typs. Der Laufzeittyp kann ein Referenztyp oder ein Wertetyp sein.
- Wenn für
- Ansonsten, wenn
T
ein class_type oder ein struct_type ist:- Wenn
T
ein abstrakter oder statischer class_type ist, tritt ein Kompilierfehler auf. - Der aufzurufende Instanzkonstruktor wird mithilfe der Überladungsauflösungsregeln von §12.6.4 bestimmt. Der Satz der Kandidatenkonstruktoren für Instanzen besteht aus allen in
T
deklarierten zugänglichen Instanzenkonstruktoren, die fürA
anwendbar sind (§12.6.4.2). Wenn das Set der in Frage kommenden Instanz-Konstruktoren leer ist oder wenn ein einzelner bester Instanz-Konstruktor nicht ermittelt werden kann, tritt ein Bindungszeitfehler auf. - Das Ergebnis der object_creation_expression ist ein Wert vom Typ
T
, nämlich der Wert, der durch Aufrufen des Instanzkonstruktors erzeugt wird, der im vorherigen Schritt ermittelt wurde. - Andernfalls ist die object_creation_expression ungültig, und es tritt ein Bindungszeitfehler auf.
- Wenn
Selbst wenn der object_creation_expression dynamisch gebunden ist, ist der Typ bei der Kompilierung immer noch T
.
Die Ausführungszeitverarbeitung eines object_creation_expression mit der neuen Form T(A)
, wobei T
ein class_type oder ein struct_type ist und A
eine optionale argument_list ist, besteht aus den folgenden Schritten:
- Wenn
T
ein class_type ist:- Eine neue Instanz der Klasse
T
wird erstellt. Wenn nicht genügend Speicher für die Zuweisung der neuen Instanz zur Verfügung steht, wird einSystem.OutOfMemoryException
ausgelöst und es werden keine weiteren Schritte ausgeführt. - Alle Felder der neuen Instanz werden auf ihre Standardwerte initialisiert (§9.3).
- Der Instanzkonstruktor wird gemäß den Regeln des Funktionsmemberaufrufs aufgerufen (§12.6.6). Ein Verweis auf die neu zugewiesene Instanz wird automatisch an den Instanzkonstruktor übergeben, und auf die Instanz kann innerhalb dieses Konstruktors als „this” zugegriffen werden.
- Eine neue Instanz der Klasse
- Wenn
T
ein struct_type ist:- Eine Instanz vom Typ
T
wird durch Zuweisung einer temporären lokalen Variablen erstellt. Da ein Instanz-Konstruktor eines struct_type jedem Feld der zu erstellenden Instanz definitiv einen Wert zuweisen muss, ist keine Initialisierung der temporären Variablen erforderlich. - Der Instanzkonstruktor wird gemäß den Regeln des Funktionsmemberaufrufs aufgerufen (§12.6.6). Ein Verweis auf die neu zugewiesene Instanz wird automatisch an den Instanzkonstruktor übergeben, und auf die Instanz kann innerhalb dieses Konstruktors als „this” zugegriffen werden.
- Eine Instanz vom Typ
12.8.17.3 Objekt-Initialisierer
Ein Objektinitialisierer legt Werte für null oder mehr Felder, Eigenschaften oder indizierte Elemente eines Objekts fest.
object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: initializer_target '=' initializer_value
;
initializer_target
: identifier
| '[' argument_list ']'
;
initializer_value
: expression
| object_or_collection_initializer
;
Ein Objektinitialisierer besteht aus einer Sequenz von Mitgliedsinitialisierern, die von den Tokens {
und }
eingeschlossen und durch Kommas getrennt werden. Jeder member_initializer bestimmt ein Ziel für die Initialisierung. Ein Bezeichner muss ein zugängliches Feld oder eine zugängliche Eigenschaft des Objekts benennen, das initialisiert wird, während eine in eckigen Klammern eingeschlossene argument_list die Argumente für einen zugänglichen Indexer für das zu initialisierende Objekt angeben soll. Es wird ein Fehler ausgegeben, wenn ein Objektinitialisierer mehr als einen Memberinitialisierer für dasselbe Feld oder dieselbe Eigenschaft enthält.
Anmerkung: Während ein Objekt-Initialisierer dasselbe Feld oder dieselbe Eigenschaft nicht mehr als einmal festlegen darf, gibt es für Indexer keine derartigen Einschränkungen. Ein Objekt-Initialisierer kann mehrere Initialisierungsziele enthalten, die auf Indexer verweisen, und kann sogar dieselben Indexer-Argumente mehrfach verwenden. Hinweisende
Jedem initializer_target folgt ein Gleichheitszeichen und entweder ein Ausdruck, ein Objektinitialisierer oder ein Auflistungsinitialisierer. Es ist nicht möglich, dass sich Ausdrücke innerhalb des Objekt-Initialisierers auf das neu erstellte Objekt beziehen, das er initialisiert.
Ein Memberinitialisierer, der einen Ausdruck nach dem Gleichheitszeichen angibt, wird auf die gleiche Weise wie eine Zuweisung (§12.21.2) an das Ziel verarbeitet.
Ein Memberinitialisierer, der einen Objektinitialisierer nach dem Gleichheitszeichen angibt, ist ein geschachtelter Objektinitialisierer, d. h. die Initialisierung eines eingebetteten Objekts. Anstatt dem Feld oder der Eigenschaft einen neuen Wert zuzuweisen, werden die Zuweisungen im eingebetteten Objekt-Initialisierer als Zuweisungen an Mitglieder des Feldes oder der Eigenschaft behandelt. Eingebettete Objektinitialisierer können nicht auf Eigenschaften mit einem Wertetyp oder auf schreibgeschützte Felder mit einem Wertetyp angewendet werden.
Ein Memberinitialisierer, der einen Sammlungsinitialisierer nach dem Gleichheitszeichen angibt, ist eine Initialisierung einer eingebetteten Auflistung. Anstatt dem Zielfeld, der Eigenschaft oder dem Indexer eine neue Auflistung zuzuweisen, werden die im Initialisierer angegebenen Elemente der Auflistung hinzugefügt, auf die vom Ziel verwiesen wird. Das Ziel ist ein Sammlungstyp, der den Anforderungen gemäß §12.8.17.4 entspricht.
Wenn sich ein Initialisierungsziel auf einen Indexer bezieht, werden die Argumente für den Indexer immer genau einmal ausgewertet. Selbst wenn die Argumente ungenutzt bleiben (z.B. aufgrund eines leeren geschachtelten Initialisierers), werden sie dennoch auf ihre Nebenwirkungen hin ausgewertet.
Beispiel: Die folgende Klasse repräsentiert einen Punkt mit zwei Koordinaten:
public class Point { public int X { get; set; } public int Y { get; set; } }
Eine Instanz von
Point
kann wie folgt erstellt und initialisiert werden:Point a = new Point { X = 0, Y = 1 };
Dies hat die gleiche Wirkung wie das folgende Beispiel:
Point __a = new Point(); __a.X = 0; __a.Y = 1; Point a = __a;
wobei
__a
eine ansonsten unsichtbare und unzugängliche temporäre Variable ist.Die folgende Klasse zeigt ein aus zwei Punkten erstelltes Rechteck sowie die Erstellung und Initialisierung einer
Rectangle
-Instanz.public class Rectangle { public Point P1 { get; set; } public Point P2 { get; set; } }
Eine Instanz von
Rectangle
kann wie folgt erstellt und initialisiert werden:Rectangle r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y = 3 } };
Dies hat die gleiche Wirkung wie das folgende Beispiel:
Rectangle __r = new Rectangle(); Point __p1 = new Point(); __p1.X = 0; __p1.Y = 1; __r.P1 = __p1; Point __p2 = new Point(); __p2.X = 2; __p2.Y = 3; __r.P2 = __p2; Rectangle r = __r;
wobei
__r
,__p1
und__p2
temporäre Variablen sind, die ansonsten unsichtbar und unzugänglich sind.Wenn der Konstruktor von
Rectangle
die beiden eingebettetenPoint
-Instanzen zuordnet, können sie verwendet werden, um die eingebettetenPoint
-Instanzen zu initialisieren, anstatt neue Instanzen zu erstellen.public class Rectangle { public Point P1 { get; } = new Point(); public Point P2 { get; } = new Point(); }
Das folgende Konstrukt kann verwendet werden, um die eingebetteten
Point
-Instanzen zu initialisieren, anstatt neue Instanzen zuzuweisen:Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } };
Dies hat die gleiche Wirkung wie das folgende Beispiel:
Rectangle __r = new Rectangle(); __r.P1.X = 0; __r.P1.Y = 1; __r.P2.X = 2; __r.P2.Y = 3; Rectangle r = __r;
Ende des Beispiels
12.8.17.4 Auflistungsinitialisierer
Ein Sammlungsinitialisierer spezifiziert die Elemente einer Sammlung.
collection_initializer
: '{' element_initializer_list '}'
| '{' element_initializer_list ',' '}'
;
element_initializer_list
: element_initializer (',' element_initializer)*
;
element_initializer
: non_assignment_expression
| '{' expression_list '}'
;
expression_list
: expression
| expression_list ',' expression
;
Ein Sammlungsinitialisierer besteht aus einer Sequenz von Elementinitialisierern, die durch die Tokens {
und }
eingeschlossen und durch Kommas getrennt sind. Jeder Objekt-Initialisierer gibt ein Element an, das dem zu initialisierenden Sammlungsobjekt hinzugefügt werden soll, und besteht aus einer Liste von Ausdrücken, die von {
- und }
-Token eingeschlossen und durch Kommata getrennt sind. Ein Initialisierer für ein einzelnes Ausdruckselement kann ohne geschweifte Klammern geschrieben werden, kann aber kein Zuordnungsausdruck sein, um Mehrdeutigkeit mit Memberinitialisierern zu vermeiden. Die non_assignment_expression-Produktion ist in §12.22 definiert.
Beispiel: Nachfolgend finden Sie ein Beispiel für einen Ausdruck zur Objekterstellung, der einen Initialisierer für Auflistungen enthält.
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Ende des Beispiels
Das Auflistungsobjekt, auf das ein Sammlungsinitialisierer angewendet wird, muss einen Typ haben, der System.Collections.IEnumerable
implementiert, oder es tritt ein Kompilierungsfehler auf. Für jedes angegebene Element wird in der Reihenfolge von links nach rechts eine normale Mitgliedersuche durchgeführt, um ein Mitglied namens Add
zu finden. Wenn das Ergebnis der Membersuche keine Methodengruppe ist, tritt ein Fehler bei der Kompilierung auf. Andernfalls wird die Überladungsauflösung mit der Ausdrucksliste des Elementinitialisierers als Argumentliste angewendet, und der Sammlungsinitialisierer ruft die resultierende Methode auf. Daher muss das Sammlungsobjekt eine anwendbare Instanz oder Erweiterungsmethode mit dem Namen Add
für jeden Elementinitialisierer enthalten.
Beispiel: Im Folgenden wird eine Klasse gezeigt, die einen Kontakt mit einem Namen und einer Liste von Telefonnummern darstellt, sowie die Erstellung und Initialisierung eines
List<Contact>
.public class Contact { public string Name { get; set; } public List<string> PhoneNumbers { get; } = new List<string>(); } class A { static void M() { var contacts = new List<Contact> { new Contact { Name = "Chris Smith", PhoneNumbers = { "206-555-0101", "425-882-8080" } }, new Contact { Name = "Bob Harris", PhoneNumbers = { "650-555-0199" } } }; } }
die gleiche Wirkung hat wie
var __clist = new List<Contact>(); Contact __c1 = new Contact(); __c1.Name = "Chris Smith"; __c1.PhoneNumbers.Add("206-555-0101"); __c1.PhoneNumbers.Add("425-882-8080"); __clist.Add(__c1); Contact __c2 = new Contact(); __c2.Name = "Bob Harris"; __c2.PhoneNumbers.Add("650-555-0199"); __clist.Add(__c2); var contacts = __clist;
wobei
__clist
,__c1
und__c2
temporäre Variablen sind, die ansonsten unsichtbar und unzugänglich sind.Ende des Beispiels
12.8.17.5 Ausdrücke zur Array-Erstellung
Ein array_creation_expression wird verwendet, um eine neue Instanz eines array_type zu erstellen.
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
Ein Array-Erstellungsausdruck der ersten Form weist eine Array-Instanz des Typs zu, der sich aus dem Löschen jedes einzelnen Ausdrucks aus der Ausdrucksliste ergibt.
Beispiel: Der Array-Erstellungsausdruck
new int[10,20]
erzeugt eine Arrayinstanz vom Typint[,]
, und der Array-Erstellungsausdruck newint[10][,]
erzeugt eine Arrayinstanz vom Typint[][,]
. Ende des Beispiels
Jeder Ausdruck in der Ausdrucksliste muss vom Typ int
, uint
, long
oder ulong
sein oder implizit in einen oder mehrere dieser Typen konvertierbar sein. Der Wert jedes Ausdrucks bestimmt die Länge der entsprechenden Dimension in der neu zugewiesenen Array-Instanz. Da die Länge einer Arraydimension nicht negativ sein soll, handelt es sich um einen Kompilierungszeitfehler, um einen konstanten Ausdruck mit einem negativen Wert in der Ausdrucksliste zu haben.
Außer in einem unsicheren Kontext (§23.2), ist das Layout von Arrays nicht festgelegt.
Wenn ein Arrayerstellungsausdruck der ersten Form einen Arrayinitialisierer enthält, muss jeder Ausdruck in der Ausdrucksliste eine Konstante sein, und die durch die Ausdrucksliste angegebenen Rang- und Dimensionslängen entsprechen denen des Arrayinitialisierers.
In einem Array-Erstellungsausdruck der zweiten oder dritten Form muss der Rang des angegebenen Arraytyps oder des Rangspezifikators mit dem des Array-Initialisierers übereinstimmen. Die einzelnen Dimensionslängen werden von der Anzahl der Elemente in jedem der entsprechenden Schachtelungsebenen des Arrayinitialisierers abgeleitet. Der Initialisierungsausdruck in der folgenden Deklaration
var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};
entspricht exakt
var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};
Ein Arrayerstellungsausdruck der dritten Form wird als implizit typisierter Arrayerstellungsausdruck bezeichnet. Sie ähnelt der zweiten Form, mit der Ausnahme, dass der Elementtyp des Arrays nicht explizit angegeben wird, sondern als der beste gemeinsame Typ (§12.6.3.15) des Sets von Ausdrücken im Array-Initialisierer festgelegt wird. Bei einem mehrdimensionalen Array, d. h. einem Array, in dem der Rangbezeichner mindestens ein Komma enthält, umfasst diese Menge alle expressions in den geschachtelten Array-Initialisierern.
Arrayinitialisierer werden in §17.7 detailliert beschrieben.
Das Ergebnis der Auswertung eines Arrayerstellungsausdrucks wird als Wert klassifiziert, nämlich ein Verweis auf die neu zugeordnete Arrayinstanz. Die Laufzeitverarbeitung von einem Array-Erstellungsausdruck besteht aus den folgenden Schritten:
- Die Dimensionslängenausdrücke der expression_list werden von links nach rechts ausgewertet. Nach der Auswertung jedes Ausdrucks wird eine implizite Umwandlung (§10.2) in einen der folgenden Typen durchgeführt:
int
,uint
,long
,ulong
. Der erste Typ in dieser Liste, für den eine implizite Konversion existiert, wird ausgewählt. Wenn die Auswertung eines Ausdrucks oder die anschließende implizite Konversion eine Ausnahme verursacht, werden keine weiteren Ausdrücke ausgewertet und keine weiteren Schritte ausgeführt. - Die berechneten Werte für die Längen der Dimensionen werden wie folgt validiert: Wenn einer oder mehrere der Werte kleiner als Null sind, wird ein
System.OverflowException
ausgelöst und es werden keine weiteren Schritte ausgeführt. - Eine Array-Instanz mit den angegebenen Längen der Dimensionen wird zugewiesen. Wenn nicht genügend Speicher für die Zuweisung der neuen Instanz zur Verfügung steht, wird ein
System.OutOfMemoryException
ausgelöst und es werden keine weiteren Schritte ausgeführt. - Alle Elemente der neuen Array-Instanz werden auf ihre Standardwerte initialisiert (§9.3).
- Wenn der Array-Erstellungsausdruck einen Arrayinitialisierer enthält, wird jedes Ausdruck im Arrayinitialisierer ausgewertet und dem jeweiligen Arrayelement zugewiesen. Die Auswertungen und Zuordnungen werden in der Reihenfolge ausgeführt, in der die Ausdrücke im Array-Initialisierer geschrieben werden, das heißt, die Elemente werden in aufsteigender Indexreihenfolge initialisiert, wobei die rechte Dimension zuerst zunimmt. Wenn die Auswertung eines gegebenen Ausdrucks oder die nachfolgende Zuordnung zum entsprechenden Arrayelement eine Ausnahme verursacht, werden keine weiteren Elemente initialisiert (und die verbleibenden Elemente haben somit ihre Standardwerte).
Ein Ausdruck zur Erstellung von Arrays ermöglicht das Erstellen eines Arrays mit Elementen eines Arraytyps, aber die Elemente eines solchen Arrays müssen manuell initialisiert werden.
Beispiel: Die Anweisung
int[][] a = new int[100][];
erstellt ein eindimensionales Array mit 100 Elementen vom Typ
int[]
. Der Anfangswert eines jeden Elements istnull
. Es ist nicht möglich, dass ein und derselbe Ausdruck zum Erstellen eines Arrays auch die Unterarrays erzeugt, und die Anweisungint[][] a = new int[100][5]; // Error
führt zu einem Kompilierungszeitfehler. Die Instanziierung der Unterarrays kann stattdessen manuell durchgeführt werden, wie in
int[][] a = new int[100][]; for (int i = 0; i < 100; i++) { a[i] = new int[5]; }
Ende des Beispiels
Anmerkung: Wenn ein Array von Arrays eine „rechteckige“ Form hat, d.h. wenn die Unter-Arrays alle gleich lang sind, ist es effizienter, ein mehrdimensionales Array zu verwenden. im obigen Beispiel. Die Instanziierung des Arrays von Arrays erstellt 101 Objekte – ein äußeres Array und 100 Unterarrays. Im Gegensatz dazu
int[,] a = new int[100, 5];
erstellt nur ein einziges Objekt, ein zweidimensionales Array, und führt die Zuweisung in einer einzigen Anweisung durch.
Hinweisende
Beispiel: Im Folgenden sind Beispiele für implizit typisierte Arrayerstellungsausdrücke aufgeführt:
var a = new[] { 1, 10, 100, 1000 }; // int[] var b = new[] { 1, 1.5, 2, 2.5 }; // double[] var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,] var d = new[] { 1, "one", 2, "two" }; // Error
Der letzte Ausdruck führt zu einem Kompilierfehler, da weder
int
nochstring
implizit in den jeweils anderen konvertierbar ist und es daher keinen besten gemeinsamen Typ gibt. In diesem Fall muss ein explizit typisierter Array-Erstellungsausdruck verwendet werden, zum Beispiel indem der Typobject[]
angegeben wird. Alternativ kann eines der Elemente in einen gemeinsamen Basistyp gecastet werden, der dann der abgeleitete Elementtyp wäre.Ende des Beispiels
Implizit eingegebene Arrayerstellungsausdrücke können mit anonymen Objektinitialisierern (§12.8.17.7) kombiniert werden, um anonym typisierte Datenstrukturen zu erstellen.
Beispiel:
var contacts = new[] { new { Name = "Chris Smith", PhoneNumbers = new[] { "206-555-0101", "425-882-8080" } }, new { Name = "Bob Harris", PhoneNumbers = new[] { "650-555-0199" } } };
Ende des Beispiels
12.8.17.6 Delegaterstellungsausdrücke
Ein delegate_creation_expression wird verwendet, um eine Instanz eines delegate_type abzurufen.
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
Das Argument eines Delegaterstellungsausdrucks muss eine Methodengruppe, eine anonyme Funktion oder ein Wert des Kompilierungszeittyps dynamic
oder eines Delegattyps sein. Wenn es sich bei dem Argument um eine Methodengruppe handelt, wird die Methode und für eine Instanzmethode das Objekt identifiziert, für das ein Delegat erstellt werden soll. Wenn es sich bei dem Argument um eine anonyme Funktion handelt, definiert es direkt die Parameter und den Methodenkörper des Delegatenziels. Wenn das Argument ein Wert ist, bezeichnet es eine Delegatinstanz, von der eine Kopie erstellt werden soll.
Wenn der expression den Kompilierungszeittyp dynamic
hat, wird der delegate_creation_expression dynamisch gebunden (§12.8.17.6), und die folgenden Regeln werden zur Laufzeit mithilfe des Laufzeittyps des expression angewendet. Andernfalls werden die Regeln während der Kompilierung angewendet.
Die Bindungszeitverarbeitung eines delegate_creation_expression in der neuen Form D(E)
, wobei D
ein delegate_type ist und E
ein Ausdruck ist, besteht aus den folgenden Schritten:
Wenn
E
eine Methodengruppe ist, wird der Delegaterstellungsausdruck ähnlich wie eine Methodengruppenkonvertierung (§10.8) vonE
inD
verarbeitet.Wenn
E
eine anonyme Funktion ist, wird der Delegaterstellungsausdruck auf dieselbe Weise wie eine anonyme Funktionskonvertierung (§10.7) vonE
inD
verarbeitet.Wenn
E
ein Wert ist, mussE
kompatibel sein (§20.2) mitD
, und das Ergebnis ist ein Verweis auf einen neu erstellten Delegat mit einer Aufrufliste, die nur einen Eintrag enthält undE
aufruft.
Die Ausführungszeitverarbeitung eines delegate_creation_expression in der neuen Form D(E)
, wobei D
ein delegate_type ist und E
ein Ausdruck ist, besteht aus den folgenden Schritten:
- Wenn
E
eine Methodengruppe ist, wird der Delegaterstellungsausdruck als Methodengruppenkonvertierung (§10.8) vonE
inD
ausgewertet. - Wenn
E
eine anonyme Funktion ist, wird die Delegatenerstellung als anonyme Funktionskonversion vonE
nachD
ausgewertet (§10.7). - Wenn
E
ein Wert eines delegate_type ist:-
E
wird ausgewertet. Wenn diese Auswertung eine Ausnahme verursacht, werden keine weiteren Schritte ausgeführt. - Wenn der Wert von
E
null
ist, wird einSystem.NullReferenceException
ausgegeben, und es werden keine weiteren Schritte ausgeführt. - Es wird eine neue Instanz des Delegattyps
D
zugewiesen. Wenn nicht genügend Speicher für die Zuweisung der neuen Instanz zur Verfügung steht, wird einSystem.OutOfMemoryException
ausgelöst und es werden keine weiteren Schritte ausgeführt. - Die neue Delegatinstanz wird mit einer Aufrufliste, die einen einzigen Eintrag hat, initialisiert, die
E
aufruft.
-
Die Aufrufliste eines Delegaten wird bestimmt, wenn der Delegat instanziiert wird und bleibt dann für die gesamte Lebensdauer des Delegaten konstant. Mit anderen Worten, es ist nicht möglich, die aufrufbaren Zielentitäten eines Delegaten zu ändern, nachdem sie erstellt wurde.
Hinweis: Denken Sie daran, dass eine neue Kombination oder das Entfernen eines Delegaten aus einem anderen einen neuen Delegaten ergibt; kein vorhandener Delegat hat seinen Inhalt geändert. Hinweisende
Es ist nicht möglich, einen Delegaten zu erstellen, der auf eine Eigenschaft, einen Indexer, einen benutzerdefinierten Operator, einen Instanz-Konstruktor, einen Finalisierer oder einen statischen Konstruktor verweist.
Beispiel: Wie oben beschrieben, wird, wenn ein Delegat aus einer Methodengruppe erstellt wird, durch die Parameterliste und den Rückgabetyp des Delegaten bestimmt, welche der überladenen Methoden ausgewählt werden soll. Im Beispiel
delegate double DoubleFunc(double x); class A { DoubleFunc f = new DoubleFunc(Square); static float Square(float x) => x * x; static double Square(double x) => x * x; }
Das Feld
A.f
wird mit einem Delegaten initialisiert, der auf die zweiteSquare
-Methode verweist, da diese Methode exakt mit der Parameterliste und dem Rückgabetyp vonDoubleFunc
übereinstimmt. Wäre die zweiteSquare
Methode nicht vorhanden gewesen, wäre ein Kompilierfehler aufgetreten.Ende des Beispiels
12.8.17.7 Ausdrücke für die Erstellung anonymer Objekte
Ein anonymous_object_creation_expression wird verwendet, um ein Objekt eines anonymen Typs zu erstellen.
anonymous_object_creation_expression
: 'new' anonymous_object_initializer
;
anonymous_object_initializer
: '{' member_declarator_list? '}'
| '{' member_declarator_list ',' '}'
;
member_declarator_list
: member_declarator (',' member_declarator)*
;
member_declarator
: simple_name
| member_access
| null_conditional_projection_initializer
| base_access
| identifier '=' expression
;
Ein anonymer Objekt-Initialisierer deklariert einen anonymen Typ und gibt eine Instanz dieses Typs zurück. Ein anonymer Typ ist ein namenloser Klassentyp, der direkt von object
erbt. Die Member eines anonymen Typs sind eine Abfolge schreibgeschützter Eigenschaften, die aus dem anonymen Objektinitialisierer abgeleitet werden, der zum Erstellen einer Instanz des Typs verwendet wird. Insbesondere ein anonymer Objektinitialisierer des Formulars
new {
p₁=
e₁,
p₂=
e₂,
… pᵥ=
eᵥ}
deklariert einen anonymen Typ der Form
class __Anonymous1
{
private readonly «T1» «f1»;
private readonly «T2» «f2»;
...
private readonly «Tn» «fn»;
public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
{
«f1» = «a1»;
«f2» = «a2»;
...
«fn» = «an»;
}
public «T1» «p1» { get { return «f1»; } }
public «T2» «p2» { get { return «f2»; } }
...
public «Tn» «pn» { get { return «fn»; } }
public override bool Equals(object __o) { ... }
public override int GetHashCode() { ... }
}
dabei ist jeder „Tx” der Typ des entsprechenden Ausdrucks „ex”. Der in einem member_declarator verwendete Ausdruck muss einen Typ haben. Daher liegt ein Kompilierungszeitfehler vor, wenn ein Ausdruck in einem member_declarator ein null
oder eine anonyme Funktion ist.
Die Namen eines anonymen Typs und des Parameters seiner Equals
Methode werden automatisch vom Compiler generiert und können im Programmtext nicht referenziert werden.
Innerhalb desselben Programms erzeugen zwei anonyme Objekt-Initialisierer, die eine Sequenz von Eigenschaften mit denselben Namen und Typen bei der Kompilierung in derselben Reihenfolge angeben, Instanzen desselben anonymen Typs.
Beispiel: Im Beispiel
var p1 = new { Name = "Lawnmower", Price = 495.00 }; var p2 = new { Name = "Shovel", Price = 26.95 }; p1 = p2;
die Zuweisung in der letzten Zeile ist zulässig, weil
p1
undp2
vom selben anonymen Typ sind.Ende des Beispiels
Die Equals
- und GetHashcode
-Methoden auf anonymen Typen überschreiben die von object
geerbten Methoden und sind in Bezug auf die Equals
- und GetHashcode
-Eigenschaften definiert, so dass zwei Instanzen desselben anonymen Typs gleich sind, wenn und nur wenn alle ihre Eigenschaften gleich sind.
Ein Memberdeklarator kann mit einem einfachen Namen (§12.8.4), einem Memberzugriff (§12.8.7), einem bedingten Initialisierer für Nullprojektionen (§12.8.8) oder einem Basiszugriff (§12.8.15) abgekürzt werden. Dies wird als ein Projektionsinitialisierer bezeichnet und ist eine Kurzform für eine Deklaration und Zuweisung zu einer Eigenschaft mit demselben Namen. Insbesondere Memberdeklaratoren der Formulare
«identifier»
, «expr» . «identifier»
und «expr» ? . «identifier»
sind exakt gleichbedeutend mit den folgenden:
«identifer» = «identifier»
, «identifier» = «expr» . «identifier»
und «identifier» = «expr» ? . «identifier»
In einem Projektionsinitialisierer wählt der Bezeichner also sowohl den Wert als auch das Feld oder die Eigenschaft aus, dem der Wert zugewiesen wird. Intuitiv gesehen projiziert ein Projektionsinitialisierer nicht nur einen Wert, sondern auch den Namen des Wertes.
12.8.18 Der typeof-Operator
Der typeof
-Operator wird verwendet, um das System.Type
-Objekt für einen Typ abzurufen.
typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;
unbound_type_name
: identifier generic_dimension_specifier?
| identifier '::' identifier generic_dimension_specifier?
| unbound_type_name '.' identifier generic_dimension_specifier?
;
generic_dimension_specifier
: '<' comma* '>'
;
comma
: ','
;
Die erste Form von typeof_expression besteht aus einem typeof
Schlüsselwort, gefolgt von einem in Klammern gesetzten Typ. Das Ergebnis eines Ausdrucks dieser Form ist das System.Type
Objekt für den genannten Typ. Es gibt nur ein einziges System.Type
-Objekt für jeden gegebenen Typ. Dies bedeutet, dass typeof(T) == typeof(T)
für den Typ T
immer auf „true“ festgelegt ist. Der Typ darf nicht gleich dynamic
sein.
Die zweite Form der typeof_expression besteht aus einem typeof
Schlüsselwort, gefolgt von einem in Klammern gesetzten unbound_type_name.
Hinweis: Ein unbound_type_name ist einem type_name (§7.8) sehr ähnlich, außer dass ein unbound_type_name einen generic_dimension_specifier enthält, wo ein type_name eine type_argument_list enthält. Hinweisende
Wenn der Operand eines typeof_expression eine Abfolge von Token ist, die sowohl die Grammatiken von unbound_type_name als auch type_name erfüllt, nämlich wenn er weder ein generic_dimension_specifier noch eine type_argument_list enthält, wird die Abfolge von Token als type_name betrachtet. Die Bedeutung eines unbound_type_name wird wie folgt bestimmt:
- Konvertieren Sie die Abfolge von Token in eine type_name, indem Sie jede generic_dimension_specifier durch eine type_argument_list mit derselben Anzahl von Kommas und dem Schlüsselwort
object
wie jedes type_argument ersetzen. - Wertet den resultierenden type_name aus, während alle Typparametereinschränkungen ignoriert werden.
- Der unbound_type_name wird in den ungebundenen generischen Typ aufgelöst, der dem resultierenden konstruierten Typ zugeordnet ist (§8.4).
Es ist ein Fehler, wenn der Typname ein NULL-Verweistyp ist.
Das Ergebnis der typeof_expression ist das System.Type
-Objekt für den resultierenden ungebundenen generischen Typ.
Die dritte Form von typeof_expression besteht aus einem typeof
Schlüsselwort gefolgt von einem in Klammern gesetzten void
-Schlüsselwort. Das Ergebnis eines Ausdrucks dieser Form ist das System.Type
-Objekt, das das Fehlen eines Typs darstellt. Das von typeof(void)
zurückgegebene Typobjekt unterscheidet sich von dem Typobjekt, das für jeden Typ zurückgegeben wird.
Hinweis: Dieses spezielle
System.Type
-Objekt kann in Klassenbibliotheken, die die Reflexion auf Methoden in der Sprache ermöglichen, in denen diese Methoden eine Möglichkeit haben möchten, den Rückgabetyp jeder Methode, einschließlichvoid
-Methoden, dazu verwendet werden, mit einer Instanz vonSystem.Type
darzustellen. Hinweisende
Der typeof
-Operator kann für einen Typ-Parameter verwendet werden. Es handelt sich um einen Kompilierungszeitfehler, wenn der Typname bekannterweise ein Verweistyp ist, der NULL-Werte zulässt. Das Ergebnis ist das System.Type
-Objekt für den Laufzeittyp, der an den Typparameter gebunden wurde. Wenn der Laufzeittyp ein Verweistyp ist, der NULL-Werte zulässt, ist das Ergebnis der entsprechende Verweistyp, der keine NULL-Werte zulässt. Der typeof
-Operator kann auch auf einen konstruierten Typ oder einen ungebundenen generischen Typ angewendet werden (§8.4.4). Das System.Type
-Objekt für einen ungebundenen generischen Typ ist nicht dasselbe wie das System.Type
-Objekt des Instanztyps (§15.3.2). Der Instanztyp ist zur Laufzeit immer ein geschlossener konstruierter Typ, sodass sein System.Type
Objekt von den zur Laufzeit verwendeten Typargumenten abhängt. Der ungebundene generische Typ hingegen hat keine Typargumente und liefert unabhängig von den Typargumenten der Runtime das gleiche System.Type
-Objekt.
Beispiel: Das Beispiel
class X<T> { public static void PrintTypes() { Type[] t = { typeof(int), typeof(System.Int32), typeof(string), typeof(double[]), typeof(void), typeof(T), typeof(X<T>), typeof(X<X<T>>), typeof(X<>) }; for (int i = 0; i < t.Length; i++) { Console.WriteLine(t[i]); } } } class Test { static void Main() { X<int>.PrintTypes(); } }
erzeugt die folgende Ausgabe:
System.Int32 System.Int32 System.String System.Double[] System.Void System.Int32 X`1[System.Int32] X`1[X`1[System.Int32]] X`1[T]
Beachten Sie, dass
int
undSystem.Int32
derselbe Typ sind. Das Ergebnis vontypeof(X<>)
hängt nicht vom Typargument ab, aber das Ergebnis vontypeof(X<T>)
schon.Ende des Beispiels
12.8.19 Der sizeof-Operator
Der sizeof
-Operator gibt die Anzahl der 8-Bit-Bytes zurück, die von einer Variablen eines bestimmten Typs belegt werden. Der Typ, der als Operand von „sizeof” angegeben ist, muss ein unmanaged_type sein (§8.8).
sizeof_expression
: 'sizeof' '(' unmanaged_type ')'
;
Für bestimmte vordefinierte Typen liefert der Operator sizeof
einen konstanten int
-Wert, wie in der folgenden Tabelle dargestellt.
Ausdruck | Ergebnis |
---|---|
sizeof(sbyte) |
1 |
sizeof(byte) |
1 |
sizeof(short) |
2 |
sizeof(ushort) |
2 |
sizeof(int) |
4 |
sizeof(uint) |
4 |
sizeof(long) |
8 |
sizeof(ulong) |
8 |
sizeof(char) |
2 |
sizeof(float) |
4 |
sizeof(double) |
8 |
sizeof(bool) |
1 |
sizeof(decimal) |
16 |
Bei einem Enumerationstyp T
ist das Ergebnis des Ausdrucks sizeof(T)
ein konstanter Wert, der der Größe des zugrunde liegenden Typs entspricht, wie oben angegeben. Für alle anderen Operandentypen wird der Operator sizeof
in §23.6.9definiert.
12.8.20 Die Checked- und Unchecked-Operatoren
Mit den Operatoren checked
und unchecked
wird der Überlaufüberprüfungs-Kontext für arithmetische Operationen für Ganzzahltypen und Konvertierungen gesteuert.
checked_expression
: 'checked' '(' expression ')'
;
unchecked_expression
: 'unchecked' '(' expression ')'
;
Der checked
-Operator wertet den enthaltenen Ausdruck in einem überprüften Kontext aus, und der unchecked
-Operator wertet den enthaltenen Ausdruck in einem nicht überprüften Kontext aus. Ein checked_expression oder unchecked_expression entspricht genau einem parenthesized_expression (§12.8.5), es sei denn, der enthaltene Ausdruck wird im angegebenen Überlaufüberprüfungskontext ausgewertet.
Der Überlaufüberprüfungskontext kann auch über die checked
und unchecked
Anweisungen gesteuert werden (§13.12).
Die folgenden Vorgänge sind von dem Überlaufüberprüfungskontext betroffen, der von den aktivierten und deaktivierten Operatoren und Anweisungen eingerichtet wurde:
- Die vordefinierten
++
und--
Operatoren (§12.8.16 und §12.9.6), wenn der Operand einen integralen oder einen Enum-Typ darstellt. - Der vordefinierte
-
unäre Operator (§12.9.3), wenn der Operand ein Ganzzahltyp ist. - Die vordefinierten binäre Operatoren
+
,-
,*
und/
(§12.10), wenn beide Operanden integrale oder Enumerationstypen sind. - Explizite numerische Konvertierungen (§10.3.2) von einem Integral- oder Enumerationstyp in einen anderen Integral- oder Enumerationstyp oder von
float
oderdouble
in einen Integral- oder Enumerationstyp.
Wenn eine der oben genannten Operationen ein Ergebnis liefert, das zu groß ist, um es im Zieltyp darzustellen, steuert der Kontext, in dem die Operation ausgeführt wird, das resultierende Verhalten:
- In einem
checked
-Kontext, wenn die Operation ein konstanter Ausdruck ist (§12.23), tritt ein Kompilierfehler auf. Andernfalls wird, wenn die Operation zur Laufzeit ausgeführt wird, eineSystem.OverflowException
-Ausnahme ausgelöst. - In einem
unchecked
-Kontext wird das Ergebnis gekürzt, indem alle höherwertigen Bits verworfen werden, die nicht in den Zieltyp passen.
Bei nicht konstanten Ausdrücken (§12.23) (Ausdrücke, die zur Laufzeit ausgewertet werden), die nicht von checked
oder unchecked
-Operatoren oder Anweisungen eingeschlossen sind, wird der Standardüberlaufüberprüfungskontext deaktiviert, es sei denn, externe Faktoren (z. B. Compilerschalter und Ausführungsumgebungskonfiguration) rufen zur überprüften Auswertung auf.
Für konstante Ausdrücke (§12.23)(Ausdrücke, die zur Kompilierzeit vollständig ausgewertet werden können) wird der Standardkontext der Überlaufüberprüfung immer geprüft. Überläufe, die während der Auswertung des Ausdrucks zur Kompilierzeit auftreten, führen zu Kompilierzeitfehlern, wenn der konstante Ausdruck nicht explizit in einen unchecked
-Kontext gebracht wird.
Der Textkörper einer anonymen Funktion wird nicht durch die Kontexte checked
oder unchecked
beeinflusst, in denen die anonyme Funktion vorkommt.
Beispiel: Im folgenden Code
class Test { static readonly int x = 1000000; static readonly int y = 1000000; static int F() => checked(x * y); // Throws OverflowException static int G() => unchecked(x * y); // Returns -727379968 static int H() => x * y; // Depends on default }
Es werden keine Kompilierungsfehler gemeldet, da keiner der Ausdrücke zur Kompilierungszeit ausgewertet werden kann. Zur Laufzeit löst die
F
-MethodeSystem.OverflowException
aus, und dieG
-Methode gibt –727379968 zurück (die unteren 32 Bit des Ergebnisses vom Typ „Wert außerhalb des gültigen Bereichs“). Das Verhalten derH
-Methode hängt vom standardmäßigen Kontext der Überlaufprüfung für die Kompilierung ab, ist jedoch entweder identisch mitF
oder identisch mitG
.Ende des Beispiels
Beispiel: Im folgenden Code
class Test { const int x = 1000000; const int y = 1000000; static int F() => checked(x * y); // Compile-time error, overflow static int G() => unchecked(x * y); // Returns -727379968 static int H() => x * y; // Compile-time error, overflow }
die Überläufe, die beim Auswerten der Konstantenausdrücke in
F
undH
auftreten, verursachen Kompilierungsfehler, da die Ausdrücke in einemchecked
-Kontext ausgewertet werden. Ein Überlauf tritt auch beim Evaluieren des konstanten Ausdrucks inG
auf, aber da die Evaluierung in einemunchecked
-Kontext stattfindet, wird der Überlauf nicht gemeldet.Ende des Beispiels
Die Operatoren checked
und unchecked
wirken sich nur auf den Überlaufprüfungskontext für Operationen aus, die textuell in den Tokens "(
" und ")
" enthalten sind. Die Operatoren haben keine Auswirkungen auf Funktionsmember, die infolge der Auswertung des enthaltenen Ausdrucks aufgerufen werden.
Beispiel: Im folgenden Code
class Test { static int Multiply(int x, int y) => x * y; static int F() => checked(Multiply(1000000, 1000000)); }
Die Verwendung von
checked
in F wirkt sich nicht auf die Auswertung vonx * y
inMultiply
aus. Daher wirdx * y
im Standardkontext für Überlaufprüfung ausgewertet.Ende des Beispiels
Der unchecked
-Operator ist nützlich beim Schreiben von Konstanten der signierten integralen Typen in hexadezimaler Schreibweise.
Beispiel:
class Test { public const int AllBits = unchecked((int)0xFFFFFFFF); public const int HighBit = unchecked((int)0x80000000); }
Die beiden hexadezimalen Konstanten oben sind vom Typ
uint
. Da sich die Konstanten außerhalb desint
-Bereichs befinden, würden die Umwandlungen inunchecked
ohne denint
-Operator zu Kompilierungsfehlern führen.Ende des Beispiels
Hinweis: Die
checked
undunchecked
Operatoren und Anweisungen ermöglichen es Programmierern, bestimmte Aspekte einiger numerischer Berechnungen zu steuern. Das Verhalten einiger numerischer Operatoren hängt jedoch von den Datentypen ihrer Operanden ab. Das Multiplizieren von zwei Dezimalzahlen führt immer zu einer Ausnahme beim Überlauf, beispielsweise auch in einem explizit nicht überprüften Konstrukt. Ebenso führt das Multiplizieren von zwei Floats niemals zu einer Ausnahme beim Überlauf auch innerhalb eines explizit überprüften Konstrukts. Darüber hinaus sind andere Operatoren nie von der Art der Überprüfung betroffen, unabhängig davon, ob es sich um eine Standard- oder eine explizite Überprüfung handelt. Hinweisende
12.8.21 Ausdrücke für Standardwerte
Ein Ausdruck für einen Standardwert wird verwendet, um den Standardwert (§9.3) eines Typs abzurufen.
default_value_expression
: explictly_typed_default
| default_literal
;
explictly_typed_default
: 'default' '(' type ')'
;
default_literal
: 'default'
;
Ein default_literal stellt einen Standardwert dar (§9.3). Er hat keinen Typ, kann aber durch eine Standardliteral-Konversion (§10.2.16) in einen beliebigen Typ konvertiert werden.
Das Ergebnis eines default_value_expression ist der Standardwert (§9.3) des expliziten Typs in einem explictly_typed_default oder der Zieltyp der Konvertierung für einen default_value_expression.
Eine default_value_expression ist ein konstanter Ausdruck (§12.23), wenn es sich bei dem Typ um einen der folgenden handelt:
- ein Verweistyp
- ein Typparameter, von dem bekannt ist, dass er ein Referenztyp ist (§8.2);
- einen der folgenden Wertetypen:
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,bool,
; oder - ein Enumerationstyp.
12.8.22 Stapelzuweisung
Ein Stapelzuweisungsausdruck weist einen Speicherblock aus dem Ausführungsstapel zu. Der Ausführungsstack ist ein Bereich des Speichers, in dem lokale Variablen gespeichert werden. Der Ausführungsstapel ist nicht Teil des verwalteten Heaps. Der für die Speicherung lokaler Variablen verwendete Speicher wird automatisch wiederhergestellt, wenn die aktuelle Funktion zurückkehrt.
Die Regeln für sichere Kontexte für einen Stapelzuweisungsausdruck werden in §16.4.12.7 beschrieben.
stackalloc_expression
: 'stackalloc' unmanaged_type '[' expression ']'
| 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
;
stackalloc_initializer
: '{' stackalloc_initializer_element_list '}'
;
stackalloc_initializer_element_list
: stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
;
stackalloc_element_initializer
: expression
;
Der unmanaged_type (§8.8) gibt den Typ der Elemente an, die am neu zugeordneten Speicherort gespeichert werden, und der Ausdruck gibt die Anzahl dieser Elemente an. Zusammen geben diese die erforderliche Zuweisungsgröße an. Der Typ des Ausdrucks muss implizit in den Typ int
konvertierbar sein.
Da die Größe einer Stackzuweisung nicht negativ sein kann, ist es ein Kompilierungszeitfehler, die Anzahl der Elemente als constant_expression anzugeben, der zu einem negativen Wert ausgewertet wird.
Wenn die Anzahl der zuzuweisenden Elemente zur Laufzeit einen negativen Wert hat, ist das Verhalten undefiniert. Ist sie gleich Null, wird keine Zuweisung vorgenommen und der zurückgegebene Wert ist durch die Implementierung definiert. Wenn nicht genügend Arbeitsspeicher verfügbar ist, um die Elemente zuzuweisen, wird ein System.StackOverflowException
-Fehler ausgelöst.
Wenn ein stackalloc_initializer vorhanden ist:
- Wenn unmanaged_type weggelassen wird, wird er nach den Regeln für den am häufigsten verwendeten Typ (§12.6.3.15) für den Satz von stackalloc_element_initializers abgeleitet.
- Wenn constant_expression weggelassen wird, wird angenommen, dass es die Anzahl der stackalloc_element_initializer ist.
- Wenn constant_expression vorhanden ist, soll er der Anzahl von stackalloc_element_initializers entsprechen.
Jeder stackalloc_element_initializer muss eine implizite Umwandlung in unmanaged_type haben (§10.2). Die stackalloc_element_initializer initialisieren Elemente im zugewiesenen Speicher in zunehmender Reihenfolge, beginnend mit dem Element bei Index 0. Wenn kein stackalloc_initializervorhanden ist, bleibt der Inhalt des neu zugewiesenen Speichers ungewiss.
Tritt ein stackalloc_expression direkt als Initialisierungsausdruck einer local_variable_declaration (§13.6.2) auf, wobei der local_variable_type entweder ein Zeigertyp ist (§23.3) oder abgeleitet (var
) ist, ist das Ergebnis des stackalloc_expression ein Zeiger von Typ T*
(§23.9). In diesem Fall muss der stackalloc_expression im unsicheren Code auftreten. Andernfalls ist das Ergebnis eines stackalloc_expression eine Instanz vom Typ Span<T>
, wobei T
der unmanaged_type ist:
Span<T>
(§C.3) ist ein Verweisstrukturtyp (§16.2.3), der einen Speicherblock darstellt, hier der Block, der von der stackalloc_expression zugewiesen wurde, als eine indizierbare Sammlung typisierter (T
) Elemente.- Die Eigenschaft
Length
des Ergebnisses gibt die Anzahl der zugewiesenen Elemente zurück. - Der Indexer des Ergebnisses (§15.9) gibt einen variable_reference (§9.5) an ein Element des zugewiesenen Blocks zurück und ist hinsichtlich des Bereichs überprüft.
Stapelzuweisungsinitialisierer sind in catch
oder finally
-Blöcken nicht zulässig (§13.11).
Anmerkung: Es gibt keine Möglichkeit, mit
stackalloc
zugewiesenen Speicher explizit kostenlos freizugeben. Hinweisende
Alle während der Ausführung eines Funktionsmitglieds erstellten Stack-allokierten Speicherblöcke werden automatisch verworfen, wenn das Funktionsmitglied zurückkehrt.
Mit Ausnahme des stackalloc
-Operators bietet C# keine vordefinierten Konstrukte für die Verwaltung von nicht durch Garbage Collection gesammeltem Speicher. Solche Dienste werden in der Regel von unterstützenden Klassenbibliotheken bereitgestellt oder direkt vom zugrunde liegenden Betriebssystem importiert.
Beispiel:
// Memory uninitialized Span<int> span1 = stackalloc int[3]; // Memory initialized Span<int> span2 = stackalloc int[3] { -10, -15, -30 }; // Type int is inferred Span<int> span3 = stackalloc[] { 11, 12, 13 }; // Error; result is int*, not allowed in a safe context var span4 = stackalloc[] { 11, 12, 13 }; // Error; no conversion from Span<int> to Span<long> Span<long> span5 = stackalloc[] { 11, 12, 13 }; // Converts 11 and 13, and returns Span<long> Span<long> span6 = stackalloc[] { 11, 12L, 13 }; // Converts all and returns Span<long> Span<long> span7 = stackalloc long[] { 11, 12, 13 }; // Implicit conversion of Span<T> ReadOnlySpan<int> span8 = stackalloc int[] { 10, 22, 30 }; // Implicit conversion of Span<T> Widget<double> span9 = stackalloc double[] { 1.2, 5.6 }; public class Widget<T> { public static implicit operator Widget<T>(Span<double> sp) { return null; } }
Bei
span8
führtstackalloc
zu einerSpan<int>
, die von einem impliziten Operator inReadOnlySpan<int>
konvertiert wird. Ebenso wird fürspan9
die resultierendeSpan<double>
mithilfe der Umwandlung in den benutzerdefinierten TypWidget<double>
umgewandelt, wie gezeigt. Ende des Beispiels
12.8.23 Der nameof-Operator
Ein nameof_expression wird verwendet, um den Namen einer Programmentität als konstante Zeichenfolge abzurufen.
nameof_expression
: 'nameof' '(' named_entity ')'
;
named_entity
: named_entity_target ('.' identifier type_argument_list?)*
;
named_entity_target
: simple_name
| 'this'
| 'base'
| predefined_type
| qualified_alias_member
;
Da nameof
kein Schlüsselwort ist, ist ein nameof_expression immer syntaktisch mehrdeutig mit einem Aufruf des einfachen Namens nameof
. Aus Kompatibilitätsgründen wird, falls eine Namenssuche (§12.8.4) des Namens nameof
erfolgreich ist, der Ausdruck als eine invocation_expression behandelt, unabhängig davon, ob der Aufruf gültig ist. Andernfalls handelt es sich um einen nameof_expression.
Einfache Namens- und Memberzugriffssuchen werden zur Kompilierungszeit für die named_entity ausgeführt, wobei die in §12.8.4 und §12.8.7 beschriebenen Regeln gelten. Wenn jedoch die in §12.8.4 und §12.8.7 beschriebene Suche zu einem Fehler führt, weil ein Instanzmitglied in einem statischen Kontext gefunden wurde, erzeugt ein nameof_expression keinen solchen Fehler.
Es ist ein Kompilierungszeitfehler für eine named_entity, die eine Methodengruppe bezeichnet, eine type_argument_list zu haben. Es handelt sich um einen Kompilierungszeitfehler für einen named_entity_target, der den Typ dynamic
hat.
Ein nameof_expression ist ein konstanter Ausdruck vom Typ string
und hat zur Laufzeit keine Auswirkung. Insbesondere wird die named_entity nicht ausgewertet und für die Zwecke der endgültigen Zuordnungsanalyse ignoriert (§9.4.4.22). Der Wert ist der letzte Bezeichner der named_entity vor der optionalen endgültigen type_argument_list, transformiert auf folgende Weise:
- Das Präfix „
@
“, falls verwendet, wird entfernt. - Jede Unicode-Escape-Sequenz wird in das entsprechende Unicode-Zeichen umgewandelt.
- Alle formatting_characters werden entfernt.
Dies sind die gleichen Transformationen, die in §6.4.3 beim Testen der Gleichheit zwischen Bezeichnern angewendet werden.
Beispiel: Im Folgenden werden die Ergebnisse verschiedener
nameof
-Ausdrücke veranschaulicht, wobei davon ausgegangen wird, dass ein generischer TypList<T>
imSystem.Collections.Generic
-Namespace deklariert wird:using TestAlias = System.String; class Program { static void Main() { var point = (x: 3, y: 4); string n1 = nameof(System); // "System" string n2 = nameof(System.Collections.Generic); // "Generic" string n3 = nameof(point); // "point" string n4 = nameof(point.x); // "x" string n5 = nameof(Program); // "Program" string n6 = nameof(System.Int32); // "Int32" string n7 = nameof(TestAlias); // "TestAlias" string n8 = nameof(List<int>); // "List" string n9 = nameof(Program.InstanceMethod); // "InstanceMethod" string n10 = nameof(Program.GenericMethod); // "GenericMethod" string n11 = nameof(Program.NestedClass); // "NestedClass" // Invalid // string x1 = nameof(List<>); // Empty type argument list // string x2 = nameof(List<T>); // T is not in scope // string x3 = nameof(GenericMethod<>); // Empty type argument list // string x4 = nameof(GenericMethod<T>); // T is not in scope // string x5 = nameof(int); // Keywords not permitted // Type arguments not permitted for method group // string x6 = nameof(GenericMethod<Program>); } void InstanceMethod() { } void GenericMethod<T>() { string n1 = nameof(List<T>); // "List" string n2 = nameof(T); // "T" } class NestedClass { } }
Möglicherweise überraschende Teile dieses Beispiels sind die Auflösung von
nameof(System.Collections.Generic)
zu „Generic” anstelle des vollständigen Namespace und vonnameof(TestAlias)
als „TestAlias” anstelle von "String". Ende des Beispiels
12.8.24 Anonyme Methodenausdrücke
Ein anonymous_method_expression ist eine von zwei Möglichkeiten, eine anonyme Funktion zu definieren. Diese sind in §12.19 näher beschrieben.
12.9 Unäre Operatoren
12.9.1 Allgemein
+
, -
, !
(logische Negation §12.9.4 nur), ~
, ++
, --
, Umwandlungen und await
-Operatoren werden als unäre Operatoren bezeichnet.
Hinweis: Der nulltolerante Postfix-Operator (§12.8.9),
!
, wird aufgrund seiner Kompilierungszeit und nicht überladender Art aus der obigen Liste ausgeschlossen. Hinweisende
unary_expression
: primary_expression
| '+' unary_expression
| '-' unary_expression
| logical_negation_operator unary_expression
| '~' unary_expression
| pre_increment_expression
| pre_decrement_expression
| cast_expression
| await_expression
| pointer_indirection_expression // unsafe code support
| addressof_expression // unsafe code support
;
pointer_indirection_expression (§23.6.2) und addressof_expression (§23.6.5) sind nur im unsicheren Code verfügbar (§23).
Wenn der Operand eines unary_expression zur Kompilierzeit den Typ dynamic
hat, wird er dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des unary_expression dynamic
, und die unten beschriebene Auflösung erfolgt zur Laufzeit mithilfe des Laufzeittyps des Operanden.
12.9.2 Unärer Plus-Operator
Für einen Vorgang der Form +x
wird die Überladungsauflösung für binäre Operatoren (§12.4.4) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Der Operand wird in den Parametertyp des ausgewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators. Die vordefinierten unären Plus-Operatoren sind:
int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);
Für jeden dieser Operatoren ist das Ergebnis einfach der Wert des Operanden.
Lifted (§12.4.8) Formen der oben definierten vordefinierten unären Plus-Operatoren sind ebenfalls vordefinierte Operatoren.
12.9.3 Unärer Minus-Operator
Für einen Vorgang der Form –x
wird die Überladungsauflösung für binäre Operatoren (§12.4.4) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Der Operand wird in den Parametertyp des ausgewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators. Die vordefinierten unären Minus-Operatoren sind:
Ganzzahlnegation:
int operator –(int x); long operator –(long x);
Das Ergebnis wird berechnet, indem
X
von Null subtrahiert wird. Wenn der Wert vonX
der kleinste darstellbare Wert des Operandentyps ist (-2³¹ fürint
oder -2⁶³ fürlong
), dann ist die mathematische Negation vonX
innerhalb des Operandentyps nicht darstellbar. Wenn dies innerhalb eineschecked
-Kontexts erfolgt, wird einSystem.OverflowException
ausgelöst; tritt es innerhalb einesunchecked
-Kontexts auf, ist das Ergebnis der Wert des Operanden und der Überlauf wird nicht gemeldet.Wenn der Operand des Negationsoperators vom Typ
uint
ist, wird er in den Typlong
konvertiert, und der Typ des Ergebnisses istlong
. Eine Ausnahme ist die Regel, die es erlaubt, denint
Wert−2147483648
(−2³¹) als dezimales ganzzahliges Literal zu schreiben (§6.4.5.3).Wenn der Operand des Negationsoperators vom Typ
ulong
ist, tritt ein Kompilierfehler auf. Eine Ausnahme von der Regel erlaubt es, dass der Wertlong
−9223372036854775808
(−2⁶³) als Dezimalzahl-Literal geschrieben werden kann (§6.4.5.3)Gleitkomma-Negation:
float operator –(float x); double operator –(double x);
Das Ergebnis ist der Wert von
X
mit umgekehrtem Vorzeichen. Wennx
NaN
ist, ist das ErgebnisNaN
.Dezimalzahl-Negation:
decimal operator –(decimal x);
Das Ergebnis wird berechnet, indem
X
von Null subtrahiert wird. Die dezimale Negation entspricht der Verwendung des unären Minusoperators vom TypSystem.Decimal
.
Lifted (§12.4.8) Formen der oben definierten vordefinierten unären Minus-Operatoren sind ebenfalls vordefinierte Operatoren.
12.9.4 Der logische Negationsoperator
Für einen Vorgang der Form !x
wird die Überladungsauflösung für binäre Operatoren (§12.4.4) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Der Operand wird in den Parametertyp des ausgewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators. Es gibt nur einen vordefinierten logischen Negationsoperator:
bool operator !(bool x);
Dieser Operator berechnet die logische Negation des Operanden: Wenn der Operand true
ist, ist das Ergebnis false
. Wenn der Operand false
ist, ist das Ergebnis true
.
Lifted (§12.4.8) Formen des oben definierten vordefinierten logischen Negationsoperators sind ebenfalls vordefiniert.
Hinweis: Die Operatoren Postfix, logische Negation und Präfix, nulltolerant (§12.8.9), obwohl sie durch dasselbe lexikalische Token (!
) dargestellt werden, unterscheiden sich. Hinweisende
12.9.5 Bitweiser Komplement-Operator
Für einen Vorgang der Form ~x
wird die Überladungsauflösung für binäre Operatoren (§12.4.4) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Der Operand wird in den Parametertyp des ausgewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators. Die vordefinierten bitweisen Komplement-Operatoren sind:
int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);
Für jeden dieser Operatoren ist das Ergebnis der Operation die bitweise Ergänzung von x
.
Jeder Enumerationstyp E
stellt implizit den folgenden bitweisen Ergänzungsoperator bereit:
E operator ~(E x);
Das Ergebnis der Auswertung von ~x
, wobei X
ein Ausdruck eines Aufzählungstyps E
mit einem zugrunde liegenden Typ U
ist, entspricht genau der Auswertung von (E)(~(U)x)
, außer dass die Umwandlung in E
immer so erfolgt, als ob sie in einem unchecked
-Kontext durchgeführt wird (§12.8.20).
Lifted (§12.4.8) Formen der oben definierten vordefinierten bitweise Komplement-Operatoren sind ebenfalls vordefinierte Operatoren.
12.9.6 Inkrementierungs- und Dekrementierungsoperatoren in Präfixnotation
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
Der Operand eines Präfix-Inkrement- oder Dekrementvorgangs muss ein Ausdruck sein, der als Variable, Eigenschaftszugriff oder Indexerzugriff klassifiziert ist. Das Ergebnis der Operation ist ein Wert vom gleichen Typ wie der Operand.
Wenn der Operand eines Präfix-Inkrement- oder Dekrementvorgangs eine Eigenschaft oder ein Indexerzugriff ist, muss die Eigenschaft oder der Indexer sowohl über einen get- als auch einen set-Accessor verfügen. Wenn dies nicht der Fall ist, tritt ein Bindungszeitfehler auf.
Die unäre Operatorüberladungsauflösung (§12.4.4) wird angewendet, um eine bestimmte Operatorimplementierung auszuwählen. Es sind vordefinierte ++
und --
-Operatoren für die folgenden Typen vorhanden: sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
und alle Enumerationstypen. Die vordefinierten ++
-Operatoren geben den Wert zurück, der durch das Hinzufügen von 1
zum Operanden entsteht, und die vordefinierten --
-Operatoren geben den Wert zurück, der durch das Subtrahieren von 1
vom Operanden entsteht. In einem checked
-Kontext, wenn sich das Ergebnis dieser Addition oder Subtraktion außerhalb des Bereichs des Ergebnistyps befindet und der Ergebnistyp ein integraler Typ oder ein Enumerationstyp ist, wird ein System.OverflowException
ausgegeben.
Es muss eine implizite Konvertierung vom Rückgabetyp des ausgewählten unären Operators in den Typ des unary_expression geben, andernfalls tritt ein Kompilierungszeitfehler auf.
Die Laufzeitverarbeitung eines Präfix-Inkrement- oder Dekrementvorgangs des Formulars ++x
oder --x
besteht aus den folgenden Schritten:
- Wenn
x
als Variable klassifiziert wird:-
x
wird ausgewertet, um die Variable zu erzeugen. - Der Wert von
x
wird in den Operandentyp des ausgewählten Operators umgewandelt und der Operator wird mit diesem Wert als Argument aufgerufen. - Der Wert, den der Operator zurückgibt, wird in den Typ von
x
konvertiert. Der resultierende Wert wird an dem Speicherort gespeichert, der durch die Auswertung vonx
gegeben ist und wird zum Ergebnis der Operation.
-
- Wenn
x
als Zugriff auf eine Eigenschaft oder einen Indexer klassifiziert wird:- Der Instanzausdruck (wenn
x
nichtstatic
ist) und die Argumentliste (wennx
ein Indexerzugriff ist), die mitx
verknüpft sind, werden ausgewertet, und die Ergebnisse werden bei den nachfolgenden get- und set-Accessor-Aufrufen verwendet. - Der get-Accessor von
x
wird aufgerufen. - Der vom Accessor get zurückgegebene Wert wird in den Operandentyp des ausgewählten Operators umgewandelt und der Operator wird mit diesem Wert als Argument aufgerufen.
- Der Wert, den der Operator zurückgibt, wird in den Typ von
x
konvertiert. Der set-Accessor vonx
wird mit diesem Wert als Wertargument aufgerufen. - Dieser Wert wird ebenfalls das Ergebnis der Operation.
- Der Instanzausdruck (wenn
Die Operatoren ++
und --
unterstützen auch die Postfix-Notation (§12.8.16). Das Ergebnis von x++
oder x--
ist der Wert von x
vor der Operation, während das Ergebnis von ++x
oder --x
der Wert von x
nach der Operation ist. In beiden Fällen hat x
selbst nach der Operation den gleichen Wert.
Eine Implementierung des Operators ++
oder --
kann entweder in der Postfix- oder Präfix-Notation aufgerufen werden. Es ist nicht möglich, separate Implementierungen der Operatoren für die beiden Notationen zu haben.
Lifted (§12.4.8) Formen der oben definierten vordefinierten Präfix-Inkrementierungs- und Dekrementoperatoren sind ebenfalls vordefinierte.
12.9.7 Umwandlungsausdrücke
Ein cast_expression wird verwendet, um explizit einen Ausdruck in einen vorgegebenen Typ zu konvertieren.
cast_expression
: '(' type ')' unary_expression
;
Ein cast_expression der Form (T)E
, wobei T
ein Typ ist und E
ein unary_expression ist, führt eine explizite Konvertierung (§10.3) des Werts von E
in den Typ T
aus. Wenn keine explizite Konversion von E
nach T
existiert, tritt ein Bindungszeitfehler auf. Andernfalls ist das Ergebnis der Wert, der durch die explizite Umwandlung erzeugt wird. Das Ergebnis wird immer als Wert klassifiziert, auch wenn E
eine Variable bezeichnet.
Die Grammatik für einen cast_expression führt zu bestimmten syntaktischen Mehrdeutigkeiten.
Beispiel: Der Ausdruck
(x)–y
könnte entweder als ein cast_expression (eine Umwandlung von–y
in den Typx
) oder ein additive_expression kombiniert mit einem parenthesized_expression (der den Wertx – y
berechnet) interpretiert werden. Ende des Beispiels
Um die Mehrdeutigkeiten des cast_expression aufzulösen, ist die folgende Regel vorhanden: Eine in Klammern eingeschlossene Abfolge eines oder mehrerer Tokens (§6.4) wird nur dann als Anfang eines cast_expression betrachtet, wenn mindestens eine der folgenden Bedingungen zutrifft:
- Die Sequenz von Token ist eine korrekte Grammatik für einen Typ, aber nicht für einen Ausdruck.
- Die Abfolge von Tokens entspricht der korrekten Grammatik eines Typs, und das Token unmittelbar nach den schließenden Klammern ist entweder das Token „
~
”, das Token „!
”, das Token „(
”, ein Bezeichner (§6.4.3), ein Literal (§6.4.5) oder ein beliebiges Schlüsselwort (§6.4.4) außeras
undis
.
Der obige Begriff „korrekte Grammatik“ bedeutet nur, dass die Sequenz von Token der jeweiligen grammatikalischen Produktion entsprechen muss. Sie berücksichtigt ausdrücklich nicht die tatsächliche Bedeutung von Komponentenbezeichnern.
Beispiel: Wenn
x
undy
Bezeichner sind, dann istx.y
eine korrekte Grammatik für einen Typ, auch wennx.y
nicht wirklich einen Typ bezeichnet. Ende des Beispiels
Hinweis: Aus der Regel zur Sicherstellung der Eindeutigkeit folgt, dass, wenn
x
undy
Bezeichner sind,(x)y
,(x)(y)
und(x)(-y)
cast_expressions, jedoch kein(x)-y
ist, auch wennx
einen Typ bezeichnet. Wenn jedochx
ein Schlüsselwort ist, das einen vordefinierten Typ bezeichnet (z. B.int
), handelt es sich bei allen vier Formen um cast_expressions (weil ein solches Schlüsselwort an sich keinen Ausdruck bilden kann). Hinweisende
12.9.8 Await-Ausdrücke
12.9.8.1 Allgemein
Der await
-Operator wird verwendet, um die Auswertung der umgebenden asynchronen Funktion auszusetzen, bis der asynchrone Vorgang, der durch den Operanden dargestellt wird, abgeschlossen ist.
await_expression
: 'await' unary_expression
;
Ein await_expression ist nur im Textkörper einer asynchronen Funktion zulässig (§15.15). Innerhalb der nächsten eingeschlossenen asynchronen Funktion darf an folgenden Stellen kein await_expression auftreten:
- Innerhalb einer geschachtelten (nicht asynchronen) anonymen Funktion
- Innerhalb des Blocks eines lock_statement
- In einer anonymen Funktionsumwandlung zu einem Ausdrucksstrukturtyp (§10.7.3)
- In einem unsicheren Kontext
Hinweis: Ein await_expression kann nicht an den meisten Stellen innerhalb eines query_expression auftreten, da diese syntaktisch transformiert werden, um nicht asynchrone Lambda-Ausdrücke zu verwenden. Hinweisende
Innerhalb einer asynchronen Funktion darf await
nicht als available_identifier verwendet werden, obwohl der wörtliche Bezeichner @await
verwendet werden kann. Es gibt daher keine syntaktische Mehrdeutigkeit zwischen await_expressions und verschiedenen Ausdrücken, die Bezeichner enthalten. Außerhalb von asynchronen Funktionen verhält sich await
wie ein normaler Bezeichner.
Der Operand eines await_expression wird als Aufgabe bezeichnet. Sie stellt einen asynchronen Vorgang dar, der zum Zeitpunkt der Auswertung des await_expression möglicherweise noch nicht abgeschlossen ist. Der Zweck des Operators await
besteht darin, die Ausführung der eingeschlossenen asynchronen Funktion anzuhalten, bis die erwartete Aufgabe abgeschlossen ist, und dann das Ergebnis zu erhalten.
12.9.8.2 Awaitable-Ausdrücke
Die Aufgabe eines await_expression muss erwartbar sein. Ein Ausdruck t
ist erwartbar, wenn eine der folgenden Bedingungen zutrifft:
-
t
ist ein Kompilierungszeittypdynamic
t
verfügt über eine zugängliche Instanz- oder Erweiterungsmethode namensGetAwaiter
ohne Parameter und Typparameter und einen RückgabetypA
, für den alle folgenden Bedingungen gelten:A
implementiert die SchnittstelleSystem.Runtime.CompilerServices.INotifyCompletion
(nachfolgend unterINotifyCompletion
zur Vereinfachung bezeichnet)A
verfügt über eine zugängliche, lesbare Instanz-EigenschaftIsCompleted
vom Typbool
A
verfügt über eine zugängliche InstanzmethodeGetResult
ohne Parameter und ohne Typparameter.
Der Zweck der GetAwaiter
-Methode besteht darin, einen -Awaiter für die Aufgabe abzurufen. Der Typ A
wird als der Awaiter-Typ für den Await-Ausdruck bezeichnet.
Der Zweck der IsCompleted
-Eigenschaft besteht darin, zu bestimmen, ob die Aufgabe bereits abgeschlossen ist. Wenn das der Fall ist, gibt es keinen Grund, die Auswertung auszusetzen.
Der Zweck der INotifyCompletion.OnCompleted
-Methode besteht darin, eine „Fortsetzung“ für die Aufgabe einzutragen, d. h. einen Delegat (vom Typ System.Action
), der aufgerufen wird, sobald die Aufgabe abgeschlossen ist.
Der Zweck der GetResult
-Methode ist es, das Ergebnis der Aufgabe abzurufen, sobald sie abgeschlossen ist. Das Ergebnis kann ein erfolgreicher Abschluss sein, wobei ein Ergebniswert möglich ist, oder es kann eine Ausnahme sein, die von der GetResult
-Methode ausgelöst wird.
12.9.8.3 Klassifizierung von Await-Ausdrücken
Der Ausdruck await t
wird auf die gleiche Weise klassifiziert wie der Ausdruck (t).GetAwaiter().GetResult()
. Wenn der Rückgabetyp von GetResult
also void
ist, wird der await_expression als „nichts” klassifiziert. Wenn er einenvoid
Rückgabetyp T
hat, wird der await_expression als ein Wert vom Typ T
klassifiziert.
12.9.8.4 Laufzeitauswertung von await-Ausdrücken
Zur Laufzeit wird der Ausdruck await t
wie folgt ausgewertet:
- Ein Awaiter
a
wird abgerufen, indem der Ausdruck(t).GetAwaiter()
ausgewertet wird. - Ein
bool
b
wird durch Auswerten des Ausdrucks(a).IsCompleted
erzielt. - Wenn
b
false
ist, hängt die Auswertung davon ab, oba
die SchnittstelleSystem.Runtime.CompilerServices.ICriticalNotifyCompletion
implementiert (im Folgenden der Kürze halber alsICriticalNotifyCompletion
bezeichnet). Diese Überprüfung erfolgt zur Bindungszeit; d. h. zur Laufzeit, wenna
den Kompilierungszeittypdynamic
aufweist, und andernfalls zur Kompilierungszeit. Lassen Sier
den Wiederaufnahmedelegat bezeichnen (§15.15):- Wenn
a
ICriticalNotifyCompletion
nicht implementiert, wird der Ausdruck((a) as INotifyCompletion).OnCompleted(r)
ausgewertet. - Wenn
a
ICriticalNotifyCompletion
implementiert, wird der Ausdruck((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)
ausgewertet. - Die Auswertung wird dann angehalten, und die Kontrolle wird an den aktuellen Aufrufer der asynchronen Funktion zurückgegeben.
- Wenn
- Entweder unmittelbar nach (wenn
b
true
war) oder nach einem späteren Aufruf des Wiederaufnahmedelegats (wennb
false
war), wird der Ausdruck(a).GetResult()
ausgewertet. Wenn ein Wert zurückgegeben wird, ist dieser Wert das Ergebnis des await_expression. Andernfalls ist das Ergebnis „nichts”.
Die Implementierung der Schnittstellenmethoden INotifyCompletion.OnCompleted
und ICriticalNotifyCompletion.UnsafeOnCompleted
von dem Awaiter sollte sicherstellen, dass der Delegat r
höchstens einmal aufgerufen wird. Andernfalls ist das Verhalten der eingeschlossenen asynchronen Funktion nicht definiert.
12.10 Arithmetische Operatoren
12.10.1 Allgemein
Die *
, /
, %
, +
und -
Operatoren werden als arithmetische Operatoren bezeichnet.
multiplicative_expression
: unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
Wenn ein Operand eines arithmetischen Operators den Typ bei der Kompilierung dynamic
hat, dann wird der Ausdruck dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Ausdrucks dynamic
, und die unten beschriebene Auflösung erfolgt zur Laufzeit mithilfe des Laufzeittyps dieser Operanden, die den Kompilierungszeittyp dynamic
haben.
12.10.2 Multiplikations-Operator
Für einen Vorgang der Form x * y
wird die Überladungsauflösung für binäre Operatoren (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Die vordefinierten Multiplikationsoperatoren sind im Folgenden aufgeführt. Alle Operatoren berechnen das Produkt von x
und y
.
Ganzzahlmultiplikation:
int operator *(int x, int y); uint operator *(uint x, uint y); long operator *(long x, long y); ulong operator *(ulong x, ulong y);
Im Kontext von
checked
, wenn sich das Produkt außerhalb des Bereiches des Ergebnistyps befindet, wird einSystem.OverflowException
ausgelöst. In einemunchecked
-Kontext werden Überläufe nicht gemeldet, und alle höherwertigen Bits außerhalb des Bereichs des Ergebnistyps werden verworfen.Gleitkommamultiplikation:
float operator *(float x, float y); double operator *(double x, double y);
Das Produkt wird nach den Regeln der IEC 60559 Arithmetik berechnet. In der folgenden Tabelle sind die Ergebnisse aller möglichen Kombinationen von begrenzten Werten ungleich Null, Nullen, Unendlichkeiten und NaNs aufgeführt. In der Tabelle sind
x
undy
positive endliche Werte.z
ist das Ergebnis vonx * y
, gerundet auf den nächstliegenden darstellbaren Wert. Wenn die Größe des Ergebnisses zu groß für den Zieltyp ist, istz
unendlich. Aufgrund der Rundung kannz
Null sein, auch wenn wederx
nochy
Null ist.+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
-z
+0
-0
+∞
-∞
NaN
-x
-z
+z
-0
+0
-∞
+∞
NaN
+0
+0
-0
+0
-0
NaN
NaN
NaN
-0
-0
+0
-0
+0
NaN
NaN
NaN
+∞
+∞
-∞
NaN
NaN
+∞
-∞
NaN
-∞
-∞
+∞
NaN
NaN
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(Soweit nicht anders angegeben, bedeutet in den Gleitkommatabellen in §12.10.2 sowie–§12.10.6 die Verwendung von „
+
“, dass der Wert positiv ist; die Verwendung von „-
“, dass der Wert negativ ist; und das Fehlen eines Vorzeichens, dass der Wert entweder positiv oder negativ sein kann oder kein Vorzeichen (NaN) aufweist.)Dezimalmultiplikation:
decimal operator *(decimal x, decimal y);
Wenn der Betrag des Ergebnisses zu groß für die Darstellung im Dezimalformat ist, wird ein
System.OverflowException
ausgelöst. Aufgrund der Rundung kann das Ergebnis gleich Null sein, obwohl keiner der beiden Operanden gleich Null ist. Die Staffelung des Ergebnisses vor jeder Rundung ist die Summe der Staffelungen der beiden Operanden. Die dezimale Multiplikation entspricht der Verwendung des Multiplikationsoperators vom TypSystem.Decimal
.
Lifted (§12.4.8) Formen der oben definierten vordefinierten unären Multiplikationsoperatoren sind ebenfalls vordefinierte Operatoren.
12.10.3 Divisions-Operator
Für einen Vorgang der Form x / y
wird die Überladungsauflösung für binäre Operatoren (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Die vordefinierten Divisionsoperatoren sind unten aufgeführt. Alle Operatoren berechnen den Quotienten von x
und y
.
Ganzzahldivision:
int operator /(int x, int y); uint operator /(uint x, uint y); long operator /(long x, long y); ulong operator /(ulong x, ulong y);
Wenn der Wert des rechten Operanden 0 ist, wird
System.DivideByZeroException
ausgelöst.Die Division rundet das Ergebnis in Richtung Null ab. Der Absolutwert des Ergebnisses ist also die größtmögliche ganze Zahl, die kleiner oder gleich dem Absolutwert des Quotienten der beiden Operanden ist. Das Ergebnis ist Null oder positiv, wenn die beiden Operanden das gleiche Vorzeichen haben und Null oder negativ, wenn die beiden Operanden entgegengesetzte Vorzeichen haben.
Wenn der linke Operand der kleinste darstellbare Wert
int
oderlong
ist und der rechte Operand–1
ist, tritt ein Überlauf auf. In einemchecked
-Kontext verursacht dies das Auslösen einerSystem.ArithmeticException
(oder einer Unterklasse davon). In einemunchecked
-Kontext wird durch die Implementierung bestimmt, ob eineSystem.ArithmeticException
(oder eine Unterklasse davon) ausgelöst wird oder der Überlauf nicht gemeldet wird, sodass der resultierende Wert dem des linken Operanden entspricht.Gleitkommadivision:
float operator /(float x, float y); double operator /(double x, double y);
Der Quotient wird nach den Regeln der IEC 60559 Arithmetik berechnet. In der folgenden Tabelle sind die Ergebnisse aller möglichen Kombinationen von begrenzten Werten ungleich Null, Nullen, Unendlichkeiten und NaNs aufgeführt. In der Tabelle sind
x
undy
positive endliche Werte.z
ist das Ergebnis vonx / y
, gerundet auf den nächstliegenden darstellbaren Wert.+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
-z
+∞
-∞
+0
-0
NaN
-x
-z
+z
-∞
+∞
-0
+0
NaN
+0
+0
-0
NaN
NaN
+0
-0
NaN
-0
-0
+0
NaN
NaN
-0
+0
NaN
+∞
+∞
-∞
+∞
-∞
NaN
NaN
NaN
-∞
-∞
+∞
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
Dezimaldivision:
decimal operator /(decimal x, decimal y);
Wenn der Wert des rechten Operanden 0 ist, wird
System.DivideByZeroException
ausgelöst. Wenn der Betrag des Ergebnisses zu groß für die Darstellung im Dezimalformat ist, wird einSystem.OverflowException
ausgelöst. Aufgrund der Rundung kann das Ergebnis Null sein, auch wenn der erste Operand nicht Null ist. Die Skala des Ergebnisses vor jeglicher Rundung ist die nächstgelegene Skala zur bevorzugten Skala, die ein Ergebnis liefert, das dem exakten Ergebnis entspricht. Die bevorzugte Staffelung ist der Maßstab vonx
minus der Maßstab vony
.Dezimalteilung entspricht der Verwendung des Divisionoperators vom Typ
System.Decimal
.
Lifted (§12.4.8) Formen der oben definierten vordefinierten unären Divisionsoperatoren sind ebenfalls vordefinierte Operatoren.
12.10.4 Rest-Operator
Für einen Vorgang der Form x % y
wird die Überladungsauflösung für binäre Operatoren (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Die vordefinierten Rest-Operatoren werden unten aufgeführt. Alle Operatoren berechnen den Rest der Division zwischen x
und y
.
Ganzzahlrest:
int operator %(int x, int y); uint operator %(uint x, uint y); long operator %(long x, long y); ulong operator %(ulong x, ulong y);
Das Ergebnis von
x % y
ist der durchx – (x / y) * y
erzeugte Wert. Wenny
null ist, wird einSystem.DivideByZeroException
ausgelöst.Wenn der linke Operand der kleinste
int
- oderlong
-Wert ist und der rechte Operand–1
ist, wird einSystem.OverflowException
nur dann ausgelöst, wennx / y
eine Ausnahme auslösen würde.Gleitkommarest:
float operator %(float x, float y); double operator %(double x, double y);
In der folgenden Tabelle sind die Ergebnisse aller möglichen Kombinationen von begrenzten Werten ungleich Null, Nullen, Unendlichkeiten und NaNs aufgeführt. In der Tabelle sind
x
undy
positive endliche Werte.z
ist das Ergebnis vonx % y
und wird berechnet alsx – n * y
, wobei n die größtmögliche ganze Zahl ist, die kleiner oder gleichx / y
ist. Diese Methode zur Berechnung des Rests ist analog zu derjenigen, die für ganzzahlige Operanden verwendet wird, unterscheidet sich aber von der Definition der IEC 60559 (in dern
die Ganzzahl ist, diex / y
am nächsten kommt).+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
+z
NaN
NaN
+x
+x
NaN
-x
-z
-z
NaN
NaN
-x
-x
NaN
+0
+0
+0
NaN
NaN
+0
+0
NaN
-0
-0
-0
NaN
NaN
-0
-0
NaN
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
Dezimaler Rest:
decimal operator %(decimal x, decimal y);
Wenn der Wert des rechten Operanden 0 ist, wird
System.DivideByZeroException
ausgelöst. Es ist implementationsspezifisch, wann einSystem.ArithmeticException
(oder eine Unterklasse davon) ausgelöst wird. Eine konforme Implementierung darf in keinem Fall eine Ausnahme fürx % y
auslösen, wennx / y
keine Ausnahme auslöst. Die Staffelung des Ergebnisses, vor jeglicher Rundung, ist die größere der Staffelungen der beiden Operanden, und das Vorzeichen des Ergebnisses, wenn es nicht null ist, entspricht dem vonx
.Dezimaler Rest entspricht der Verwendung des Restoperators vom Typ
System.Decimal
.Hinweis: Diese Regeln stellen sicher, dass das Ergebnis für alle Typen nie das gegenteilige Zeichen des linken Operanden aufweist. Hinweisende
Lifted (§12.4.8) Formen der oben definierten vordefinierten Restoperatoren sind ebenfalls vordefinierte Operatoren.
12.10.5 Additions-Operator
Für einen Vorgang der Form x + y
wird die Überladungsauflösung für binäre Operatoren (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Die vordefinierten Additionsoperatoren sind im Folgenden aufgeführt. Für numerische und Aufzählungstypen berechnen die vordefinierten Additionsoperatoren die Summe der beiden Operanden. Wenn ein oder beide Operanden vom Typ string
sind, verketten die vordefinierten Additionsoperatoren die Zeichenfolgendarstellung der Operanden.
Ganzzahladdition:
int operator +(int x, int y); uint operator +(uint x, uint y); long operator +(long x, long y); ulong operator +(ulong x, ulong y
Im Kontext von
checked
, wenn sich das Summenprodukt außerhalb des Bereiches des Ergebnistyps befindet, wird einSystem.OverflowException
ausgelöst. In einemunchecked
-Kontext werden Überläufe nicht gemeldet, und alle höherwertigen Bits außerhalb des Bereichs des Ergebnistyps werden verworfen.Gleitkommaaddition:
float operator +(float x, float y); double operator +(double x, double y);
Die Summe wird gemäß den Regeln der IEC 60559 Arithmetik berechnet. In der folgenden Tabelle sind die Ergebnisse aller möglichen Kombinationen von begrenzten Werten ungleich Null, Nullen, Unendlichkeiten und NaNs aufgeführt. In der Tabelle sind
x
undy
endliche Werte ungleich Null, undz
ist das Ergebnis ausx + y
. Wennx
undy
die gleiche Größe jedoch andere Vorzeichen haben, istz
eine positive Null. Wennx + y
zu groß ist, um im Zieltyp darzustellen, istz
eine Unendlichkeit mit demselben Zeichen wiex + y
.y
+0
-0
+∞
-∞
NaN
x
z
x
x
+∞
-∞
NaN
+0
y
+0
+0
+∞
–∞
NaN
-0
y
+0
-0
+∞
-∞
NaN
+∞
+∞
+∞
+∞
+∞
NaN
NaN
-∞
-∞
-∞
-∞
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
Dezimaladdition:
decimal operator +(decimal x, decimal y);
Wenn der Betrag des Ergebnisses zu groß für die Darstellung im Dezimalformat ist, wird ein
System.OverflowException
ausgelöst. Die Staffelung des Ergebnisses vor jeder Rundung ist die größere der Staffelungen der beiden Operanden.Die Dezimaladdition entspricht der Verwendung des Additionsoperators vom Typ
System.Decimal
.Enumerationsaddition Jeder Aufzählungstyp stellt implizit die folgenden vordefinierten Operatoren zur Verfügung, wobei
E
der Aufzählungstyp undU
der zugrunde liegende Typ vonE
ist:E operator +(E x, U y); E operator +(U x, E y);
Zur Laufzeit werden diese Operatoren genau wie
(E)((U)x + (U)y
) ausgewertet.Zeichenfolgenverkettung:
string operator +(string x, string y); string operator +(string x, object y); string operator +(object x, string y);
Diese Überladungen des binären Operators
+
führen eine Verkettung von Zeichenfolgen durch. Wenn ein Operand der Zeichenfolgenverkettungnull
ist, wird eine leere Zeichenfolge ersetzt. Andernfalls wird jeder Nicht-string
-Operand in seine Zeichenfolgendarstellung konvertiert, indem die virtuelleToString
-Methode, die vom Typobject
geerbt wurde, aufgerufen wird. WennToString
null
zurückgibt, wird eine leere Zeichenfolge ersetzt.Beispiel:
class Test { static void Main() { string s = null; Console.WriteLine("s = >" + s + "<"); // Displays s = >< int i = 1; Console.WriteLine("i = " + i); // Displays i = 1 float f = 1.2300E+15F; Console.WriteLine("f = " + f); // Displays f = 1.23E+15 decimal d = 2.900m; Console.WriteLine("d = " + d); // Displays d = 2.900 } }
Die in den Kommentaren angezeigte Ausgabe ist das typische Ergebnis eines US-English-Systems. Die genaue Ausgabe kann möglicherweise von den regionalen Einstellungen der Ausführungsumgebung abhängen. Der Zeichenfolgenverkettungsoperator selbst verhält sich in jedem Fall auf die gleiche Weise, aber die
ToString
- Methoden, die während der Ausführung implizit aufgerufen werden, können von regionalen Einstellungen beeinflusst werden.Ende des Beispiels
Das Ergebnis des String-Konkatenationsoperators ist ein
string
, der aus den Zeichen des linken Operanden besteht, gefolgt von den Zeichen des rechten Operanden. Der Zeichenfolgenverkettungsoperator gibt niemals einennull
-Wert zurück. EineSystem.OutOfMemoryException
könnte ausgelöst werden, wenn nicht genügend Arbeitsspeicher verfügbar ist, um die resultierende Zeichenkette zuzuordnen.Delegatkombination Jeder Delegatentyp stellt implizit den folgenden vordefinierten Operator zur Verfügung, wobei
D
der Delegatentyp ist:D operator +(D x, D y);
Wenn der erste Operand
null
ist, ist das Ergebnis der Operation der Wert des zweiten Operanden (auch wenn dieser ebenfallsnull
ist). Andernfalls, wenn der zweite Operandnull
ist, ist das Ergebnis des Vorgangs der Wert des ersten Operanden. Andernfalls ist das Ergebnis des Vorgangs eine neue Stellvertretungsinstanz, deren Aufrufliste aus den Elementen in der Aufrufliste des ersten Operanden besteht, gefolgt von den Elementen in der Aufrufliste des zweiten Operanden. Das bedeutet, dass die Aufrufliste des resultierenden Delegaten die Verkettung der Aufruflisten der beiden Operanden ist.Hinweis: Beispiele für Delegatkombinationen finden Sie unter §12.10.6 und §20.6. Da
System.Delegate
kein Delegattyp ist, ist der Operator + dafür nicht definiert. Hinweisende
Lifted (§12.4.8) Formen der oben definierten vordefinierten unären Additionsoperatoren sind ebenfalls vordefinierte Operatoren.
12.10.6 Subtraktions-Operator
Für einen Vorgang der Form x – y
wird die Überladungsauflösung für binäre Operatoren (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Die vordefinierten Subtraktionsoperatoren sind im Folgenden aufgeführt. Die Operatoren subtrahieren alle y
von x
.
Ganzzahlsubtraktion:
int operator –(int x, int y); uint operator –(uint x, uint y); long operator –(long x, long y); ulong operator –(ulong x, ulong y
Im einem
checked
-Kontext, wenn sich das Produkt außerhalb des Bereiches des Ergebnistyps befindet, wird einSystem.OverflowException
ausgelöst. In einemunchecked
-Kontext werden Überläufe nicht gemeldet, und alle höherwertigen Bits außerhalb des Bereichs des Ergebnistyps werden verworfen.Gleitkommasubtraktion:
float operator –(float x, float y); double operator –(double x, double y);
Die Differenz wird nach den Regeln der IEC 60559 Arithmetik berechnet. In der folgenden Tabelle sind die Ergebnisse aller möglichen Kombinationen von begrenzten Werten ungleich Null, Nullen, Unendlichkeiten und NaNs aufgeführt. In der Tabelle sind
x
undy
endliche Werte ungleich Null, undz
ist das Ergebnis ausx – y
. Wennx
undy
gleich sind, istz
eine positive Null. Wennx – y
zu groß ist, um im Zieltyp darzustellen, istz
eine Unendlichkeit mit demselben Vorzeichen wiex – y
.y
+0
-0
+∞
-∞
NaN
x
z
x
x
-∞
+∞
NaN
+0
-y
+0
+0
-∞
+∞
NaN
-0
-y
-0
+0
-∞
+∞
NaN
+∞
+∞
+∞
+∞
NaN
+∞
NaN
-∞
-∞
-∞
-∞
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(In der obigen Tabelle bezeichnen die
-y
-Einträge die Negation vony
, nicht, dass der Wert negativ ist.)Dezimale Subtraktion:
decimal operator –(decimal x, decimal y);
Wenn der Betrag des Ergebnisses zu groß für die Darstellung im Dezimalformat ist, wird ein
System.OverflowException
ausgelöst. Die Staffelung des Ergebnisses vor jeder Rundung ist die größere der Staffelungen der beiden Operanden.Die Dezimalsubtraktion ist gleichbedeutend mit der Verwendung des Subtraktionsoperators vom Typ
System.Decimal
.Enumerationsubstraktion Jeder Aufzählungstyp bietet implizit den folgenden vordefinierten Operator, wobei
E
der Aufzählungstyp undU
der zugrunde liegende Typ vonE
ist:U operator –(E x, E y);
Dieser Operator wird genau wie
(U)((U)x – (U)y)
ausgewertet. Mit anderen Worten, der Operator berechnet die Differenz zwischen den Ordinalwerten vonx
undy
, und der Typ des Ergebnisses ist der zugrunde liegende Typ der Aufzählung.E operator –(E x, U y);
Dieser Operator wird genau wie
(E)((U)x – y)
ausgewertet. Mit anderen Worten, der Operator subtrahiert einen Wert vom zugrunde liegenden Typ der Enumeration und ergibt einen Wert der Enumeration.Delegatentfernung Jeder Delegatentyp stellt implizit den folgenden vordefinierten Operator zur Verfügung, wobei
D
der Delegatentyp ist:D operator –(D x, D y);
Die Semantik ist wie folgt:
- Wenn der erste Operand
null
ist, ist das Ergebnis der Operationnull
. - Andernfalls, wenn der zweite Operand
null
ist, ist das Ergebnis des Vorgangs der Wert des ersten Operanden. - Andernfalls repräsentieren beide Operanden nicht-leere Aufruflisten (§20.2).
- Wenn die Listen gleich sind, wie vom Delegatggleichheitsoperator (§12.12.9) bestimmt, wird das Ergebnis des Vorgangs
null
. - Andernfalls ist das Ergebnis der Operation eine neue Aufrufliste, die aus der Liste des ersten Operanden besteht, aus der die Einträge des zweiten Operanden entfernt wurden, vorausgesetzt, die Liste des zweiten Operanden ist eine Unterliste der ersten. (Um die Gleichheit von Unterlisten zu ermitteln, werden entsprechende Einträge für den Delegatggleichheitsoperator verglichen.) Wenn die Liste des zweiten Operanden mehreren zusammenhängenden Unterlisten von Einträgen in der Liste des ersten Operanden entspricht, wird die letzte übereinstimmende zusammenhängende Unterliste entfernt.
- Andernfalls ist das Ergebnis des Vorgangs der Wert des linken Operanden.
- Wenn die Listen gleich sind, wie vom Delegatggleichheitsoperator (§12.12.9) bestimmt, wird das Ergebnis des Vorgangs
Keine der Operandenlisten (falls vorhanden) wird im Prozessverlauf geändert.
Beispiel:
delegate void D(int x); class C { public static void M1(int i) { ... } public static void M2(int i) { ... } } class Test { static void Main() { D cd1 = new D(C.M1); D cd2 = new D(C.M2); D list = null; list = null - cd1; // null list = (cd1 + cd2 + cd2 + cd1) - null; // M1 + M2 + M2 + M1 list = (cd1 + cd2 + cd2 + cd1) - cd1; // M1 + M2 + M2 list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2); // M2 + M1 list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd2); // M1 + M1 list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd1); // M1 + M2 list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd1); // M1 + M2 + M2 + M1 list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2 + cd2 + cd1); // null } }
Ende des Beispiels
- Wenn der erste Operand
Lifted (§12.4.8) Formen der oben definierten vordefinierten Substraktionsoperatoren sind ebenfalls vordefinierte Operatoren.
12.11 Schiebeoperatoren
Die <<
- und >>
-Operatoren werden verwendet, um Bit-Verschiebungsoperationen durchzuführen.
shift_expression
: additive_expression
| shift_expression '<<' additive_expression
| shift_expression right_shift additive_expression
;
Wenn ein Operand eines shift_expression den Kompilierungszeittyp dynamic
hat, wird der Ausdruck dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Ausdrucks dynamic
, und die unten beschriebene Auflösung erfolgt zur Laufzeit mithilfe des Laufzeittyps dieser Operanden, die den Kompilierungszeittyp dynamic
haben.
Für einen Vorgang der Form x << count
oder x >> count
wird die Überladungsauflösung für binäre Operatoren (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Bei der Deklarierung eines überladenen Shift-Operators muss der Typ des ersten Operanden immer die Klasse oder Struktur sein, die die Operatordeklaration enthält, und der Typ des zweiten Operanden muss immer int
sein.
Die vordefinierten Verschiebungsoperatoren sind unten aufgeführt.
Nach links verschieben:
int operator <<(int x, int count); uint operator <<(uint x, int count); long operator <<(long x, int count); ulong operator <<(ulong x, int count);
Der
<<
-Operator verschiebtx
um eine Anzahl von Bits nach links, die wie unten beschrieben berechnet wird.Die höherwertigen Bits außerhalb des Bereichs des Ergebnistyps von
x
werden verworfen, die verbleibenden Bits werden nach links verschoben, und die leeren Bitpositionen mit niedriger Wertigkeit werden auf Null gesetzt.Nach rechts verschieben:
int operator >>(int x, int count); uint operator >>(uint x, int count); long operator >>(long x, int count); ulong operator >>(ulong x, int count);
Der
>>
-Operator verschiebtx
um eine Anzahl von Bits nach rechts, die wie unten beschrieben berechnet wird.Wenn
x
vom Typint
oderlong
ist, werden die niederwertigen Bits vonx
verworfen, die verbleibenden Bits nach rechts verschoben, und die höherwertigen leeren Bitpositionen auf Null gesetzt, wennx
nicht negativ ist, oder auf Eins gesetzt, wennx
negativ ist.Wenn
x
vom Typuint
oderulong
ist, werden die niedrigwertigen Bits vonx
verworfen, die verbleibenden Bits nach rechts verschoben, und die leerstehenden höherwertigen Bitstellen auf Null festgelegt.
Für die vordefinierten Operatoren wird die Anzahl der zu verschiebenden Bits wie folgt berechnet:
- Wenn der Typ von
x
int
oderuint
ist, wird die Anzahl der Verschiebungen durch die niederwertigen fünf Bits voncount
bestimmt. Mit anderen Worten, die Anzahl der Verschiebungen wird auscount & 0x1F
berechnet. - Wenn der Typ von
x
long
oderulong
ist, wird die Anzahl der Verschiebungen durch die niederwertigen sechs Bits voncount
bestimmt. Mit anderen Worten, die Anzahl der Verschiebungen wird auscount & 0x3F
berechnet.
Wenn die resultierende Anzahl der zu verschiebenden Bits gleich Null ist, geben die Verschiebeoperatoren einfach den Wert x
zurück.
Verschiebevorgänge verursachen niemals Überläufe und führen sowohl in geprüften als auch in ungeprüften Kontexten zu identischen Ergebnissen.
Wenn der linke Operand des >>
-Operators einen signierten integralen Typ aufweist, führt der Operator ein arithmetisches Schieben nach rechts aus, wobei der Wert des höchstwertigen Bits (das Vorzeichenbit) des Operanden auf die höherwertigen leeren Bitpositionen verteilt wird. Wenn der linke Operand des >>
-Operators einen nicht-signierten integralen Typs hat, führt der Operator eine logische Verschiebung nach rechts aus, wobei höherwertige leere Bitpositionen immer auf Null gesetzt werden. Zum Ausführen des entgegengesetzten Vorgangs, der vom Operandentyp abgeleitet wurde, können explizite Umwandlungen verwendet werden.
Beispiel: Wenn
x
eine Variable vom Typint
ist, führt der Vorgangunchecked ((int)((uint)x >> y))
eine logische Verschiebung nach rechts vonx
aus. Ende des Beispiels
Lifted (§12.4.8) Formen der oben definierten vordefinierten Shift-Operatoren sind ebenfalls vordefinierte Operatoren.
12.12 Relationale und Typtest-Operatoren
12.12.1 Allgemein
Die Operatoren ==
, !=
, <
, >
, <=
, >=
, is
und as
werden als relationale und Typentest-Operatoren bezeichnet.
relational_expression
: shift_expression
| relational_expression '<' shift_expression
| relational_expression '>' shift_expression
| relational_expression '<=' shift_expression
| relational_expression '>=' shift_expression
| relational_expression 'is' type
| relational_expression 'is' pattern
| relational_expression 'as' type
;
equality_expression
: relational_expression
| equality_expression '==' relational_expression
| equality_expression '!=' relational_expression
;
Hinweis: Die Suche nach dem rechten Operanden des
is
-Operators muss zuerst als Typ und dann als Ausdruck getestet werden, der mehrere Token umfassen kann. Wenn der Operand ein Ausdruck ist, muss der Musterausdruck mindestens ebenso hohe Priorität wie der Shift-Ausdruck haben. Hinweisende
Der is
-Operator wird in §12.12.12 und der as
-Operator in §12.12.13 beschrieben.
Die Operatoren ==
, !=
, <
, >
, <=
und >=
sind Vergleichsoperatoren.
Wenn ein default_literal (§12.8.21) als Operand eines <
, >
, <=
oder >=
-Operators verwendet wird, tritt ein Kompilierungszeitfehler auf.
Wenn ein default_literal als beide Operanden eines ==
- oder eines !=
-Operators verwendet wird, tritt ein Kompilierungszeitfehler auf. Wenn ein default_literal als linker Operand des is
- oder as
-Operators verwendet wird, tritt ein Kompilierungszeitfehler auf.
Wenn ein Operand eines Vergleichsoperators den Typ bei der Kompilierung dynamic
hat, dann wird der Ausdruck dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Ausdrucks dynamic
, und die unten beschriebene Auflösung erfolgt zur Laufzeit mithilfe des Laufzeittyps dieser Operanden, die den Kompilierungszeittyp dynamic
haben.
Für einen Vorgang der Form x «op» y
, wobei „op“ ein Vergleichsoperator ist, wird die Überladungsauflösung (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators. Wenn beide Operanden eines equality_expression das null
-Literal sind, erfolgt keine Überladungsauflösung, und der Ausdruck wird dementsprechend auf einen konstanten Wert von true
oder false
ausgewertet, je nachdem, ob der Operator ==
oder !=
ist.
Die vordefinierten Vergleichsoperatoren werden in den folgenden Unterklauseln beschrieben. Alle vordefinierten Vergleichsoperatoren liefern ein Ergebnis vom Typ bool, wie in der folgenden Tabelle beschrieben.
Vorgang | Ergebnis |
---|---|
x == y |
true wenn x gleich y ist, false sonst |
x != y |
true wenn x nicht gleich y ist, sonst false |
x < y |
true wenn x kleiner ist als y , false sonst |
x > y |
true wenn x größer ist als y , false sonst |
x <= y |
true wenn x kleiner oder gleich y ist, sonst false . |
x >= y |
true wenn x größer als oder gleich y ist, sonst false |
12.12.2 Ganzzahlvergleichs-Operatoren
Die vordefinierten ganzzahligen Vergleichsoperatoren sind:
bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);
bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);
bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);
bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);
bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);
Jeder dieser Operatoren vergleicht die numerischen Werte der beiden ganzzahligen Operanden und gibt einen bool
Wert zurück, der angibt, ob die jeweilige Beziehung true
oder false
ist.
Lifted (§12.4.8) Formen der oben definierten vordefinierten Ganzzahl-Vergleichsoperatoren sind ebenfalls vordefinierte Operatoren.
12.12.3 Gleitkommavergleichsoperatoren
Die vordefinierten Gleitkommavergleichsoperatoren sind:
bool operator ==(float x, float y);
bool operator ==(double x, double y);
bool operator !=(float x, float y);
bool operator !=(double x, double y);
bool operator <(float x, float y);
bool operator <(double x, double y);
bool operator >(float x, float y);
bool operator >(double x, double y);
bool operator <=(float x, float y);
bool operator <=(double x, double y);
bool operator >=(float x, float y);
bool operator >=(double x, double y);
Die Operatoren vergleichen die Operanden nach den Regeln der Norm IEC 60559:
Wenn einer der Operanden NaN ist, ist das Ergebnis für alle Operatoren false
, mit Ausnahme von !=
, für den das Ergebnis true
ist. Für zwei beliebige Operanden liefert x != y
immer das gleiche Ergebnis wie !(x == y)
. Wenn jedoch ein oder beide Operanden naN sind, erzeugen die Operatoren <
, >
, <=
und >=
nicht die gleichen Ergebnisse wie die logische Negation des entgegengesetzten Operators.
Beispiel: Wenn eine(r) von
x
undy
NaN ist, dann istx < y
false
, aber!(x >= y)
isttrue
. Ende des Beispiels
Wenn kein Operand NaN ist, vergleichen die Operatoren die Werte der beiden Gleitkommaoperanden in Bezug auf die Sortierung.
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
wobei min
und max
die kleinsten und größten positiven begrenzten Werte sind, die in dem gegebenen Fließkommaformat dargestellt werden können. Wichtige Auswirkungen dieser Reihenfolge:
- Negative und positive Nullen gelten als gleich.
- Eine negative Unendlichkeit wird als weniger als alle anderen Werte betrachtet, ist jedoch gleich einer anderen negativen Unendlichkeit.
- Eine positive Unendlichkeit gilt als größer als alle anderen Werte, aber gleich einer anderen positiven Unendlichkeit.
Lifted (§12.4.8) Formen der oben definierten vordefinierten Gleitkomma-Vergleichsoperatoren sind ebenfalls vordefinierte Operatoren.
12.12.4 Dezimalvergleichsoperatoren
Die vordefinierten dezimalen Vergleichsoperatoren sind:
bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);
Jeder dieser Operatoren vergleicht die numerischen Werte der beiden dezimalen Operanden und gibt einen bool
Wert zurück, der angibt, ob die jeweilige Beziehung true
oder false
ist. Jeder Dezimalvergleich entspricht der Verwendung des entsprechenden relationalen oder Gleichheitsoperators vom Typ System.Decimal
.
Lifted (§12.4.8) Formen der oben definierten vordefinierten Dezimalvergleichsoperatoren sind ebenfalls vordefinierte Operatoren.
12.12.5 Boolesche Gleichheitsoperatoren
Die vordefinierten booleschen Gleichheitsoperatoren sind:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
Das Ergebnis von ==
ist true
, wenn x
und y
beide true
sind oder wenn x
und y
beide false
sind. Andernfalls ist das Ergebnis false
.
Das Ergebnis von !=
ist false
, wenn x
und y
beide true
sind oder wenn x
und y
beide false
sind. Andernfalls ist das Ergebnis true
. Wenn die Operanden vom Typ bool
sind, liefert der !=
-Operator das gleiche Ergebnis wie der ^
-Operator.
Lifted (§12.4.8) Formen der oben definierten vordefinierten Booleschen Gleichheitsoperatoren sind ebenfalls vordefinierte Operatoren.
12.12.6 Enumerationsvergleichsoperatoren
Jeder Aufzählungstyp bietet implizit die folgenden vordefinierten Vergleichsoperatoren
bool operator ==(E x, E y);
bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);
Das Ergebnis der Bewertung von x «op» y
, wenn x und y Ausdrücke eines Enumerationstyps E
mit einem zugrunde liegenden Typ U
sind und «op» einer der Vergleichsoperatoren ist, ist genau dasselbe wie die Bewertung von ((U)x) «op» ((U)y)
. Mit anderen Worten: Die Vergleichsoperatoren des Aufzählungstyps vergleichen einfach die zugrunde liegenden ganzzahligen Werte der beiden Operanden.
Transformierte (§12.4.8) Formen der oben definierten vordefinierten Enumerationsvergleichsoperatoren sind ebenfalls vordefinierte Operatoren.
12.12.7 Gleichheitsoperatoren für Verweistypen
Jeder Klassentyp C
stellt implizit die folgenden vordefinierten Gleichheitsoperatoren für Referenztypen bereit.
bool operator ==(C x, C y);
bool operator !=(C x, C y);
sofern keine vordefinierten Gleichheitsoperatoren für C
vorhanden sind (z. B. wenn C
string
oder System.Delegate
ist).
Die Operatoren geben das Ergebnis des Vergleichs der beiden Verweise auf Gleichheit oder Ungleichheit zurück. operator ==
gibt true
zurück, wenn und nur wenn x
und y
sich auf die gleiche Instanz beziehen oder beide null
sind, während operator !=
true
zurückgibt, wenn und nur wenn operator ==
mit den gleichen Operanden false
zurückgeben würde.
Zusätzlich zu den normalen Anwendbarkeitsregeln (§12.6.4.2) erfordern die vordefinierten Referenztyp-Gleichheitsoperatoren eine der folgenden Bedingungen, um anwendbar zu sein:
- Beide Operanden sind ein Wert eines Typs, der als reference_type oder als Literal-
null
bekannt ist. Darüber hinaus gibt es eine Identitäts- oder explizite Verweiskonvertierung (§10.3.5) von einem der Operanden in den Typ des anderen Operanden. - Ein Operand ist das Literal
null
, und der andere Operand ist ein Wert vom TypT
, wobeiT
ein -Typparameter ist, der nicht als Werttyp bekannt ist und keine Werttypeinschränkung besitzt.- Wenn zur Laufzeit
T
ein nicht-nullbarer Wertetyp ist, ist das Ergebnis von==
false
und das Ergebnis von!=
true
. - Wenn zur Laufzeit
T
ein Nullwerttyp ist, wird das Ergebnis aus derHasValue
-Eigenschaft des Operanden berechnet, wie in (§12.12.10) beschrieben. - Sollte
T
zur Laufzeit ein Verweistyp sein, ist das Ergebnistrue
, wenn der Operandnull
ist, andernfallsfalse
.
- Wenn zur Laufzeit
Wenn eine dieser Bedingungen nicht wahr ist, tritt ein Bindungszeitfehler auf.
Anmerkung: Die wichtigsten Auswirkungen dieser Regeln sind:
- Es ist ein Bindungszeitfehler, die vordefinierten Gleichheitsoperatoren für Referenztypen zu verwenden, um zwei Verweise zu vergleichen, die zur Bindungszeit nachweislich unterschiedlich sind. Wenn es sich bei den Bindungszeittypen der Operanden beispielsweise um zwei Klassentypen handelt und keiner der beiden Typen vom anderen abgeleitet ist, dann können die beiden Operanden unmöglich auf dasselbe Objekt verweisen. Daher wird die Operation als Bindungszeitfehler betrachtet.
- Die vordefinierten v lassen keinen Vergleich von Werttypoperanden zu, außer wenn Typparameter mit
null
verglichen werden, was speziell behandelt wird.- Bei vorgegebenen Gleichheitsoperatoren für Verweistypen werden die Operanden nie geschachtelt. Es wäre bedeutungslos, solche Boxing-Operationen durchzuführen, da die Verweise auf die neu zugewiesenen Boxing-Instanzen zwangsläufig von allen anderen Verweisen abweichen würden.
Bei einer Operation der Form
x == y
oderx != y
, wenn benutzerdefinierteoperator ==
oderoperator !=
anwendbar sind, werden die Regeln der Operatorüberladungsauflösung (§12.4.5) diesen Operator anstelle des vordefinierten Gleichheitsoperators für Referenztypen auswählen. Es ist immer möglich, den vordefinierten Gleichheitsoperator für Verweistypen auszuwählen, indem einer oder beide Operanden explizit in den Typobject
umgewandelt werden.Hinweisende
Beispiel: Im folgenden Beispiel wird überprüft, ob ein Argument eines nicht eingeschränkten Typparametertyps
null
ist.class C<T> { void F(T x) { if (x == null) { throw new ArgumentNullException(); } ... } }
Das
x == null
-Konstrukt ist zulässig, obwohlT
einen nicht-nullable Werttyp darstellen kann, und das Ergebnis wird einfach alsfalse
definiert, wennT
ein nicht-nullable Werttyp ist.Ende des Beispiels
Bei einer Operation der Form x == y
oder x != y
, wenn benutzerdefinierte operator ==
oder operator !=
anwendbar sind, werden die Regeln der Operatorüberladungsauflösung (§12.4.5) diesen Operator anstelle des vordefinierten Gleichheitsoperators für Referenztypen auswählen.
Note: Es ist immer möglich, den vordefinierten Gleichheitsoperator für Verweistypen auszuwählen, indem einer oder beide Operanden explizit in den Typ
object
umgewandelt werden. Hinweisende
Beispiel: Das Beispiel
class Test { static void Main() { string s = "Test"; string t = string.Copy(s); Console.WriteLine(s == t); Console.WriteLine((object)s == t); Console.WriteLine(s == (object)t); Console.WriteLine((object)s == (object)t); } }
erzeugt die Ausgabe
True False False False
Die Variablen
s
undt
beziehen sich auf zwei verschiedene Instanzen von Zeichenfolgen, die dieselben Zeichen enthalten. Die erste Vergleichsausgabe istTrue
, weil der vordefinierte Zeichenfolgen-Gleichheitsoperator (§12.12.8) ausgewählt wird, wenn beide Operanden vom Typstring
sind. Die restlichen Vergleiche ergeben alleFalse
, da die Überladung vonoperator ==
im Typstring
nicht anwendbar ist, wenn ein Operand einen Bindungszeittyp vonobject
hat.Beachten Sie, dass die obige Technik für Wertetypen nicht sinnvoll ist. Das Beispiel
class Test { static void Main() { int i = 123; int j = 123; Console.WriteLine((object)i == (object)j); } }
gibt
False
aus, da die Umwandlungen Verweise auf zwei separate Instanzen von geschachteltenint
-Werten erstellen.Ende des Beispiels
12.12.8 Zeichenfolgen-Gleichheitsoperatoren
Die vordefinierten Zeichenfolgen-Gleichheitsoperatoren sind:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
Zwei string
Werte werden als gleich angesehen, wenn einer der folgenden Punkte zutrifft:
- Beide Werte sind
null
. - Beide Werte sind nicht-
null
-Verweise auf Zeichenfolgen, die identische Länge und dieselben Zeichen in jeder Zeichenposition haben.
Die Operatoren für die Zeichenfolgengleichheit vergleichen Zeichenfolgenwerte und nicht Zeichenfolgenreferenzen. Wenn zwei separate Instanzen von Zeichenketten genau dieselbe Sequenz von Zeichen enthalten, sind die Werte der Zeichenketten gleich, aber die Referenzen sind unterschiedlich.
Hinweis: Wie in §12.12.7 beschrieben, können die Gleichheitsoperatoren für Verweistypen verwendet werden, um Zeichenfolgenverweise anstelle von Zeichenfolgenwerten zu vergleichen. Hinweisende
12.12.9 Delegatgleichheitsoperatoren
Die vordefinierten Delegatgleichheitsoperatoren sind:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
Zwei Delegatinstanzen werden wie folgt als gleich angesehen:
- Wenn eine der Delegateninstanzen
null
ist, sind sie genau dann gleich, wenn beidenull
sind. - Wenn die Delegaten unterschiedliche Laufzeittypen haben, sind sie niemals gleich.
- Wenn beide Delegatinstanzen eine Aufrufliste haben (§20.2), sind diese Instanzen gleich, wenn und nur wenn ihre Aufruflisten die gleiche Länge aufweisen und jeder Eintrag in einer Aufrufliste in der Reihenfolge dem entsprechenden Eintrag in der anderen Aufrufliste gleich ist (wie unten definiert).
Die folgenden Regeln regeln die Gleichheit von Aufruflisteneinträgen:
- Wenn zwei Einträge in der Aufrufliste beide auf dieselbe statische Methode verweisen, sind die Einträge gleich.
- Wenn zwei Einträge in der Aufrufliste beide auf dieselbe nicht-statische Methode auf demselben Zielobjekt verweisen (wie durch die Referenzgleichheitsoperatoren definiert), dann sind die Einträge gleich.
- Aufruflisteneinträge aus der Auswertung semantisch identischer anonymer Funktionen (§12.19) mit demselben (möglicherweise leeren) Satz erfasster äußerer Variableninstanzen dürfen (aber müssen nicht) gleich sein.
Wenn die Operatorüberladungsauflösung in einen Delegatgleichstellungsoperator aufgelöst wird und die Bindungszeittypen beider Operanden Delegattypen sind, wie in §20 anstelle von System.Delegate
beschrieben, und es gibt keine Identitätskonvertierung zwischen den Bindungstyp-Operandentypen, tritt ein Bindungszeitfehler auf.
Hinweis: Diese Regel verhindert Vergleiche, die niemals Nicht-
null
-Werte als gleich betrachten können, da es Verweise auf Instanzen unterschiedlicher Typen von Delegaten sind. Hinweisende
12.12.10 Gleichheitsoperatoren zwischen Nullwertetypen und dem Nullliteral
Die Operatoren ==
und !=
lassen zu, dass ein Operand ein Wert eines Nullwerttyps ist, während der andere das null
-Literal ist, auch wenn für den Vorgang kein vordefinierter oder benutzerdefinierter Operator (in nicht-Lifted oder Lifted-Form) vorhanden ist.
Für einen Vorgang einer der Formen
x == null null == x x != null null != x
wenn x
ein Ausdruck eines Nullwerttyps ist und die Operatorüberladungsauflösung (§12.4.5) keinen anwendbaren Operator findet, wird das Ergebnis stattdessen aus der HasValue
-Eigenschaft von x
berechnet. Insbesondere werden die ersten beiden Formulare in !x.HasValue
übersetzt, und die letzten beiden Formulare werden in x.HasValue
übersetzt.
12.12.11 Tupelgleichheitsoperatoren
Die Tupelgleichheitsoperatoren werden paarweise auf die Elemente der Tupeloperanden in lexikalischer Reihenfolge angewendet.
Wenn jeder Operand x
und y
eines ==
oder !=
-Operators entweder als Tupel oder als Wert mit einem Tupeltyp klassifiziert wird (§8.3.11), ist der Operator ein Tupelgleichheitssoperator.
Wenn ein Operand e
als Tupel klassifiziert wird, sind die Elemente e1...en
die Ergebnisse der Auswertung Elementausdrücke des Tupelausdrucks. Andernfalls, wenn e
ein Wert eines Tupeltyps ist, sollen die Elemente t.Item1...t.Itemn
sein, wobei t
das Ergebnis der Auswertung von e
ist.
Die Operanden x
und y
eines Tupelgleichheitsoperators müssen dieselbe Arität haben, ansonsten tritt ein Kompilierfehler auf. Für jedes Paar von Elementen xi
und yi
gilt derselbe Gleichheitsoperator und liefert ein Ergebnis vom Typ bool
, dynamic
, einem Typ, der eine implizite Konversion nach bool
hat, oder einem Typ, der die Operatoren true
und false
definiert.
Der Tupelgleichheitsoperator x == y
wird wie folgt ausgewertet:
- Der linke Operand
x
wird ausgewertet. - Der rechte Operand
y
wird ausgewertet. - Für jedes Elementpaar
xi
undyi
in der lexikalischen Reihenfolge:- Der Operator
xi == yi
wird ausgewertet, und ein Ergebnis vom Typbool
wird auf folgende Weise erzielt:- Wenn der Vergleich
bool
ergeben hat, dann ist das das Ergebnis. - Andernfalls wird, wenn der Vergleich ein
dynamic
ergibt, der Operatorfalse
dynamisch darauf angewendet, und der daraus resultierendebool
-Wert wird mit dem logischen Negationsoperator (!
) negiert. - Andernfalls wird, wenn der Typ des Vergleichs eine implizite Konvertierung in
bool
hat, diese Konvertierung angewendet. - Andernfalls, wenn der Typ des Vergleichs einen Operator
false
hat, wird dieser Operator aufgerufen und der resultierendebool
-Wert mit dem logischen Negationsoperator (!
) negiert.
- Wenn der Vergleich
- Wenn das resultierende
bool
false
ist, findet keine weitere Auswertung statt, und das Ergebnis des Tupelgleichheitsoperators istfalse
.
- Der Operator
- Wenn alle Elementvergleiche
true
ergeben haben, ist das Ergebnis des Tupelgleichheitsoperatorstrue
.
Der Tupelgleichheitsoperator x != y
wird wie folgt ausgewertet:
- Der linke Operand
x
wird ausgewertet. - Der rechte Operand
y
wird ausgewertet. - Für jedes Elementpaar
xi
undyi
in der lexikalischen Reihenfolge:- Der Operator
xi != yi
wird ausgewertet, und ein Ergebnis vom Typbool
wird auf folgende Weise erzielt:- Wenn der Vergleich
bool
ergeben hat, dann ist das das Ergebnis. - Andernfalls, wenn der Vergleich ein
dynamic
ergibt, wird der Operatortrue
dynamisch darauf angewendet, und der resultierende Wertbool
ist das Ergebnis. - Andernfalls wird, wenn der Typ des Vergleichs eine implizite Konvertierung in
bool
hat, diese Konvertierung angewendet. - Wenn der Typ des Vergleichs den Operator
true
aufweist, wird dieser Operator aufgerufen, und der resultierendebool
-Wert ist das Ergebnis.
- Wenn der Vergleich
- Wenn das resultierende
bool
true
ist, findet keine weitere Auswertung statt, und das Ergebnis des Tupelgleichheitsoperators isttrue
.
- Der Operator
- Wenn alle Elementvergleiche
false
ergeben haben, ist das Ergebnis des Tupelgleichheitsoperatorsfalse
.
12.12.12 Der is-Operator
Es gibt zwei Formen des is
-Operators. Eine ist der is-Typoperator, der einen Typ auf der rechten Seite hat. Die andere ist der is-Musteroperator, der ein Muster auf der rechten Seite hat.
12.12.12.1 Der Is-Typoperator
Der is-type-Operator wird verwendet, um zu überprüfen, ob der Laufzeittyp eines Objekts mit einem angegebenen Typ kompatibel ist. Die Überprüfung wird zur Laufzeit durchgeführt. Das Ergebnis des Vorgangs E is T
, wobei E
ein Ausdruck ist und T
ein anderer Typ als dynamic
ist, ist ein boolescher Wert, der angibt, ob E
nicht null ist und erfolgreich in den Typ T
durch eine Verweiskonvertierung, eine Boxing-Konvertierung, eine Unboxing-Konvertierung, eine Umwandlungskonvertierung oder eine Entpackungskonvertierung konvertiert werden kann.
Die Operation wird wie folgt ausgewertet:
- Wenn
E
eine anonyme Funktion oder Methodengruppe ist, tritt ein Kompilierungszeitfehler auf. - Wenn
E
das Literalnull
ist, oder der Wert vonE
null
ist, wird das Ergebnisfalse
sein. - Andernfalls:
- Lassen Sie
R
der Laufzeittyp vonE
sein. - Lassen Sie
D
wie folgt vonR
ableiten: - Wenn
R
ein Nullwerttyp ist, istD
der zugrunde liegende Typ vonR
. - Andernfalls lautet
D
R
. - Das Ergebnis hängt von
D
undT
wie folgt ab: - Wenn
T
ein Verweistyp ist, ist das Ergebnistrue
, wenn gilt:- es eine Identitätskonvertierung zwischen
D
undT
gibt, D
ein Verweistyp uist und eine implizite Verweiskonvertierung vonD
inT
existiert, oder- Entweder:
D
ist ein Werttyp und eine Boxing-Konvertierung vonD
inT
ist vorhanden.
Oder:D
ist ein Wertetyp undT
ist ein Schnittstellentyp, der durchD
implementiert wird.
- es eine Identitätskonvertierung zwischen
- Wenn
T
ein nicht löschbarer Wertetyp ist, ist das Ergebnistrue
, wennD
der zugrunde liegende Typ vonT
ist. - Wenn
T
ein nicht-nullbarer Wertetyp ist, ist das Ergebnistrue
, wennD
undT
der gleiche Typ sind. - Andernfalls ist das Ergebnis
false
.
Benutzerdefinierte Konversionen werden von dem is
-Operator nicht berücksichtigt.
Anmerkung: Da der
is
-Operator zur Laufzeit ausgewertet wird, wurden alle Typ-Argumente ersetzt und es gibt keine offenen Typen (§8.4.3), die berücksichtigt werden müssen. Hinweisende
Anmerkung: Der Operator
is
kann in Bezug auf Typen bei der Kompilierung und Konversionen wie folgt verstanden werden, wobeiC
der Typ bei der Kompilierung vonE
ist:
- Wenn der Kompilierungszeittyp von
e
mitT
identisch ist oder eine implizite Verweiskonvertierung (§10.2.8), Boxing-Konvertierung (§10.2.9), Umbruchkonvertierung (§10.6) oder eine explizite Entpackungskonvertierung (§10.6) vom KompilierungszeittypE
inT
existiert:
- Wenn
C
von einem nicht-nullbaren Werttyp ist, ist das Ergebnis des Vorgangstrue
.- Andernfalls entspricht das Ergebnis des Vorgangs der Auswertung von
E != null
.- Andernfalls, wenn eine explizite Verweiskonvertierung (§10.3.5) oder eine Unboxing-Konvertierung (§10.3.7) von
C
inT
vorhanden ist oderC
oderT
ein offener Typ ist (§8.4.3), werden Laufzeitüberprüfungen wie oben beschrieben durchgeführt.- Andernfalls sind keine Referenz-, Boxing-, Umbruch- oder Unwrapping-Konvertierungen von
E
zum TypT
möglich, und das Ergebnis des Vorgangs istfalse
. Ein Compiler kann auf der Grundlage des Typs bei der Kompilierung Optimierungen implementieren.Hinweisende
12.12.12.2 Der is-Musteroperator
Der is-Musteroperator wird verwendet, um zu überprüfen, ob der von einem Ausdruck berechnete Wert Wert einem bestimmten Muster entspricht (§11). Die Überprüfung wird zur Laufzeit durchgeführt. Das Ergebnis des Operators is-pattern ist wahr, wenn der Wert mit dem Muster übereinstimmt; andernfalls ist es falsch.
Bei einem Ausdruck der Form E is P
, wobei E
ein relationaler Ausdruck vom Typ T
und P
ein Muster ist, handelt es sich um einen Kompilierungszeitfehler, wenn eine der folgenden Bedingungen erfüllt ist:
-
E
bezeichnet keinen Wert oder hat keinen Typ. - Das Muster
P
ist nicht anwendbar (§11.2) auf den TypT
.
12.12.13 Der as-Operator
Der Operator as
wird verwendet, um einen Wert explizit in einen bestimmten Referenztyp oder nullbaren Werttyp zu konvertieren. Im Gegensatz zum Cast-Ausdruck (§12.9.7), löst der as
-Operator nie eine Ausnahme aus. Wenn die angegebene Konversion nicht möglich ist, ist der resultierende Wert stattdessen null
.
Bei einer Vorgang der Form E as T
muss E
ein Ausdruck sein und T
muss ein Referenztyp, ein Typparameter, der als Referenztyp bekannt ist, oder ein Nullwerttyp sein. Außerdem muss mindestens eine der folgenden Bedingungen erfüllt sein, andernfalls tritt ein Kompilierfehler auf:
- Eine Identitätskonvertierung (§10.2.2), eine implizite Nullwertkonvertierung (§10.2.6), ein impliziter Verweis (§10.2.8), eine Boxing-Konvertierung (§10.2.9), eine explizite Nullwertkonvertierung (§10.3.4), ein expliziter Verweis (§10.3.5) oder eine Wrapping-Konvertierung (§8.3.12) von
E
inT
ist vorhanden. - Der Typ von
E
oderT
ist ein offener Typ. -
E
ist dasnull
-Literal.
Wenn der Typ bei der Kompilierung von E
nicht dynamic
ist, liefert die Operation E as T
das gleiche Ergebnis wie
E is T ? (T)(E) : (T)null
außer dass E
nur einmal überprüft wird. Es wird erwartet, dass ein Compiler E as T
so optimiert, dass höchstens eine Laufzeittypüberprüfung durchgeführt wird, im Vergleich zu den zwei Laufzeittypüberprüfungen, die durch die oben genannte Erweiterung impliziert werden.
Wenn der Kompilierungszeittyp von E
dynamic
ist, ist der as
-Operator im Gegensatz zum Umwandlungsoperator nicht dynamisch gebunden (§12.3.3). Daher lautet die Erweiterung in diesem Fall:
E is T ? (T)(object)(E) : (T)null
Beachten Sie, dass einige Konvertierungen, z. B. benutzerdefinierte Konvertierungen, nicht mit dem as
-Operator möglich sind und stattdessen mithilfe von Umwandlungsausdrücken ausgeführt werden sollten.
Beispiel: Im Beispiel
class X { public string F(object o) { return o as string; // OK, string is a reference type } public T G<T>(object o) where T : Attribute { return o as T; // Ok, T has a class constraint } public U H<U>(object o) { return o as U; // Error, U is unconstrained } }
der Typparameter
T
vonG
ist bekanntlich ein Referenztyp, da er die Klassenbeschränkung hat. Der TypparameterU
vonH
ist es jedoch nicht; daher ist die Verwendung desas
-Operators inH
unzulässig.Ende des Beispiels
12.13 Logische Operatoren
12.13.1 Allgemein
Die Operatoren &
, ^
und |
werden als logische Operatoren bezeichnet.
and_expression
: equality_expression
| and_expression '&' equality_expression
;
exclusive_or_expression
: and_expression
| exclusive_or_expression '^' and_expression
;
inclusive_or_expression
: exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression
;
Wenn ein Operand eines logischen Operators den Typ bei der Kompilierung dynamic
hat, dann ist der Ausdruck dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Ausdrucks dynamic
, und die unten beschriebene Auflösung erfolgt zur Laufzeit mithilfe des Laufzeittyps dieser Operanden, die den Kompilierungszeittyp dynamic
haben.
Für eine Operation der Form x «op» y
, wobei „op“ einer der logischen Operatoren ist, wird die Überladungsauflösung (§12.4.5) angewendet, um eine bestimmte Implementierung des Operators auszuwählen. Die Operanden werden in die Parametertypen des gewählten Operators umgewandelt, und der Typ des Ergebnisses ist der Rückgabetyp des Operators.
Die vordefinierten logischen Operatoren werden in den folgenden Unterklauseln beschrieben.
12.13.2 Logische Ganzzahl-Operatoren
Die vordefinierten ganzzahligen logischen Operatoren sind:
int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);
int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);
int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);
Der &
-Operator berechnet das bitweise logische „AND” der beiden Operanden, der |
-Operator berechnet das bitweise logische „OR” der beiden Operanden, und der ^
-Operator berechnet das bitweise logische exklusive „OR” der beiden Operanden. Bei diesen Operationen sind keine Überläufe möglich.
Lifted (§12.4.8) Formen der oben definierten vordefinierten logischen Ganzzahloperatoren sind ebenfalls vordefinierte Operatoren.
12.13.3 Logische Operatoren für Auflistungen
Jeder Enumerationstyp E
stellt implizit die folgenden vordefinierten logischen Operatoren bereit:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
Das Ergebnis der Auswertung von x «op» y
, wobei x
und y
Ausdrücke eines Enumerationstyps E
mit einem zugrunde liegenden Typ U
sind und «op» einer der logischen Operatoren ist, ist identisch mit der Auswertung von (E)((U)x «op» (U)y)
. Mit anderen Worten führen die logischen Operatoren des Enumerationstyps einfach den logischen Vorgang für den zugrunde liegenden Typ der beiden Operanden aus.
Lifted (§12.4.8) Formen der oben definierten vordefinierten logischen Enumerationsoperatoren sind ebenfalls vordefinierte Operatoren.
12.13.4 Logische Operatoren für Booleans
Die vordefinierten logischen Operatoren für Booleans sindd:
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
Das Ergebnis von x & y
ist true
, wenn sowohl x
als auch y
zu true
ausgewertet werden. Andernfalls ist das Ergebnis false
.
Das Ergebnis von x | y
ist true
, wenn entweder x
oder y
true
ist. Andernfalls ist das Ergebnis false
.
Das Ergebnis von x ^ y
ist true
, wenn x
true
und y
false
ist, oder x
ist false
und y
ist true
. Andernfalls ist das Ergebnis false
. Wenn die Operanden vom Typ bool
sind, berechnet der ^
Operator das gleiche Ergebnis wie der !=
Operator.
12.13.5 Boolesche Nullwertoperatoren & „|”-Operatoren
Der nullable boolesche Typ bool?
kann drei Werte, true
, false
und null
darstellen.
Wie bei den anderen binären Operatoren sind auch die erhobenen Formen der logischen Operatoren &
und |
(§12.13.4) vordefiniert.
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
Die Semantik der Lifted-Operatoren &
und |
wird in der folgenden Tabelle definiert.
x |
y |
x & y |
x \| y |
---|---|---|---|
true |
true |
true |
true |
true |
false |
false |
true |
true |
null |
null |
true |
false |
true |
false |
true |
false |
false |
false |
false |
false |
null |
false |
null |
null |
true |
null |
true |
null |
false |
false |
null |
null |
null |
null |
null |
Anmerkung: Der Typ
bool?
ist konzeptionell ähnlich dem dreiwertigen Typ, der für boolesche Ausdrücke in SQL verwendet wird. Die obige Tabelle folgt der gleichen Semantik wie SQL, wohingegen es bei der Anwendung der Regeln von §12.4.8 auf die Operatoren&
und|
nicht der Fall wäre. Die Regeln von §12.4.8 stellen bereits SQL-ähnliche Semantik für den Lifted-Operator^
bereit. Hinweisende
12.14 Bedingte logische Operatoren
12.14.1 Allgemein
Die Operatoren &&
und ||
werden als bedingte logische Operatoren bezeichnet. Sie werden auch als „kurzschließende” logische Operatoren bezeichnet.
conditional_and_expression
: inclusive_or_expression
| conditional_and_expression '&&' inclusive_or_expression
;
conditional_or_expression
: conditional_and_expression
| conditional_or_expression '||' conditional_and_expression
;
Die &&
und ||
Operatoren sind bedingte Versionen der &
und |
Operatoren:
- Die Operation
x && y
entspricht der Operationx & y
, mit der Ausnahme, dassy
nur ausgewertet wird, wennx
nichtfalse
ist. - Die Operation
x || y
entspricht der Operationx | y
, mit der Ausnahme, dassy
nur ausgewertet wird, wennx
nichttrue
ist.
Hinweis: Der Grund dafür, dass beim Kurzschließen die Bedingungen „nicht wahr“ und „nicht falsch“ verwendet werden, besteht darin, dass benutzerdefinierte bedingte Operatoren festlegen können, wann das Kurzschließen angewendet wird. Benutzerdefinierte Typen können sich in einem Status befinden, in dem
operator true
false
undoperator false
false
ergibt. In diesen Fällen würde weder&&
noch||
einen Kurzschluss verursachen. Hinweisende
Wenn ein Operand eines bedingten logischen Operators den Typ bei der Kompilierung dynamic
hat, dann wird der Ausdruck dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Ausdrucks dynamic
, und die unten beschriebene Auflösung erfolgt zur Laufzeit mithilfe des Laufzeittyps dieser Operanden, die den Kompilierungszeittyp dynamic
haben.
Ein Vorgang der Form x && y
oder x || y
wird durch Anwenden der binären Operatorüberladungsauflösung (§12.4.5) so verarbeitet, als wäre der Vorgang als x & y
oder x | y
geschrieben. Dies ergibt folgende Szenarien:
- Wenn die Überladungsauflösung keinen einzigen besten Operator findet oder wenn die Überladungsauflösung einen der vordefinierten logischen Operatoren für ganze Zahlen oder boolesche logische Nullwertoperatoren (§12.13.5) auswählt, tritt ein Bindungszeitfehler auf.
- Andernfalls, wenn der ausgewählte Operator einer der vordefinierten booleschen logischen Operatoren (§12.13.4) ist, wird der Vorgang wie in §12.14.2beschrieben verarbeitet.
- Andernfalls ist der ausgewählte Operator ein benutzerdefinierter Operator, und die Operation wird wie in §12.14.3 beschrieben verarbeitet.
Es ist nicht möglich, die bedingten logischen Operatoren direkt zu überladen. Da die bedingten logischen Operatoren jedoch in Bezug auf die regulären logischen Operatoren ausgewertet werden, werden Überladungen der regulären logischen Operatoren mit gewissen Einschränkungen auch als Überladungen der bedingten logischen Operatoren betrachtet. Dies wird in §12.14.3 näher beschrieben.
12.14.2 Boolesche bedingte logische Operatoren
Wenn die Operanden von &&
oder ||
vom Typ bool
sind, oder wenn die Operanden von Typen sind, die kein anwendbares operator &
oder operator |
definieren, aber implizite Konversionen nach bool
definieren, wird die Operation wie folgt verarbeitet:
- Der Vorgang
x && y
wird alsx ? y : false
ausgewertet. Mit anderen Worten,x
wird zuerst ausgewertet und in Typbool
konvertiert. Wennx
true
ist, wirdy
ausgewertet und in Typbool
konvertiert, und dies wird zum Ergebnis des Vorgangs. Anderenfalls ist das Ergebnis des Vorgangsfalse
. - Der Vorgang
x || y
wird alsx ? true : y
ausgewertet. Mit anderen Worten,x
wird zuerst ausgewertet und in Typbool
konvertiert. Wennx
true
ist, wird das Ergebnis des Vorgangstrue
. Andernfals wirdy
ausgewertet und in Typbool
konvertiert, und dies wird zum Ergebnis des Vorgangs.
12.14.3 Benutzerdefinierte bedingte logische Operatoren
Wenn die Operanden von &&
oder ||
die Typen aufweisen, die eine anwendbare benutzerdefinierte operator &
oder operator |
definieren, sollen beide der folgenden Bedingungen erfüllt sein, wobei T
der Typ ist, in dem der ausgewählte Operator definiert wird:
- Der Rückgabetyp und der Typ der einzelnen Parameter des ausgewählten Operators sollen
T
sein. Mit anderen Worten, der Operator berechnet das logische UND oder das logische ODER von zwei Operanden vom TypT
und gibt ein Ergebnis vom TypT
zurück. T
soll Deklarationen vonoperator true
undoperator false
enthalten.
Ein Bindungszeitfehler tritt auf, wenn eine dieser Bedingungen nicht erfüllt ist. Andernfalls wird der &&
- oder ||
-Vorgang ausgewertet, indem der benutzerdefinierte operator true
oder operator false
mit dem ausgewählten benutzerdefinierten Operator kombiniert wird:
- Die Operation
x && y
wird alsT.false(x) ? x : T.&(x, y)
ausgewertet, wobeiT.false(x)
ein Aufruf des inT
deklariertenoperator false
ist, undT.&(x, y)
ein Aufruf des ausgewähltenoperator &
ist. Mit anderen Worten,x
wird zuerst ausgewertet, undoperator false
wird auf das Ergebnis angewendet, um zu prüfen, obx
definitiv falsch ist. Wennx
dann definitiv falsch ist, ist das Ergebnis der Operation der zuvor fürx
berechnete Wert. Andernfalls wirdy
ausgewertet, und der ausgewählteoperator &
wird auf den berechneten Wert fürx
und füry
angewendet, um das Ergebnis des Vorgangs zu erzielen. - Die Operation
x || y
wird alsT.true(x) ? x : T.|(x, y)
ausgewertet, wobeiT.true(x)
ein Aufruf des inT
deklariertenoperator true
ist, undT.|(x, y)
ein Aufruf des ausgewähltenoperator |
ist. Mit anderen Worten,x
wird zuerst ausgewertet undoperator true
wird auf das Ergebnis angewendet, um zu prüfen, obx
definitiv wahr ist. Wenn dannx
definitiv wahr ist, ist das Ergebnis der Operation der zuvor fürx
berechnete Wert. Andernfalls wirdy
ausgewertet, und der ausgewählteoperator |
wird auf den berechneten Wert fürx
und füry
angewendet, um das Ergebnis des Vorgangs zu erzielen.
In einem dieser Vorgänge wird der von x
angegebene Ausdruck nur einmal ausgewertet, und der von y
angegebene Ausdruck wird entweder nicht ausgewertet oder genau einmal ausgewertet.
12.15 Der Null-Sammeloperator
Der Operator ??
wird als Null-Koaleszenz-Operator bezeichnet.
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
| throw_expression
;
In einem Null-Sammelausdruck der Form a ?? b
, wenn a
nichtnull
ist, ist das Ergebnis a
; andernfalls ist das Ergebnis b
. Der Vorgang wertet b
nur aus, wenn a
null
ist.
Der Null-Sammeloperator ist rechtsassoziativ, d. h. dass die Vorgänge von rechts nach links gruppiert werden.
Beispiel: Ein Ausdruck in der Form
a ?? b ?? c
wird alsa ?? (b ?? c)
ausgewertet. Im Allgemeinen gibt ein Ausdruck der FormE1 ?? E2 ?? ... ?? EN
den ersten Operanden zurück, der nichtnull
ist, odernull
, wenn alle Operandennull
sind. Ende des Beispiels
Der Typ des Ausdrucks a ?? b
hängt davon ab, welche impliziten Konversionen für die Operanden verfügbar sind. In der empfohlenen Reihenfolge ist der Typ von a ?? b
A₀
, A
oder B
, wobei A
der Typ von a
ist (sofern a
einen Typ aufweist), B
der Typ von b
ist (vorausgesetzt, dass b
einen Typ aufweist), und A₀
der zugrunde liegende Typ von A
ist, wenn A
ein Nullwerttyp ist, oder andernfalls A
. Insbesondere wird a ?? b
wie folgt verarbeitet:
- Wenn
A
vorhanden ist und kein Nullwert- oder Verweistyp ist, tritt ein Kompilierungsfehler auf. - Andernfalls, wenn
A
existiert undb
ein dynamischer Ausdruck ist, ist der Ergebnistypdynamic
. Während der Laufzeit wird zunächsta
ausgewertet. Wenna
nichtnull
ist, wirda
indynamic
umgewandelt und dies wird das Ergebnis. Andernfalls wirdb
ausgewertet, und dies ergibt das Ergebnis. - Andernfalls, wenn
A
vorhanden ist und ein Nullwerttyp ist und eine implizite Konvertierung vonb
inA₀
vorhanden ist, wird der ErgebnistypA₀
. Während der Laufzeit wird zunächsta
ausgewertet. Wenna
nichtnull
ist, wirda
in den TypA₀
entpackt, und dies ergibt das Ergebnis. Andernfalls wirdb
ausgewertet und in den TypA₀
konvertiert, und dies ergibt das Ergebnis. - Andernfalls, wenn
A
existiert und eine implizite Umwandlung vonb
zuA
existiert, ist der ErgebnistypA
. Während der Laufzeit wird zunächsta
ausgewertet. Wenna
nichtnull
ist, wirda
zum Ergebnis. Andernfalls wirdb
ausgewertet und in den TypA
konvertiert, und dies ergibt das Ergebnis. - Andernfalls, wenn
A
existiert und ein Nullwerttyp ist,b
den TypB
hat und eine implizite Konvertierung vonA₀
zuB
existiert, dann ist der ErgebnistypB
. Während der Laufzeit wird zunächsta
ausgewertet. Wenna
nichtnull
ist, wirda
zu TypA₀
entpackt und in TypB
konvertiert, und dies ergibt das Ergebnis. Andernfalls wirdb
ausgewertet und dies ergibt das Ergebnis. - Andernfalls, wenn
b
einen TypB
hat und eine implizite Konvertierung vona
inB
aufweist, ist das ErgebnistypB
. Während der Laufzeit wird zunächsta
ausgewertet. Wenna
nichtnull
ist, wirda
in den TypB
umgewandelt und dies wird das Ergebnis. Andernfalls wirdb
ausgewertet und dies ergibt das Ergebnis.
Andernfalls sind a
und b
nicht kompatibel und es tritt ein Kompilierfehler auf.
12.16 Der throw-Ausdrucksoperator
throw_expression
: 'throw' null_coalescing_expression
;
Ein throw_expression löst den durch die Auswertung des null_coalescing_expression erzeugten Werts aus. Der Ausdruck soll implizit in System.Exception
konvertierbar sein, und das Ergebnis der Auswertung des Ausdrucks wird in System.Exception
umgewandelt, bevor es ausgelöst wird. Das Laufzeitverhalten der Auswertung eines throw-Ausdrucks entspricht dem für eine throw-Anweisung (§13.10.6).
Ein throw_expression hat keinen Typ. Ein throw_expression ist für jeden Typ durch eine implizite throw-Konvertierung konvertierbar.
Ein throw-Ausdruck darf nur in den folgenden syntaktischen Kontexten vorkommen:
- Als zweiter oder dritter Operand eines ternären bedingten Operators (
?:
). - Als zweiter Operand eines Null-Sammeloperators (
??
). - Als Textkörper einer Ausdruckskörperlambda- oder -members.
12.17 Deklarationsusdrücke
Ein Ausdruck für eine Deklaration deklariert eine lokale Variable.
declaration_expression
: local_variable_type identifier
;
local_variable_type
: type
| 'var'
;
Der simple_name_
wird auch als Deklarationsausdruck betrachtet, wenn eine einfache Namenssuche keine zugeordnete Deklaration gefunden hat (§12.8.4). Wenn sie als Deklarationsausdruck verwendet wird, wird _
als einfacher Ausschuss bezeichnet. Er ist semantisch äquivalent zu var _
, ist aber an mehr Stellen erlaubt.
Ein Deklarationsausdruck darf nur in den folgenden syntaktischen Kontexten vorkommen:
- Als
out
Argumentwert in einer Argumentliste. - Als einfacher Ausschuss
_
, welcher die linke Seite einer einfachen Zuweisung umfasst (§12.21.2). - Als tuple_element in einem oder mehreren rekursiv geschachtelten tuple_expressions, dessen äußerste Ebene die linke Seite einer Dekonstruktionszuweisung bildet. Ein deconstruction_expression führt zu Deklarationsausdrücken an dieser Position, obwohl die Deklarationsausdrücke syntaktisch nicht vorhanden sind.
Hinweis: Dies bedeutet, dass ein Deklarationsausdruck nicht in Klammern gesetzt werden kann. Hinweisende
Es handelt sich um einen Fehler für eine implizit typierte Variable, die mit einem declaration_expression deklariert wird, auf die innerhalb der argument_list verwiesen werden soll, in der sie deklariert wird.
Es ist ein Fehler, wenn eine mit einem declaration_expression deklarierte Variable innerhalb der destrukturierenden Zuordnung, in der sie auftritt, referenziert wird.
Ein Deklarationsausdruck, der ein einfacher Ausschuss ist oder bei dem der local_variable_type der Bezeichner var
ist, wird als implizit typisierte-Variable klassifiziert. Der Ausdruck hat keinen Typ, und der Typ der lokalen Variablen wird wie folgt aus dem syntaktischen Kontext abgeleitet:
- In einer argument_list ist der abgeleitete Typ der Variablen der deklarierte Typ des entsprechenden Parameters.
- Als linke Seite einer einfachen Zuweisung entspricht der abgeleitete Typ der Variablen dem Typ der rechten Seite der Zuweisung.
- In einem tuple_expression auf der linken Seite einer einfachen Zuweisung entspricht der abgeleitete Typ der Variablen dem Typ des entsprechenden Tupelelements auf der rechten Seite der Zuweisung (nach der Dekonstruktion).
Andernfalls wird der Deklarationsausdruck als explizit typisierte Variable klassifiziert, und der Typ des Ausdrucks sowie die deklarierte Variable müssen durch den local_variable_type angegeben werden.
Ein Deklarationsausdruck mit dem Bezeichner _
ist ein Ausschuss (§9.2.9.2), und führt keinen Namen für die Variable ein. Ein Deklarationsausdruck mit einem anderen Bezeichner als _
führt diesen Namen in den nächstgelegenen umschließenden lokalen Variablendeklarationsraum ein (§7.3).
Beispiel:
string M(out int i, string s, out bool b) { ... } var s1 = M(out int i1, "One", out var b1); Console.WriteLine($"{i1}, {b1}, {s1}"); // Error: i2 referenced within declaring argument list var s2 = M(out var i2, M(out i2, "Two", out bool b2), out b2); var s3 = M(out int _, "Three", out var _);
Die Deklaration von
s1
zeigt sowohl explizite als auch implizit typisierte Deklarationsausdrücke an. Der abgeleitete Typ vonb1
istbool
, denn das ist der Typ des entsprechenden Ausgabeparameters inM1
. Die nachfolgendeWriteLine
kann aufi1
undb1
zugreifen, die in den eingeschlossenen Bereich eingeführt wurden.Die Deklaration von
s2
zeigt einen Versuch,i2
im geschachtelten Aufruf vonM
zu verwenden, was unzulässig ist, da der Verweis in der Argumentliste auftritt, in deri2
deklariert wurde. Andererseits ist der Verweis aufb2
im letzten Argument zulässig, weil er nach dem Ende der eingebetteten Argumentliste erfolgt, in derb2
deklariert wurde.Die Deklaration von
s3
zeigt die Verwendung von sowohl implizit als auch explizit typisierten Deklarationsausdrücken, die verworfen werden. Da Ausschüsse keine benannte Variable deklarieren, sind die mehrfachen Vorkommen des Bezeichners_
zulässig.(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);
Dieses Beispiel zeigt die Verwendung von impliziten und explizit eingegebenen Deklarationsausdrücken für Variablen und Ausschüsse in einer destrukturierenden Zuweisung. Der simple_name
_
entsprichtvar _
, wenn keine Deklaration von_
gefunden wird.void M1(out int i) { ... } void M2(string _) { M1(out _); // Error: `_` is a string M1(out var _); }
In diesem Beispiel wird die Verwendung von
var _
gezeigt, um einen implizit typisierten Ausschuss bereitzustellen, wenn_
nicht verfügbar ist, da es eine Variable im übergeordneten Bereich bezeichnet.Ende des Beispiels
12.18 Bedingter Operator
Der ?:
-Operator wird als bedingter Operator bezeichnet. Diese werden manchmal auch als ternäre Operatoren bezeichnet.
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
| null_coalescing_expression '?' 'ref' variable_reference ':'
'ref' variable_reference
;
Ein throw-Ausdruck (§12.16) ist in einem bedingten Operator nicht zulässig, wenn ref
vorhanden ist.
Ein bedingter Ausdruck in der Form b ? x : y
wertet zuerst die Bedingung b
aus. Dann, wenn b
true
ist, wird x
ausgewertet und wird zum Ergebnis des Vorgangs. Andernfalls wird y
ausgewertet und wird zum Ergebnis des Vorgangs. Ein bedingter Ausdruck wertet niemals sowohl x
als auch y
aus.
Der bedingte Operator ist rechtsassoziativ, d. h. dass die Vorgänge von rechts nach links gruppiert werden.
Beispiel: Ein Ausdruck in der Form
a ? b : c ? d : e
wird alsa ? b : (c ? d : e)
ausgewertet. Ende des Beispiels
Der erste Operand des ?:
-Operators muss ein Ausdruck sein, der implizit in bool
umgewandelt werden kann, oder ein Ausdruck eines Typs, der operator true
implementiert. Wenn keine dieser beiden Bedingungen erfüllt ist, tritt ein Kompilierfehler auf.
Wenn ref
vorhanden ist:
- Eine Identitätskonvertierung muss zwischen den Typen der beiden variable_references bestehen, und der Typ des Ergebnisses kann einer der beiden Typen sein. Wenn einer der Typen
dynamic
ist, bevorzugt die Typableitungdynamic
(§8.7). Wenn einer der Typen ein Tupeltyp ist (§8.3.11), bezieht die Typableitung die Elementnamen ein, wenn die Elementnamen in derselben Reihenfolge in beiden Tupeln übereinstimmen. - Das Ergebnis ist ein Variablenverweis, der schreibbar ist, wenn beide Variablenverweise schreibbar sind.
Hinweis: Wenn
ref
vorhanden ist, gibt der conditional_expression einen Variablenverweis zurück, der einer Referenzvariable mithilfe des= ref
-Operators zugewiesen oder als Verweis-/Eingabe-/Ausgabeparameter übergeben werden kann. Hinweisende
Wenn ref
nicht vorhanden ist, steuern die zweiten und dritten Operanden, x
und y
des ?:
-Operators den Typ des bedingten Ausdrucks:
- Wenn
x
den TypX
undy
den TypY
hat, dann,- Wenn eine Identitätskonvertierung zwischen
X
undY
vorhanden ist, dann ist das Ergebnis der beste gemeinsame Typ einer Gruppe von Ausdrücken (§12.6.3.15). Wenn einer der Typendynamic
ist, bevorzugt die Typableitungdynamic
(§8.7). Wenn einer der Typen ein Tupeltyp ist (§8.3.11), bezieht die Typableitung die Elementnamen ein, wenn die Elementnamen in derselben Reihenfolge in beiden Tupeln übereinstimmen. - Andernfalls, wenn eine implizite Konversion (§10.2) von
X
nachY
, aber nicht vonY
nachX
existiert, dann istY
der Typ des bedingten Ausdrucks. - Andernfalls, wenn eine implizite Enumerationskonvertierung (§10.2.4) von
X
nachY
vorhanden ist, istY
der Typ des bedingten Ausdrucks. - Andernfalls, wenn eine implizite Enumerationskonvertierung (§10.2.4) von
Y
nachX
vorhanden ist, istX
der Typ des bedingten Ausdrucks. - Andernfalls, wenn eine implizite Konversion (§10.2) von
Y
nachX
, aber nicht vonX
nachY
existiert, dann istX
der Typ des bedingten Ausdrucks. - Andernfalls kann kein Typ des Ausdrucks bestimmt werden und es tritt ein Kompilierfehler auf.
- Wenn eine Identitätskonvertierung zwischen
- Wenn nur eines von
x
undy
einen Typ hat und sowohlx
als auchy
implizit in diesen Typ konvertierbar sind, dann ist das der Typ des bedingten Ausdrucks. - Andernfalls kann kein Typ des Ausdrucks bestimmt werden und es tritt ein Kompilierfehler auf.
Die Laufzeitverarbeitung eines bedingten Bezugsausdrucks der Form b ? ref x : ref y
besteht aus den folgenden Schritten:
- Zunächst wird
b
ausgewertet, und derbool
Wert vonb
wird bestimmt:- Wenn eine implizite Konversion vom Typ
b
nachbool
existiert, dann wird diese implizite Konversion durchgeführt, um einenbool
Wert zu erzeugen. - Andernfalls wird
operator true
, der vom Typb
definiert ist, aufgerufen, um einenbool
-Wert zu erzeugen.
- Wenn eine implizite Konversion vom Typ
- Wenn der Wert
bool
, der durch den obigen Schritt erzeugt wurde,true
ist, wirdx
ausgewertet, und der resultierende Variablenverweis wird zum Ergebnis des bedingten Ausdrucks. - Andernfalls wird
y
ausgewertet, und der resultierende Variablenverweis wird zum Ergebnis des bedingten Ausdrucks.
Die Laufzeitverarbeitung eines bedingten Ausdrucks der Form b ? x : y
besteht aus den folgenden Schritten:
- Zunächst wird
b
ausgewertet, und derbool
Wert vonb
wird bestimmt:- Wenn eine implizite Konversion vom Typ
b
nachbool
existiert, dann wird diese implizite Konversion durchgeführt, um einenbool
Wert zu erzeugen. - Andernfalls wird
operator true
, der vom Typb
definiert ist, aufgerufen, um einenbool
-Wert zu erzeugen.
- Wenn eine implizite Konversion vom Typ
- Wenn der im obigen Schritt erzeugte
bool
-Werttrue
ist, wirdx
ausgewertet und in den Typ des bedingten Ausdrucks konvertiert, und dies wird zum Ergebnis des bedingten Ausdrucks. - Andernfalls wird
y
ausgewertet und in den Typ des bedingten Ausdrucks konvertiert, und dies wird zum Ergebnis des bedingten Ausdrucks.
12.19 Anonyme Funktionsausdrücke
12.19.1 Allgemein
Eine anonyme Funktion ist ein Ausdruck, der eine "Inline"-Methodendefinition darstellt. Eine anonyme Funktion hat an sich weder einen Wert noch einen Typ, kann jedoch in einen kompatiblen Delegat- oder Ausdrucksbaumtyp konvertiert werden. Die Auswertung einer Umwandlung einer anonymen Funktion hängt vom Zieltyp der Konvertierung ab: Wenn es sich um einen Delegattyp handelt, wird die Konvertierung zu einem Delegatwert ausgewertet, der auf die von der anonymen Funktion definierte Methode verweist. Wenn es sich um einen Ausdruckstyp handelt, wird die Konvertierung in einen Ausdrucksbaum ausgewertet, der die Struktur der Methode als Objektstruktur darstellt.
Hinweis: Aus historischen Gründen gibt es zwei syntaktische Varianten anonymer Funktionen, nämlich lambda_expressions und anonymous_method_expressions. Für fast alle Zwecke sind lambda_expressions prägnanter und ausdrucksstarker als anonymous_method_expressions, die in der Sprache zur Abwärtskompatibilität verbleiben. Hinweisende
lambda_expression
: 'async'? anonymous_function_signature '=>' anonymous_function_body
;
anonymous_method_expression
: 'async'? 'delegate' explicit_anonymous_function_signature? block
;
anonymous_function_signature
: explicit_anonymous_function_signature
| implicit_anonymous_function_signature
;
explicit_anonymous_function_signature
: '(' explicit_anonymous_function_parameter_list? ')'
;
explicit_anonymous_function_parameter_list
: explicit_anonymous_function_parameter
(',' explicit_anonymous_function_parameter)*
;
explicit_anonymous_function_parameter
: anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'ref'
| 'out'
| 'in'
;
implicit_anonymous_function_signature
: '(' implicit_anonymous_function_parameter_list? ')'
| implicit_anonymous_function_parameter
;
implicit_anonymous_function_parameter_list
: implicit_anonymous_function_parameter
(',' implicit_anonymous_function_parameter)*
;
implicit_anonymous_function_parameter
: identifier
;
anonymous_function_body
: null_conditional_invocation_expression
| expression
| 'ref' variable_reference
| block
;
Wenn ein anonymous_function_body erkannt wird und die beiden Alternativen null_conditional_invocation_expression und expression sind anwendbar, dann wird die erste ausgewählt.
Hinweis: Die Überschneidung und Priorität zwischen den hier aufgeführten Alternativen dient ausschließlich der beschreibenden Zweckmäßigkeit; die Grammatikregeln könnten zum Entfernen der Überlappung ausgearbeitet werden. ANTLR und andere Grammatiksysteme adaptieren die gleiche Bequemlichkeit und so hat anonymous_function_body automatisch die angegebene Semantik. Hinweisende
Hinweis: Bei der Behandlung von expression wäre eine syntaktische Form wie z.B.
x?.M()
ein Fehler, wenn der Ergebnistyp vonM
void
ist (§12.8.13). Bei der Behandlung als null_conditional_invocation_expression darf der Ergebnistyp jedochvoid
sein. Hinweisende
Beispiel: Der Ergebnistyp von
List<T>.Reverse
istvoid
. Im folgenden Code ist der Textkörper des anonymen Ausdrucks ein null_conditional_invocation_expression, sodass es sich nicht um einen Fehler handelt.Action<List<int>> a = x => x?.Reverse();
Ende des Beispiels
Der Operator =>
verfügt über die gleiche Rangfolge wie die Zuweisung (=
) und ist rechtsassoziativ.
Eine anonyme Funktion mit dem async
-Modifikator ist eine asynchrone Funktion und folgt den in §15.15 beschriebenen Regeln.
Die Parameter einer anonymen Funktion in Form eines lambda_expression können explizit oder implizit eingegeben werden. In einer explizit typisierten Parameterliste ist der Typ jedes Parameters explizit angegeben. In einer implizit typierten Parameterliste werden die Parametertypen aus dem Kontext abgeleitet, in dem die anonyme Funktion auftritt – insbesondere, wenn die anonyme Funktion in einen kompatiblen Delegattyp oder Ausdrucksstrukturtyp konvertiert wird – stellt dieser Typ die Parametertypen bereit (§10.7).
In einem lambda_expression mit einem einzelnen, implizit typisierten Parameter können die Klammern aus der Parameterliste weggelassen werden. Mit anderen Worten, eine anonyme Funktion der Form
( «param» ) => «expr»
kann abgekürzt werden auf
«param» => «expr»
Die Parameterliste einer anonymen Funktion in Form eines anonymous_method_expression ist optional. Wenn gegeben, müssen die Parameter explizit typisiert sein. Ist dies nicht der Fall, ist die anonyme Funktion in einen Delegat mit jeder Parameterliste konvertierbar, die keine Ausgabeparameter enthält.
Ein block-Textkörper einer anonymen Funktion ist immer erreichbar (§13.2).
Beispiel: Im Folgenden finden Sie einige Beispiele für anonyme Funktionen:
x => x + 1 // Implicitly typed, expression body x => { return x + 1; } // Implicitly typed, block body (int x) => x + 1 // Explicitly typed, expression body (int x) => { return x + 1; } // Explicitly typed, block body (x, y) => x * y // Multiple parameters () => Console.WriteLine() // No parameters async (t1,t2) => await t1 + await t2 // Async delegate (int x) { return x + 1; } // Anonymous method expression delegate { return 1 + 1; } // Parameter list omitted
Ende des Beispiels
Das Verhalten von lambda_expressions und anonymous_method_expressions ist abgesehen von den folgenden Punkten identisch:
- anonymous_method_expressions erlauben, dass die Parameterliste vollständig weggelassen wird, was die Konvertierbarkeit zu Delegattypen mit beliebigen Wertparameterlisten ermöglicht.
- lambda_expression-Parametertypen dürfen weggelassen und abgeleitet werden, im Gegensatz dazu müssen bei anonymous_method_expression die Parametertypen explizit angegeben werden.
- Der Textkörper eines lambda_expression kann ein Ausdruck oder ein Block sein, während der Textkörper eines anonymous_method_expression ein Block sein muss.
- Nur lambda_expressions werden in kompatible Ausdrucksbäumetypen konvertiert (§8.6).
12.19.2 Signaturen anonymer Funktionen
Die anonymous_function_signature einer anonymen Funktion definiert die Namen und optional die Typen der Parameter für die anonyme Funktion. Der Umfang der Parameter der anonymen Funktion ist der anonymous_function_body (§7.7). Zusammen mit der Parameterliste (sofern angegeben) stellt der anonym-method-body einen Deklarationsraum dar (§7.3). Es ist daher ein Kompilierungszeitfehler, wenn der Name eines Parameters der anonymen Funktion mit dem Namen einer lokalen Variablen, einer lokalen Konstanten oder eines Parameters übereinstimmt, deren Gültigkeitsbereich die anonymous_method_expression oder lambda_expressionumfasst.
Wenn eine anonyme Funktion über eine explicit_anonymous_function_signature verfügt, ist der Satz kompatibler Delegattypen und Ausdrucksstrukturtypen auf diejenigen beschränkt, die dieselben Parametertypen und Modifizierer in derselben Reihenfolge haben (§10.7). Im Gegensatz zu den Konvertierungen von Methodengruppen (§10.8) wird die Konvertierung von anonymen Funktionsparametertypen nicht unterstützt. Wenn eine anonyme Funktion keine anonymous_function_signature hat, ist der Satz kompatibler Delegattypen und Ausdrucksstrukturtypen auf diejenigen beschränkt, die keine Ausgabeparameter aufweisen.
Beachten Sie, dass eine anonymous_function_signature keine Attribute oder ein Parameterarray enthalten kann. Dennoch kann ein anonymous_function_signature mit einem Delegattyp kompatibel sein, dessen Parameterliste ein Parameterarray enthält.
Beachten Sie auch, dass die Konvertierung in einen Ausdrucksbaumtyp, auch wenn er kompatibel ist, weiterhin zur Kompilierungszeit fehlschlagen kann (§8.6).
12.19.3 Textkörper der anonymen Funktion
Der Textkörper (expression oder block) einer anonymen Funktion unterliegt den folgenden Regeln:
- Wenn die anonyme Funktion eine Signatur enthält, sind die in der Signatur angegebenen Parameter im Body verfügbar. Wenn die anonyme Funktion keine Signatur aufweist, kann sie in einen Delegattyp oder Ausdruckstyp mit Parametern (§10.7) konvertiert werden, aber auf die Parameter kann nicht im Textkörper zugegriffen werden.
- Mit Ausnahme von by-reference-Parametern, die in der Signatur (falls vorhanden) der nächstgelegenen eingeschlossenen anonymen Funktion angegeben sind, ist es ein Kompilierungszeitfehler, wenn der Funktionskörper auf einen by-reference-Parameter zugreift.
- Mit Ausnahme von Parametern, die in der Signatur (falls vorhanden) der nächstgelegenen eingeschlossenen anonymen Funktion angegeben sind, handelt es sich um einen Kompilierungsfehler, wenn der Textkörper versucht, auf einen Parameter des
ref struct
-Typs zuzugreifen. - Wenn der Typ von
this
ein Strukturtyp ist, handelt es sich um einen Kompilierungszeitfehler für den Textkörper während des Zugriffs aufthis
. Dies gilt unabhängig davon, ob der Zugriff explizit (wie inthis.x
) oder implizit (wie inx
, wobeix
ein Instanzmitglied der Struct ist) erfolgt. Diese Regel verbietet einfach diesen Zugriff und beeinflusst nicht, ob die Mitgliedsabfrage zu einem Mitglied der Struktur führt. - Der Körper hat Zugriff auf die äußeren Variablen (§12.19.6) der anonymen Funktion. Der Zugriff auf eine äußere Variable verweist auf die Instanz der Variable, die aktiv ist, wenn der lambda_expression oder anonymous_method_expression ausgewertet wird (§12.19.7).
- Es handelt sich um einen Kompilierungsfehler für den Textkörper, der eine Anweisung des Typs
goto
,break
odercontinue
enthält, deren Ziel sich außerhalb des Textkörpers oder innerhalb des Texts einer enthaltenen anonymen Funktion befindet. - Eine
return
-Anweisung im Textkörper gibt ein Steuerelement aus einem Aufruf der nächstgelegenen umschließenden anonymen Funktion zurück, nicht aus dem umschließenden Funktionsmember.
Es wird explizit nicht angegeben, ob es möglich ist, den Block einer anonymen Funktion außer durch Auswertung und Aufruf von lambda_expression oder anonymous_method_expression auszuführen. Ein Compiler kann sich insbesondere dafür entscheiden, eine anonyme Funktion durch die Synthese einer oder mehrerer benannter Methoden oder Typen zu implementieren. Die Namen solcher synthetisierter Elemente müssen in einer Form vorliegen, die für die Verwendung durch Compiler reserviert ist (§6.4.3).
12.19.4 Überladungsauflösung
Anonyme Funktionen in einer Argumentliste nehmen am Typrückschluss und an der Überladungsauflösung teil. Siehe §12.6.3 und §12.6.4 für die genauen Regeln.
Beispiel: Das folgende Beispiel veranschaulicht, wie sich anonyme Funktionen auf die Überladungsauflösung auswirken.
class ItemList<T> : List<T> { public int Sum(Func<T, int> selector) { int sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } public double Sum(Func<T, double> selector) { double sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } }
Die Klasse
ItemList<T>
hat zweiSum
Methoden. Jede akzeptiert einselector
-Argument, das den Wert extrahiert, der aus einem Listenelement addiert werden soll. Der extrahierte Wert kann entweder einint
oder eindouble
sein und die resultierende Summe ist ebenfalls entweder einint
oder eindouble
.Die
Sum
-Methoden können z. B. verwendet werden, um Summen aus einer Liste von Detailzeilen in einer Reihenfolge zu berechnen.class Detail { public int UnitCount; public double UnitPrice; ... } class A { void ComputeSums() { ItemList<Detail> orderDetails = GetOrderDetails( ... ); int totalUnits = orderDetails.Sum(d => d.UnitCount); double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount); ... } ItemList<Detail> GetOrderDetails( ... ) { ... } }
Beim ersten Aufruf von
orderDetails.Sum
sind beideSum
-Methoden anwendbar, da die anonyme Funktiond => d.UnitCount
sowohl mitFunc<Detail,int>
als auchFunc<Detail,double>
kompatibel ist. Die Überladungsauflösung wählt jedoch die ersteSum
-Methode aus, da die Konvertierung inFunc<Detail,int>
besser ist als die Konvertierung inFunc<Detail,double>
.Beim zweiten Aufruf von
orderDetails.Sum
ist nur die zweite MethodeSum
anwendbar, weil die anonyme Funktiond => d.UnitPrice * d.UnitCount
einen Wert vom Typdouble
erzeugt. Daher wählt die Überladungsauflösung die zweite MethodeSum
für diesen Aufruf aus.Ende des Beispiels
12.19.5 Anonyme Funktionen und dynamische Bindung
Eine anonyme Funktion kann nicht Empfänger, Argument oder Operand einer dynamisch gebundenen Operation sein.
12.19.6 Äußere Variablen
12.19.6.1 Allgemein
Alle lokalen Variablen, Wertparameter oder Parameterarrays, deren Bereich den lambda_expression oder anonymous_method_expression enthält, werden als äußere Variable der anonymen Funktion bezeichnet. In einem Instanzfunktionsmitglied einer Klasse wird dieser Wert als Wertparameter betrachtet und ist eine äußere Variable jeder anonymen Funktion, die in dem Funktionsmitglied enthalten ist.
12.19.6.2 Erfasste äußere Variablen
Wenn von einer anonymen Funktion auf eine äußere Variable verwiesen wird, wird die äußere Variable von der anonymen Funktion erfasst. Normalerweise ist die Lebensdauer einer lokalen Variablen auf die Ausführung des Blocks oder der Aussage beschränkt, mit dem sie verbunden ist (§9.2.9.1). Die Lebensdauer einer erfassten äußeren Variablen wird jedoch mindestens verlängert, bis der aus der anonymen Funktion erstellte Delegat oder Ausdrucksbaum für die Garbage Collection infrage kommt.
Beispiel: Im Beispiel
delegate int D(); class Test { static D F() { int x = 0; D result = () => ++x; return result; } static void Main() { D d = F(); Console.WriteLine(d()); Console.WriteLine(d()); Console.WriteLine(d()); } }
Die lokale Variable
x
wird von der anonymen Funktion erfasst, und die Lebensdauer vonx
wird mindestens verlängert, bis der vonF
zurückgegebene Delegat für die Garbage Collection bereit ist. Da jeder Aufruf der anonymen Funktion auf derselben Instanz vonx
ausgeführt wird, lautet die Ausgabe des Beispiels:1 2 3
Ende des Beispiels
Wenn eine lokale Variable oder ein Wertparameter von einer anonymen Funktion eingefangen wird, wird die lokale Variable oder der Parameter nicht mehr als feste Variable (§23.4), sondern als bewegliche Variable betrachtet. Allerdings können gefangene äußere Variablen nicht in einer fixed
-Anweisung (§23.7) verwendet werden, so dass die Adresse einer gefangenen äußeren Variablen nicht übernommen werden kann.
Anmerkung: Anders als eine nicht erfasste Variable kann eine erfasste lokale Variable gleichzeitig mehreren Threads der Ausführung ausgesetzt sein. Hinweisende
12.19.6.3 Instanziierung lokaler Variablen
Eine lokale Variable wird als instanziiert betrachtet, wenn die Ausführung in den Bereich der Variablen wechselt.
Beispiel: Wenn zum Beispiel die folgende Methode aufgerufen wird, wird die lokale Variable
x
dreimal instanziiert und initialisiert - einmal für jede Iteration der Schleife.static void F() { for (int i = 0; i < 3; i++) { int x = i * 2 + 1; ... } }
Das Verschieben der Deklaration von
x
außerhalb der Schleife führt jedoch zu nur einer Instanziierung vonx
:static void F() { int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; ... } }
Ende des Beispiels
Wenn sie nicht erfasst wird, gibt es keine Möglichkeit, genau zu beobachten, wie oft eine lokale Variable instanziiert wird - da die Lebenszeiten der Instanziierungen disjunkt sind, ist es möglich, dass jede Instanziierung einfach denselben Speicherort verwendet. Wenn jedoch eine anonyme Funktion eine lokale Variable einfängt, werden die Auswirkungen der Instanziierung deutlich.
Beispiel: Das Beispiel
delegate void D(); class Test { static D[] F() { D[] result = new D[3]; for (int i = 0; i < 3; i++) { int x = i * 2 + 1; result[i] = () => Console.WriteLine(x); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
erzeugt die Ausgabe:
1 3 5
Wenn die Deklaration von
x
jedoch außerhalb der Schleife verschoben wird:delegate void D(); class Test { static D[] F() { D[] result = new D[3]; int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; result[i] = () => Console.WriteLine(x); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
die Ausgabe lautet:
5 5 5
Beachten Sie, dass ein Compiler zulässig ist (aber nicht erforderlich), um die drei Instanziierungen in eine einzelne Delegateninstanz zu optimieren (§10.7.2).
Ende des Beispiels
Wenn eine for-Schleife eine Iterationsvariable deklariert, wird diese Variable selbst als außerhalb der Schleife deklariert.
Beispiel: Wenn das Beispiel also geändert wird, um die Iterationsvariable selbst zu erfassen:
delegate void D(); class Test { static D[] F() { D[] result = new D[3]; for (int i = 0; i < 3; i++) { result[i] = () => Console.WriteLine(i); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
Nur eine Instanz der Iterationsvariable wird erfasst, was zu der Ausgabe führt:
3 3 3
Ende des Beispiels
Anonyme Funktionsdelegaten können einige erfasste Variablen gemeinsam nutzen, aber über separate Instanzen anderer Variablen verfügen.
Beispiel: Wenn z. B.
F
geändert wirdstatic D[] F() { D[] result = new D[3]; int x = 0; for (int i = 0; i < 3; i++) { int y = 0; result[i] = () => Console.WriteLine($"{++x} {++y}"); } return result; }
Die drei Delegaten erfassen dieselbe Instanz von
x
, jedoch separate Instanzen vony
, und die Ausgabe lautet:1 1 2 1 3 1
Ende des Beispiels
Separate anonyme Funktionen können dieselbe Instanz einer äußeren Variablen erfassen.
Beispiel: Im Beispiel:
delegate void Setter(int value); delegate int Getter(); class Test { static void Main() { int x = 0; Setter s = (int value) => x = value; Getter g = () => x; s(5); Console.WriteLine(g()); s(10); Console.WriteLine(g()); } }
Die beiden anonymen Funktionen erfassen dieselbe Instanz der lokalen Variablen
x
und können daher über diese Variable „kommunizieren“. Die Ausgabe des Beispiels lautet:5 10
Ende des Beispiels
12.19.7 Auswertung von anonymen Funktionsausdrücken
Eine anonyme Funktion F
muss immer in einen Delegattyp D
oder einen Ausdrucksbaumtyp E
konvertiert werden, entweder direkt oder durch die Ausführung eines Delegaterstellungsausdrucks new D(F)
. Diese Konversion bestimmt das Ergebnis der anonymen Funktion, wie in §10.7 beschrieben.
12.19.8 Beispiel für die Implementierung
Dieser Unterabschnitt ist informativ.
Dieser Unterabschnitt beschreibt eine mögliche Implementierung von anonymen Funktionskonvertierungen in Form von anderen C#-Konstrukten. Die hier beschriebene Implementierung basiert auf denselben Prinzipien, die auch von einem kommerziellen C#-Compiler verwendet werden. Es handelt sich dabei jedoch keineswegs um eine vorgeschriebene Implementierung und auch nicht um die einzig mögliche. Es wird nur kurz auf die Konvertierungen in Ausdrucksbäume eingegangen, da ihre genaue Semantik außerhalb des Umfangs dieser Spezifikation liegt.
Der Rest dieses Unterabschnitts enthält mehrere Beispiele für Code, der anonyme Funktionen mit unterschiedlichen Eigenschaften enthält. Für jedes Beispiel gibt es eine entsprechende Übersetzung in Code, der nur andere C#-Konstrukte verwendet. In den Beispielen wird angenommen, dass der Bezeichner D
den folgenden Delegattyp darstellt:
public delegate void D();
Die einfachste Form einer anonymen Funktion ist eine Funktion, die keine äußeren Variablen einbezieht:
delegate void D();
class Test
{
static void F()
{
D d = () => Console.WriteLine("test");
}
}
Dies kann in eine Delegateninstanziation übersetzt werden, die auf eine vom Compiler generierte statische Methode verweist, in der der Code der anonymen Funktion platziert wird:
delegate void D();
class Test
{
static void F()
{
D d = new D(__Method1);
}
static void __Method1()
{
Console.WriteLine("test");
}
}
Im folgenden Beispiel verweist die anonyme Funktion auf Instanzmember von this
:
delegate void D();
class Test
{
int x;
void F()
{
D d = () => Console.WriteLine(x);
}
}
Dies kann in eine vom Compiler generierte Instanzmethode übersetzt werden, die den Code der anonymen Funktion enthält:
delegate void D();
class Test
{
int x;
void F()
{
D d = new D(__Method1);
}
void __Method1()
{
Console.WriteLine(x);
}
}
In diesem Beispiel fängt die anonyme Funktion eine lokale Variable ein:
delegate void D();
class Test
{
void F()
{
int y = 123;
D d = () => Console.WriteLine(y);
}
}
Die Lebensdauer der lokalen Variable muss nun auf mindestens die Lebensdauer des Delegats der anonymen Funktion erweitert werden. Dies kann erreicht werden, indem die lokale Variable in ein Feld einer vom Compiler generierten Klasse „gehievt“ wird. Die Instanziierung der lokalen Variablen (§12.19.6.3) entspricht dann dem Erstellen einer Instanz der vom Compiler generierten Klasse, und der Zugriff auf die lokale Variable entspricht dem Zugriff auf ein Feld in der Instanz der vom Compiler generierten Klasse. Außerdem wird die anonyme Funktion zu einer Instanzmethode der vom Compiler generierten Klasse:
delegate void D();
class Test
{
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1
{
public int y;
public void __Method1()
{
Console.WriteLine(y);
}
}
}
Die folgende anonyme Funktion schließlich erfasst this
sowie zwei lokale Variablen mit unterschiedlichen Lebensdauern:
delegate void D();
class Test
{
int x;
void F()
{
int y = 123;
for (int i = 0; i < 10; i++)
{
int z = i * 2;
D d = () => Console.WriteLine(x + y + z);
}
}
}
Hier wird für jeden Block, in dem lokale Variablen erfasst werden, eine vom Compiler generierte Klasse erstellt, sodass die lokalen Variablen in den verschiedenen Blöcken unabhängige Lebensdauern haben können. Eine Instanz von __Locals2
, der vom Compiler generierten Klasse für den inneren Block, enthält die lokale Variable z
und ein Feld, das auf eine Instanz von __Locals1
verweist. Eine Instanz von __Locals1
, der vom Compiler generierten Klasse für den äußeren Block, enthält die lokale Variable y
und ein Feld, das auf this
des umschließenden Funktionselements verweist. Mit diesen Datenstrukturen ist es möglich, alle erfassten äußeren Variablen über eine Instanz von __Local2
zu erreichen, und der Code der anonymen Funktion kann somit als Instanzmethode dieser Klasse implementiert werden.
delegate void D();
class Test
{
int x;
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++)
{
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1
{
public Test __this;
public int y;
}
class __Locals2
{
public __Locals1 __locals1;
public int z;
public void __Method1()
{
Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}
Dieselbe Technik, die hier zum Erfassen lokaler Variablen angewendet wird, kann auch verwendet werden, um anonyme Funktionen in Ausdrucksbäume zu konvertieren: Verweise auf die vom Compiler generierten Objekte können in dem Ausdrucksbaum gespeichert werden, und der Zugriff auf die lokalen Variablen kann als Feldzugriff auf diese Objekte dargestellt werden. Der Vorteil dieses Ansatzes besteht darin, dass die lokalen „Lifted“-Variablen von Delegaten und Ausdrucksbäumen gemeinsam genutzt werden können.
Ende des informativen Textes.
12.20 Abfrageausdrücke
12.20.1 Allgemein
Abfrageausdrücke bieten eine sprachintegrierte Syntax für Abfragen, die relationalen und hierarchischen Abfragesprachen wie SQL und XQuery ähneln.
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clauses? select_or_group_clause query_continuation?
;
query_body_clauses
: query_body_clause
| query_body_clauses query_body_clause
;
query_body_clause
: from_clause
| let_clause
| where_clause
| join_clause
| join_into_clause
| orderby_clause
;
let_clause
: 'let' identifier '=' expression
;
where_clause
: 'where' boolean_expression
;
join_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression
;
join_into_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression 'into' identifier
;
orderby_clause
: 'orderby' orderings
;
orderings
: ordering (',' ordering)*
;
ordering
: expression ordering_direction?
;
ordering_direction
: 'ascending'
| 'descending'
;
select_or_group_clause
: select_clause
| group_clause
;
select_clause
: 'select' expression
;
group_clause
: 'group' expression 'by' expression
;
query_continuation
: 'into' identifier query_body
;
Ein Ausdruck für eine Abfrage beginnt mit einer from
-Klausel und endet entweder mit einer select
- oder group
-Klausel. Auf die anfängliche from
-Klausel können keine oder weitere from
-, let
-, where
-, join
- oder orderby
-Klauseln folgen. Jede from
-Klausel ist ein Generator, der eine Bereichsvariable einführt, die sich über die Elemente einer Sequenzerstreckt. Jede let
-Klausel leitet eine Bereichsvariable ein, die einen Wert repräsentiert, der mit Hilfe der vorherigen Bereichsvariablen berechnet wurde. Jede where
Klausel ist ein Filter, der Elemente aus dem Ergebnis ausschließt. Jede join
-Klausel vergleicht angegebene Schlüssel der Quellsequenz mit Schlüsseln einer anderen Sequenz und liefert so übereinstimmende Paare. Jede orderby
-Klausel ordnet die Elemente gemäß den angegebenen Kriterien neu an. Die letzte select
- oder group
-Klausel gibt die Form des Ergebnisses in Bezug auf die Bereichsvariablen an. Schließlich kann eine into
-Klausel zum „Splicen“ von Abfragen verwendet werden, indem die Ergebnisse einer Abfrage als Generator in einer nachfolgenden Abfrage behandelt werden.
12.20.2 Mehrdeutigkeiten in Abfrageausdrücken
Abfrageausdrücke verwenden eine Reihe von kontextbezogenen Schlüsselwörtern (§6.4.4): ascending
, by
, descending
, equals
, from
, group
, into
, join
, let
, on
, orderby
, select
und where
.
Um Mehrdeutigkeiten zu vermeiden, die sich aus der Verwendung dieser Bezeichner sowohl als Schlüsselwörter als auch als einfache Namen ergeben könnten, werden diese Bezeichner überall in einem Abfrageausdruck als Schlüsselwörter betrachtet. Das gilt nicht, wenn sie mit "@
" (§6.4.4) präfixiert werden, in welchem Fall sie als Bezeichner gelten. Zu diesem Zweck ist ein Abfrageausdruck jeder Ausdruck, der mit „from
identifer“ beginnt und auf den ein beliebiges Token folgt, mit Ausnahme von „;
“, „=
“ oder „,
“.
12.20.3 Übersetzung von Abfrageausdrücken
12.20.3.1 Allgemein
Die Sprache C# spezifiziert nicht die Ausführungssemantik von Abfrageausdrücken. Vielmehr werden Abfrageausdrücke in Aufrufe von Methoden übersetzt, die sich an das Abfrageausdrucksmuster halten (§12.20.4). Im Einzelnen werden Ausdrücke für Abfragen in Aufrufe von Methoden mit den Namen Where
, Select
, SelectMany
, Join
, GroupJoin
, OrderBy
, OrderByDescending
, ThenBy
, ThenByDescending
, GroupBy
und Cast
übersetzt. Diese Methoden werden voraussichtlich über bestimmte Signaturen und Rückgabetypen verfügen, wie in §12.20.4 beschrieben. Bei diesen Methoden kann es sich um Instanzmethoden des abgefragten Objekts oder um Erweiterungsmethoden handeln, die außerhalb des Objekts liegen. Diese Methoden implementieren die eigentliche Ausführung der Abfrage.
Die Übersetzung von Abfrageausdrücken in Methodenaufrufe ist eine syntaktische Zuordnung, die erfolgt, bevor eine Typbindung oder Überladungsauflösung durchgeführt wurde. Nach der Übersetzung von Ausdrücken für Abfragen werden die resultierenden Methodenaufrufe wie reguläre Methodenaufrufe verarbeitet, was wiederum Kompilierfehler aufdecken kann. Diese Fehlerbedingungen umfassen unter anderem Methoden, die nicht existieren, Argumente mit falschen Typen und generische Methoden, bei denen die Typableitung fehlschlägt.
Ein Ausdruck für eine Abfrage wird durch wiederholte Anwendung der folgenden Übersetzungen verarbeitet, bis keine weiteren Kürzungen mehr möglich sind. Die Übersetzungen werden in der Reihenfolge der Anwendung aufgeführt: Jeder Abschnitt geht davon aus, dass die Übersetzungen in den vorherigen Abschnitten vollständig ausgeführt wurden, und sobald das erfolgt ist, wird ein Abschnitt bei der Bearbeitung desselben Abfrageausdrucks nicht erneut betrachtet.
Es ist ein Kompilierfehler, wenn ein Ausdruck einer Abfrage eine Zuweisung an eine Bereichsvariable oder die Verwendung einer Bereichsvariable als Argument für eine Referenz oder einen Ausgabeparameter enthält.
Bestimmte Übersetzungen fügen Bereichsvariablen mit transparenten Bezeichnern hinzu, die mit * gekennzeichnet sind. Diese werden in §12.20.3.8 näher beschrieben.
12.20.3.2 Abfrageausdrücke mit Fortsetzungen
Ein Abfrageausdruck mit einer Fortsetzung nach dem Abfragetext
from «x1» in «e1» «b1» into «x2» «b2»
wird übersetzt in
from «x2» in ( from «x1» in «e1» «b1» ) «b2»
Die Übersetzungen in den folgenden Abschnitten gehen davon aus, dass Abfragen keine Fortsetzungen haben.
Beispiel: Das Beispiel:
from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Count() }
wird übersetzt in:
from g in (from c in customers group c by c.Country) select new { Country = g.Key, CustCount = g.Count() }
deren endgültige Übersetzung wie folgt lautet:
customers. GroupBy(c => c.Country). Select(g => new { Country = g.Key, CustCount = g.Count() })
Ende des Beispiels
12.20.3.3 Explizite Bereichsvariablentypen
Eine from
-Klausel, die explizit einen Range-Variablen-Typ angibt
from «T» «x» in «e»
wird übersetzt in
from «x» in ( «e» ) . Cast < «T» > ( )
Eine join
-Klausel, die explizit einen Range-Variablen-Typ angibt
join «T» «x» in «e» on «k1» equals «k2»
wird übersetzt in
join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»
Bei den Übersetzungen in den folgenden Abschnitten wird davon ausgegangen, dass Abfragen keine expliziten Bereichsvariablentypen haben.
Beispiel: Das Beispiel
from Customer c in customers where c.City == "London" select c
wird übersetzt in
from c in (customers).Cast<Customer>() where c.City == "London" select c
deren endgültige Übersetzung lautet
customers. Cast<Customer>(). Where(c => c.City == "London")
Ende des Beispiels
Hinweis: Explizite Bereichsvariablentypen eignen sich zum Abfragen von Auflistungen, die die nicht-generische
IEnumerable
-Schnittstelle implementieren, jedoch nicht die generischeIEnumerable<T>
-Schnittstelle. Im obigen Beispiel wäre dies der Fall, wenn Kunden vom TypArrayList
wären. Hinweisende
12.20.3.4 Degenerierte Abfrageausdrücke
Ein Abfrageausdruck der Form
from «x» in «e» select «x»
wird übersetzt in
( «e» ) . Select ( «x» => «x» )
Beispiel: Das Beispiel
from c in customers select c
wird übersetzt in
(customers).Select(c => c)
Ende des Beispiels
Ein degenerierter Abfrage-Ausdruck ist ein Ausdruck, der trivialerweise die Elemente der Quelle auswählt.
Hinweis: Spätere Phasen der Übersetzung (§12.20.3.6 und §12.20.3.7) entfernen degenerierte Abfragen, die durch andere Übersetzungsschritte eingeführt wurden, und ersetzen sie durch ihre Quelle. Es ist jedoch wichtig sicherzustellen, dass das Ergebnis eines Abfrage-Ausdrucks niemals das Quellobjekt selbst ist. Andernfalls kann das Zurückgeben des Ergebnisses einer solchen Abfrage private Daten (z. B. ein Elementarray) versehentlich für einen Aufrufer verfügbar machen. Daher schützt dieser Schritt degenerierte Anfragen, die direkt im Quellcode geschrieben wurden, indem explizit
Select
auf die Quelle angewendet wird. Es liegt dann an den Implementierern vonSelect
und anderen Abfrage-Operatoren, dafür zu sorgen, dass diese Methoden niemals das Quellobjekt selbst zurückgeben. Hinweisende
12.20.3.5 From-, let-, where-, join- und orderby-Klauseln
Ein Abfrageausdruck mit einer zweiten from
-Klausel gefolgt von einer select
-Klausel
from «x1» in «e1»
from «x2» in «e2»
select «v»
wird übersetzt in
( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )
Beispiel: Das Beispiel
from c in customers from o in c.Orders select new { c.Name, o.OrderID, o.Total }
wird übersetzt in
(customers). SelectMany(c => c.Orders, (c,o) => new { c.Name, o.OrderID, o.Total } )
Ende des Beispiels
Ein Abfrageausdruck mit einer zweiten from
-Klausel gefolgt von einem Abfragetext Q
, der einen nicht leeren Satz von Abfragetextklauseln enthält:
from «x1» in «e1»
from «x2» in «e2»
Q
wird übersetzt in
from * in («e1») . SelectMany( «x1» => «e2» ,
( «x1» , «x2» ) => new { «x1» , «x2» } )
Q
Beispiel: Das Beispiel
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.OrderID, o.Total }
wird übersetzt in
from * in (customers). SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.OrderID, o.Total }
deren endgültige Übersetzung lautet
customers. SelectMany(c => c.Orders, (c,o) => new { c, o }). OrderByDescending(x => x.o.Total). Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })
dabei handelt es sich bei
x
um einen compilergenerierten Bezeichner, der andernfalls unsichtbar und unzugänglich ist.Ende des Beispiels
Ein let
-Ausdruck nebst der vorangehenden from
-Klausel:
from «x» in «e»
let «y» = «f»
...
wird übersetzt in
from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )
...
Beispiel: Das Beispiel
from o in orders let t = o.Details.Sum(d => d.UnitPrice * d.Quantity) where t >= 1000 select new { o.OrderID, Total = t }
wird übersetzt in
from * in (orders).Select( o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) where t >= 1000 select new { o.OrderID, Total = t }
deren endgültige Übersetzung lautet
orders .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) .Where(x => x.t >= 1000) .Select(x => new { x.o.OrderID, Total = x.t })
dabei handelt es sich bei
x
um einen compilergenerierten Bezeichner, der andernfalls unsichtbar und unzugänglich ist.Ende des Beispiels
Ein where
-Ausdruck nebst der vorangehenden from
-Klausel:
from «x» in «e»
where «f»
...
wird übersetzt in
from «x» in ( «e» ) . Where ( «x» => «f» )
...
Eine join
-Klausel unmittelbar gefolgt von einer select
-Klausel
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
select «v»
wird übersetzt in
( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )
Beispiel: Das Beispiel
from c in customers join o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
wird übersetzt in
(customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })
Ende des Beispiels
Eine join
-Klausel gefolgt von einer Abfragetextklausel:
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
...
wird übersetzt in
from * in ( «e1» ) . Join(
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })
...
Eine join
-into
-Klausel unmittelbar gefolgt von einer select
-Klausel
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2» into «g»
select «v»
wird übersetzt in
( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «g» ) => «v» )
Eine join into
-Klausel gefolgt von einer Abfragetextklausel
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2» into *g»
...
wird übersetzt in
from * in ( «e1» ) . GroupJoin(
«e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...
Beispiel: Das Beispiel
from c in customers join o in orders on c.CustomerID equals o.CustomerID into co let n = co.Count() where n >= 10 select new { c.Name, OrderCount = n }
wird übersetzt in
from * in (customers).GroupJoin( orders, c => c.CustomerID, o => o.CustomerID, (c, co) => new { c, co }) let n = co.Count() where n >= 10 select new { c.Name, OrderCount = n }
deren endgültige Übersetzung lautet
customers .GroupJoin( orders, c => c.CustomerID, o => o.CustomerID, (c, co) => new { c, co }) .Select(x => new { x, n = x.co.Count() }) .Where(y => y.n >= 10) .Select(y => new { y.x.c.Name, OrderCount = y.n })
wobei
x
undy
vom Compiler generierte Bezeichner sind, die ansonsten unsichtbar und unzugänglich sind.Ende des Beispiels
Eine orderby
-Klausel und ihre vorangehende from
-Klausel:
from «x» in «e»
orderby «k1» , «k2» , ... , «kn»
...
wird übersetzt in
from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...
Wenn eine ordering
-Klausel einen absteigenden Richtungsindikator angibt, wird stattdessen ein Aufruf von OrderByDescending
oder ThenByDescending
erzeugt.
Beispiel: Das Beispiel
from o in orders orderby o.Customer.Name, o.Total descending select o
hat die endgültige Übersetzung
(orders) .OrderBy(o => o.Customer.Name) .ThenByDescending(o => o.Total)
Ende des Beispiels
Bei den folgenden Übersetzungen wird davon ausgegangen, dass in jedem Abfrageausdruck keine let
-, where
-, join
- oder orderby
-Klauseln vorhanden sind und nicht mehr als die erste from
-Klausel.
12.20.3.6 Select-Klauseln
Ein Abfrageausdruck der Form
from «x» in «e» select «v»
wird übersetzt in
( «e» ) . Select ( «x» => «v» )
außer, wenn der Bezeichner «v»
lautet «x»
, ist die Übersetzung einfach
( «e» )
Beispiel: Das Beispiel
from c in customers.Where(c => c.City == "London") select c
wird einfach übersetzt in
(customers).Where(c => c.City == "London")
Ende des Beispiels
12.20.3.7 Group-Klauseln
Eine group
-Klausel
from «x» in «e» group «v» by «k»
wird übersetzt in
( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )
außer, wenn der Bezeichner «v»
lautet «x»
, ist die Übersetzung
( «e» ) . GroupBy ( «x» => «k» )
Beispiel: Das Beispiel
from c in customers group c.Name by c.Country
wird übersetzt in
(customers).GroupBy(c => c.Country, c => c.Name)
Ende des Beispiels
12.20.3.8 Transparente Bezeichner
Bestimmte Übersetzungen fügen Bereichsvariablen mit transparenten Bezeichnern hinzu, die mit *
gekennzeichnet sind. Transparente Bezeichner existieren nur als Zwischenschritt im Übersetzungsprozess von Abfrageausdrücken.
Wenn eine Abfrageübersetzung einen transparenten Bezeichner injiziert, übertragen weitere Übersetzungsschritte den transparenten Bezeichner in anonyme Funktionen und anonyme Objekt-Initialisierer. In diesen Kontexten verhalten sich transparente Bezeichner wie folgt:
- Wenn ein transparenter Bezeichner als Parameter in einer anonymen Funktion auftritt, sind die Elemente des zugeordneten anonymen Typs automatisch im Gültigkeitsbereich des Textkörpers der anonymen Funktion.
- Wenn sich ein Member mit einem transparenten Bezeichner im Bereich befindet, sind auch die Member dieses Members im Bereich.
- Wenn ein transparenter Bezeichner als Memberdeklarator in einem anonymen Objektinitialisierer auftritt, führt es einen Member mit einem transparenten Bezeichner ein.
In den oben beschriebenen Übersetzungsschritten werden transparente Bezeichner immer zusammen mit anonymen Typen eingeführt, mit dem Ziel, mehrere Bereichsvariablen als Mitglieder eines einzigen Objekts zu erfassen. Eine Implementierung von C# darf einen anderen Mechanismus als anonyme Typen verwenden, um mehrere Bereichsvariablen zusammenzufassen. Die folgenden Übersetzungsbeispiele gehen davon aus, dass anonyme Typen verwendet werden, und zeigen eine mögliche Übersetzung von transparenten Bezeichnern.
Beispiel: Das Beispiel
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.Total }
wird übersetzt in
from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.Total }
welcher weiter übersetzt wird in
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(* => o.Total) .Select(\* => new { c.Name, o.Total })
welcher, wenn transparente Bezeichner gelöscht werden, gleichbedeutend ist mit
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(x => x.o.Total) .Select(x => new { x.c.Name, x.o.Total })
dabei handelt es sich bei
x
um einen compilergenerierten Bezeichner, der andernfalls unsichtbar und unzugänglich ist.Das Beispiel
from c in customers join o in orders on c.CustomerID equals o.CustomerID join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
wird übersetzt in
from * in (customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
welcher weiter reduziert wird auf
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }) .Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { c.Name, o.OrderDate, p.ProductName })
deren endgültige Übersetzung lautet
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d }) .Join(products, y => y.d.ProductID, p => p.ProductID, (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })
wobei
x
undy
vom Compiler generierte Bezeichner sind, die ansonsten unsichtbar und unzugänglich sind. Ende des Beispiels
12.20.4 Das Abfrage-Ausdrucksmuster
Das Abfrage-Ausdrucksmuster strukturiert ein Muster von Methoden, die Typen implementieren können, um Abfrageausdrücke zu unterstützen.
Ein generischer Typ C<T>
unterstützt das Abfrageausdrucksmuster, wenn seine öffentlichen Membermethoden und die öffentlich zugänglichen Erweiterungsmethoden durch die folgende Klassendefinition ersetzt werden können. Die Member und zugänglichen Erweiterungsmethoden werden als die „Form” eines generischen Typs C<T>
bezeichnet. Zur Veranschaulichung der richtigen Beziehungen zwischen Parameter- und Rückgabetypen wird ein generischer Typ verwendet, aber es ist auch möglich, das Muster für nicht-generische Typen zu implementieren.
delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);
class C
{
public C<T> Cast<T>() { ... }
}
class C<T> : C
{
public C<T> Where(Func<T,bool> predicate) { ... }
public C<U> Select<U>(Func<T,U> selector) { ... }
public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
Func<T,U,V> resultSelector) { ... }
public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
Func<T,E> elementSelector) { ... }
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}
class G<K,T> : C<T>
{
public K Key { get; }
}
Die oben genannten Methoden verwenden die generischen Delegattypen Func<T1, R>
und Func<T1, T2, R>
, aber sie könnten ebenso gut andere Delegat- oder Ausdrucksstrukturtypen mit denselben Beziehungen in Parameter- und Rückgabetypen verwenden.
Hinweis: Die empfohlene Beziehung zwischen
C<T>
undO<T>
, die sicherstellt, dass die MethodenThenBy
undThenByDescending
nur auf dem Ergebnis einesOrderBy
oderOrderByDescending
verfügbar sind. Hinweisende
Hinweis: Die empfohlene Form des Ergebnisses von
GroupBy
– eine Sequenz von Sequenzen, wobei jede innere Sequenz eine zusätzlicheKey
-Eigenschaft aufweist. Hinweisende
Anmerkung: Da Abfrageausdrücke durch eine syntaktische Zuordnung in Methodenaufrufe übersetzt werden, haben Typen eine beträchtliche Flexibilität bei der Implementierung einzelner oder aller Abfrageausdruckmuster. Die Methoden des Musters können z. B. als Instanzmethoden oder als Erweiterungsmethoden implementiert werden, da die beiden über dieselbe Aufrufsyntax verfügen, und die Methoden können Delegaten oder Ausdrucksbäume anfordern, da anonyme Funktionen in beide konvertiert werden können. Typen, die nur einige Teile des Abfrageausdruckmusters implementieren, unterstützen nur Übersetzungen von Abfrageausdrücken, die den Methoden zugeordnet sind, die dieser Typ unterstützt. Hinweisende
Hinweis: Der
System.Linq
-Namespace stellt eine Implementierung des Abfrageausdruckmusters für jeden Typ bereit, der dieSystem.Collections.Generic.IEnumerable<T>
-Schnittstelle implementiert. Hinweisende
12.21 Zuweisungsoperatoren
12.21.1 Allgemein
Bis auf einen weisen alle Zuordnungsoperatoren einer Variablen, einer Eigenschaft, einem Ereignis oder einem Indexerelement einen neuen Wert zu. Die Ausnahme = ref
weist eine Variablenreferenz (§9.5) einer Referenzvariable zu (§9.7).
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
| right_shift_assignment
;
Der linke Operand einer Zuweisung muss ein Ausdruck sein, der als Variable kategorisiert wird, oder, mit Ausnahme von = ref
, ein Eigenschaften-, ein Indexer-, ein Ereigniszugriff oder ein Tupel. Ein Deklarationsausdruck ist nicht direkt als linker Operand zulässig, kann aber als Schritt bei der Auswertung einer Dekonstruktionszuweisung auftreten.
Der =
-Operator wird als einfacher Zuweisungsoperator bezeichnet. Er weist den Wert oder die Werte des rechten Operanden der Variablen, der Eigenschaft, dem Indexer-Element oder den Tupelelemente zu, die vom linken Operanden angegeben werden. Der linke Operand des einfachen Zuwiesungsoperators darf kein Ereigniszugriff sein (außer wie in §15.8.2 beschrieben). Der einfache Zuweisungsoperator wird in §12.21.2 beschrieben.
Der = ref
-Operator wird als Ref-Zuweisungsoperator bezeichnet. Er macht den rechten Operanden, der ein variable_reference (§9.5) sein soll, zum Referent der Referenzvariable, die vom linken Operanden bestimmt wird. Der Ref-Zuweisungsoperator wird in §12.21.3 beschrieben.
Die Zuweisungsoperatoren außer den Operatoren =
und = ref
werden als Verbundzuweisungsoperatoren bezeichnet. Diese Operatoren führen den angegebenen Vorgang für die beiden Operanden aus und weisen dann den resultierenden Wert der durch den linken Operanden angegebenen Variable, Eigenschaft oder dem Indexerelement zu. Die Verbundzuweisungsoperatoren werden in §12.21.4 beschrieben.
Die Operatoren +=
und -=
mit einem Ereigniszugriffsausdruck als linker Operand werden als Ereigniszuweisungsoperatoren bezeichnet. Kein anderer Zuweisungsoperator ist mit einem Ereigniszugriff als linker Operand gültig. Die Operatoren für die Zuweisung von Ereignissen sind in §12.21.5 beschrieben.
Die Zuweisungsoperatoren sind rechtsassoziativ, d. h. Vorgänge werden von rechts nach links gruppiert.
Beispiel: Ein Ausdruck in der Form
a = b = c
wird alsa = (b = c)
ausgewertet. Ende des Beispiels
12.21.2 Einfache Zuweisung
Der =
-Operator wird als einfacher Zuweisungsoperator bezeichnet.
Wenn der linke Operand einer einfachen Zuordnung der Form E.P
oder E[Ei]
ist und E
den Kompilierzeittyp dynamic
hat, ist die Zuordnung dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Zuweisungsausdrucks dynamic
, und die im Folgenden beschriebene Auflösung erfolgt zur Laufzeit basierend auf dem Laufzeittyp E
. Wenn der linke Operand die Form E[Ei]
aufweist, in der mindestens ein Element von Ei
den Kompilierungszeittyp dynamic
hat und der Kompilierungszeittyp von E
kein Array ist, wird der resultierende Zugriff auf den Indexer dynamisch gebunden, aber mit eingeschränkter Kompilierungszeit-Überprüfung (§12.6.5).
Eine einfache Zuweisung, bei der der linke Operand als Tupel klassifiziert wird, wird auch als dekonstruierende Zuweisung bezeichnet. Wenn eines der Tupel-Elemente des linken Operanden einen Elementnamen hat, tritt ein Kompilierfehler auf. Wenn eines der Tupelelemente des linken Operanden ein declaration_expression ist und ein anderes Element kein declaration_expression oder ein einfacher Ausschuss ist, tritt ein Kompilierzeitfehler auf.
Der Typ einer einfachen Zuweisung x = y
ist der Typ einer Zuweisung zu x
von y
, die rekursiv wie folgt bestimmt wird:
- Wenn
x
ein Tupelausdruck(x1, ..., xn)
ist undy
in einen Tupelausdruck(y1, ..., yn)
mitn
-Elementen (§12.7) zerlegt werden kann, und wenn jede Zuweisung anxi
vonyi
den TypTi
hat, dann hat die Zuweisung den Typ(T1, ..., Tn)
. - Andernfalls, wenn
x
als Variable klassifiziert wird, die Variable nichtreadonly
ist,x
den TypT
hat, undy
eine implizite Konvertierung inT
hat, dann hat die Zuweisung den TypT
. - Andererseits, wenn
x
als implizit typierte Variable klassifiziert wird (d. h. ein implizit typierter Deklarationsausdruck) undy
den TypT
hat, ist der abgeleitete Typ der VariablenT
, und die Zuweisung hat den TypT
. - Andernfalls, wenn
x
als Eigenschafts- oder Indexerzugriff klassifiziert ist, die Eigenschaft oder der Indexer einen zugänglichen set-Accessor hat,x
über den TypT
verfügt undy
eine implizite Konvertierung inT
aufweist, hat die Zuweisung den TypT
. - Andernfalls ist die Zuweisung ungültig, und es tritt ein Bindungszeitfehler auf.
Die Laufzeitverarbeitung einer einfachen Zuweisung des Formulars x = y
mit Typ T
wird als Zuweisung zu x
von y
mit Typ T
ausgeführt, die aus den folgenden rekursiven Schritten besteht:
-
x
wird ausgewertet, wenn dies noch nicht geschehen ist. - Wenn
x
als Variable klassifiziert wird, wirdy
ausgewertet und, falls erforderlich, durch eine implizite Konversion (§10.2) inT
umgewandelt.- Wenn die von
x
angegebene Variable ein Arrayelement eines reference_type ist, wird eine Laufzeitüberprüfung durchgeführt, um sicherzustellen, dass der füry
berechnete Wert mit der Arrayinstanz kompatibel ist, von derx
ein Element ist. Die Überprüfung ist erfolgreich, wenny
null
ist oder wenn eine implizite Verweiskonvertierung (§10.2.8) vom Typ der Instanz, auf diey
verweist, auf den tatsächlichen Elementtyp der Arrayinstanz existiert, diex
enthält. Andernfalls wird eineSystem.ArrayTypeMismatchException
ausgelöst. - Der Wert, der sich aus der Auswertung und Umwandlung von
y
ergibt, wird an der durch die Auswertung vonx
bestimmten Position gespeichert und als Resultat der Zuweisung zurückgegeben.
- Wenn die von
- Wenn
x
als Zugriff auf eine Eigenschaft oder einen Indexer klassifiziert wird:y
wird ausgewertet und, falls erforderlich, implizit inT
umgewandelt (§10.2).- Der set-Accessor von
x
wird mit dem Wert aufgerufen, der aus der Bewertung und Umwandlung vony
als Argument für seinen Wert hervorgeht. - Der Wert, der sich aus der Auswertung und Umwandlung von
y
ergibt, wird als Ergebnis der Zuweisung zurückgegeben.
- Wenn
x
als Tupel(x1, ..., xn)
klassifiziert wird und die Aritätn
hat:-
y
wird mitn
Elementen zu einem Tupelausdrucke
dekonstruiert. - ein Resultat-Tupel
t
wird durch die implizite Konvertierung vone
inT
erstellt. - für jeden
xi
in der Reihenfolge von links nach rechts wird eine Zuweisung zuxi
vont.Itemi
ausgeführt, mit der Ausnahme, dass diexi
nicht erneut ausgewertet werden. -
t
wird als Ergebnis der Zuweisung zurückgegeben.
-
Anmerkung: Wenn der Compile-Time-Typ von
x
dynamic
ist und es eine implizite Konvertierung vom Compile-Time-Typ vony
nachdynamic
gibt, ist keine Auflösung zur Laufzeit erforderlich. Hinweisende
Hinweis: Die Regeln für die Arraykovarianz (§17.6) erlauben, dass ein Wert eines Arraytyps
A[]
ein Verweis auf eine Instanz eines ArraytypsB[]
ist, vorausgesetzt, eine implizite Verweiskonvertierung ist vonB
inA
vorhanden. Aufgrund dieser Regeln erfordert die Zuweisung zu einem Arrayelement eines reference_type eine Laufzeitüberprüfung, um sicherzustellen, dass der zugewiesene Wert mit der Arrayinstanz kompatibel ist. Im Beispielstring[] sa = new string[10]; object[] oa = sa; oa[0] = null; // OK oa[1] = "Hello"; // OK oa[2] = new ArrayList(); // ArrayTypeMismatchException
Die letzte Zuweisung verursacht, dass ein
System.ArrayTypeMismatchException
ausgelöst wird, da ein Verweis auf einArrayList
nicht in einem Element einesstring[]
gespeichert werden kann.Hinweisende
Wenn eine Eigenschaft oder ein Indexer, die oder der in einem struct_type deklariert ist, das Ziel einer Zuweisung ist, muss der Instanzausdruck, der der Eigenschaft oder dem Indexerzugriff zugeordnet ist, als Variable klassifiziert werden. Wenn der Instanzausdruck als Wert klassifiziert wird, tritt ein Bindungszeitfehler auf.
Hinweis: Aufgrund §12.8.7 gilt die gleiche Regel auch für Felder. Hinweisende
Beispiel: Aufgrund der Deklarationen:
struct Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } } struct Rectangle { Point a, b; public Rectangle(Point a, Point b) { this.a = a; this.b = b; } public Point A { get { return a; } set { a = value; } } public Point B { get { return b; } set { b = value; } } }
im Beispiel
Point p = new Point(); p.X = 100; p.Y = 100; Rectangle r = new Rectangle(); r.A = new Point(10, 10); r.B = p;
sind die Zuweisungen an
p.X
,p.Y
,r.A
undr.B
zulässig, dap
undr
Variablen sind. Jedoch im BeispielRectangle r = new Rectangle(); r.A.X = 10; r.A.Y = 10; r.B.X = 100; r.B.Y = 100;
die Zuweisungen sind alle ungültig, da
r.A
undr.B
keine Variablen sind.Ende des Beispiels
12.21.3 Ref-Zuweisung
Der = ref
-Operator wird als Ref-Zuweisungsoperator bezeichnet.
Der linke Operand muss ein Ausdruck sein, der auf eine Referenzvariable (§9.7), einen Referenzparameter (außer this
), einen Ausgabeparameter oder einen Eingabeparameter bindet. Der rechte Operand muss ein Ausdruck sein, der variable_reference ergibt, die einen Wert desselben Typs wie der linke Operand bezeichnet.
Es handelt sich um einen Kompilierungszeitfehler, wenn der ref-safe-context (§9.7.2) des linken Operanden breiter als der ref-safe-context des rechten Operanden ist.
Der rechte Operand muss zum Zeitpunkt der Ref-Zuweisung definitiv zugewiesen sein.
Wenn der linke Operand an einen Ausgabeparameter gebunden ist, liegt ein Fehler vor, wenn dieser Ausgabeparameter zu Beginn des Ref-Zuweisungsoperators nicht definitiv zugewiesen wurde.
Wenn der linke Operand ein schreibbarer Bezug ist (d. h., er bestimmt alles andere als ein ref readonly
lokaler oder Eingabeparameter), dann muss der rechte Operand eine schreibbare variable_reference sein. Wenn die rechte Operandenvariable schreibbar ist, kann der linke Operand schreibbar oder schreibgeschützt sein.
Der Vorgang macht den linken Operanden zu einem Alias der rechten Operandenvariablen. Der Alias kann schreibgeschützt gemacht werden, auch wenn die rechte Operandenvariable schreibbar ist.
Verweiszuweisungsoperator liefert ein variable_reference-Element vom zugewiesenen Typ. Es ist schreibbar, wenn der linke Operand schreibbar ist.
Der Verweiszuweisungsoperator darf den Speicherort, auf den der rechte Operand verweist, nicht lesen.
Beispiel: Hier sind einige Beispiele für die Verwendung von
= ref
:public static int M1() { ... } public static ref int M2() { ... } public static ref uint M2u() { ... } public static ref readonly int M3() { ... } public static void Test() { int v = 42; ref int r1 = ref v; // OK, r1 refers to v, which has value 42 r1 = ref M1(); // Error; M1 returns a value, not a reference r1 = ref M2(); // OK; makes an alias r1 = ref M2u(); // Error; lhs and rhs have different types r1 = ref M3(); // error; M3 returns a ref readonly, which r1 cannot honor ref readonly int r2 = ref v; // OK; make readonly alias to ref r2 = ref M2(); // OK; makes an alias, adding read-only protection r2 = ref M3(); // OK; makes an alias and honors the read-only r2 = ref (r1 = ref M2()); // OK; r1 is an alias to a writable variable, // r2 is an alias (with read-only access) to the same variable }
Ende des Beispiels
Hinweis: Beim Lesen eines Codes mit einem
= ref
-Operator kann es verlockend sein, denref
-Teil als Teil des Operanden zu betrachten. Dies ist besonders verwirrend, wenn der Operand ein bedingter?:
-Ausdruck ist. Wenn Sie beispielsweiseref int a = ref b ? ref x : ref y;
lesen, sollte= ref
als Operator undb ? ref x : ref y
als der rechte Operand betrachtet werden:ref int a = ref (b ? ref x : ref y);
. Wichtig ist, dass der Ausdruckref b
kein Teil dieser Anweisung ist, obwohl er auf den ersten Blick so erscheinen könnte. Hinweisende
12.21.4 Verbundzuweisung
Wenn der linke Operand einer zusammengesetzten Zuweisung die Form E.P
oder E[Ei]
annimmt, wobei E
zur Kompilierungszeit den Typ dynamic
hat, wird die Zuweisung dynamisch gebunden (§12.3.3). In diesem Fall ist der Kompilierungszeittyp des Zuweisungsausdrucks dynamic
, und die im Folgenden beschriebene Auflösung erfolgt zur Laufzeit basierend auf dem Laufzeittyp E
. Wenn der linke Operand die Form E[Ei]
aufweist, in der mindestens ein Element von Ei
den Kompilierungszeittyp dynamic
hat und der Kompilierungszeittyp von E
kein Array ist, wird der resultierende Zugriff auf den Indexer dynamisch gebunden, aber mit eingeschränkter Kompilierungszeit-Überprüfung (§12.6.5).
Ein Vorgang der Form x «op»= y
wird durch Anwenden der binären Operatorüberladungsauflösung (§12.4.5) so verarbeitet, als wäre der Vorgang als x «op» y
geschrieben. Dies ergibt folgende Szenarien:
- Wenn der Rückgabetyp des ausgewählten Operators implizit in den Typ von
x
konvertierbar ist, wird der Vorgang alsx = x «op» y
ausgewertet, außer dassx
nur einmal ausgewertet wird. - Andernfalls, wenn der ausgewählte Operator ein vordefinierter Operator ist, der Rückgabetyp des ausgewählten Operators explizit in den Typ
x
konvertierbar ist undy
implizit in den Typx
konvertierbar ist oder der Operator ein Shift-Operator ist, wird der Vorgang alsx = (T)(x «op» y)
ausgewertet, wobeiT
der Typ vonx
ist, jedoch wirdx
nur einmal ausgewertet. - Andernfalls ist die Verbundzuweisung ungültig, und ein Bindungszeitfehler tritt auf.
Der Begriff "nur einmal ausgewertet" bedeutet, dass bei der Auswertung von x «op» y
die Ergebnisse aller Teilausdrücke von x
vorübergehend gespeichert und dann bei der Ausführung der Zuordnung zu x
wiederverwendet werden.
Beispiel: In der Zuweisung
A()[B()] += C()
, wobeiA
eine Methode ist, dieint[]
zurückgibt, undB
undC
Methoden sind, dieint
zurückgeben, werden die Methoden nur einmal in der ReihenfolgeA
,B
,C
aufgerufen. Ende des Beispiels
Wenn der linke Operand einer zusammengesetzten Zuweisung ein Zugriff auf Eigenschaften oder Indexer ist, muss die Eigenschaft oder der Indexer sowohl über einen get-Accessor als auch über einen set-Accessor verfügen. Wenn dies nicht der Fall ist, tritt ein Bindungszeitfehler auf.
Die zweite Regel oben erlaubt es, dass x «op»= y
in bestimmten Kontexten als x = (T)(x «op» y)
ausgewertet wird. Die Regel besteht so, dass die vordefinierten Operatoren als zusammengesetzte Operatoren verwendet werden können, wenn der linke Operand vom Typ sbyte
, byte
, short
, ushort
oder char
ist. Selbst wenn beide Argumente von einem dieser Typen sind, erzeugen die vordefinierten Operatoren ein Ergebnis vom Typ int
, wie in §12.4.7.3 beschrieben. Ohne eine Umwandlung wäre es daher nicht möglich, das Ergebnis dem linken Operanden zuzuweisen.
Der intuitive Effekt der Regel für vordefinierte Operatoren ist einfach, dass x «op»= y
erlaubt ist, wenn sowohl x «op» y
als auch x = y
erlaubt sind.
Beispiel: Im folgenden Code
byte b = 0; char ch = '\0'; int i = 0; b += 1; // OK b += 1000; // Error, b = 1000 not permitted b += i; // Error, b = i not permitted b += (byte)i; // OK ch += 1; // Error, ch = 1 not permitted ch += (char)1; // OK
Der intuitive Grund für jeden Fehler besteht darin, dass auch eine entsprechende einfache Zuweisung ein Fehler gewesen wäre.
Ende des Beispiels
Hinweis: Dies bedeutet auch, dass zusammengesetzte Zuweisungsvorgänge Lifted-Operatoren unterstützen. Da eine zusammengesetzte Zuweisung
x «op»= y
entweder alsx = x «op» y
oderx = (T)(x «op» y)
ausgewertet wird, decken die Bewertungsregeln implizit erweiterte Operatoren ab. Hinweisende
12.21.5 Ereigniszuweisung
Wenn der linke Operand von a += or -=
-Operator als Ereigniszugriff klassifiziert wird, wird der Ausdruck wie folgt ausgewertet:
- Der Instanzausdruck (falls vorhanden) des Ereigniszugriffs wird ausgewertet.
- Der rechte Operand des
+=
- oder-=
-Operators wird ausgewertet und, wenn erforderlich, durch implizite Konvertierung in den Typ des linken Operanden konvertiert (§10.2). - Ein Ereignisaccessor des Ereignisses wird aufgerufen, wobei eine Argumentliste besteht, die aus dem im vorherigen Schritt berechneten Wert besteht. Wenn der Operator
+=
ist, wird der add-Accessor aufgerufen; wenn der Operator-=
ist, wird der remove-Accessor aufgerufen.
Ein Ereigniszuweisungsausdruck liefert keinen Wert. Somit ist ein Ereigniszuweisungsausdruck nur im Kontext einer statement_expression gültig (§13.7).
12.22 Ausdruck
Ein Ausdruck ist entweder ein non_assignment_expression-Element oder eine Zuweisung.
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: declaration_expression
| conditional_expression
| lambda_expression
| query_expression
;
12.23 Konstante Ausdrücke
Ein konstanter Ausdruck ist ein Ausdruck, der während der Kompilierung vollständig ausgewertet werden soll.
constant_expression
: expression
;
Ein konstanter Ausdruck muss entweder den Wert null
oder einen der folgenden Typen haben:
-
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,bool
,string
; - ein Enumerationstyp; oder
- ein Standardwertausdruck (§12.8.21) für einen Referenztyp.
Nur die folgenden Konstrukte sind in konstanten Ausdrücken erlaubt:
- Literale, einschließlich des
null
-Literals). - Verweise auf
const
-Member von Klassen- und Strukturtypen. - Verweise auf Member von Enumerationstypen.
- Verweise auf lokale Konstanten.
- Unterausdrücke in Klammern, die selbst konstante Ausdrücke sind.
- Umwandlungsausdrücke
checked
- undunchecked
-Ausdrücke-
nameof
-Ausdrücke - Die vordefinierten
+
,-
,!
(logische Negation) und~
unären Operatoren. - Die vordefinierten
+
,-
,*
,/
,%
,<<
,>>
,&
,|
,^
,&&
,||
,==
,!=
,<
,>
,<=
und>=
binären Operatoren. - Der bedingte
?:
-Operator - Der nulltolerante
!
-Operator (§12.8.9). sizeof
-Ausdrücke, sofern der unmanaged-type einer der in §23.6.9 angegebenen Typen ist, für diesizeof
einen konstanten Wert zurückgibt.- Standardwertausdrücke, sofern der Typ einer der oben aufgeführten Typen ist.
Die folgenden Konversionen sind in konstanten Ausdrücken zulässig:
- Identitätskonversionen
- Numerische Konversionen
- Enumerationskonvertierungen
- Konvertierungen von konstanten Ausdrücken
- Implizite und explizite Referenzkonvertierungen, vorausgesetzt, die Quelle der Konvertierungen ist ein konstanter Ausdruck, der den Wert
null
ergibt.
Hinweis: Andere Umwandlungen, einschließlich der Boxing-, Unboxing- und impliziter Referenzumwandlungen von Nicht-
null
-Werten sind in konstanten Ausdrücken nicht zulässig. Hinweisende
Beispiel: Im folgenden Code
class C { const object i = 5; // error: boxing conversion not permitted const object str = "hello"; // error: implicit reference conversion }
Die Initialisierung von
i
ist ein Fehler, da eine Boxing-Konvertierung erforderlich ist. Die Initialisierung vonstr
stellt einen Fehler dar, da eine implizite Verweiskonvertierung von einem Wert, der keinnull
ist, erforderlich ist.Ende des Beispiels
Wenn ein Ausdruck die oben aufgeführten Anforderungen erfüllt, wird der Ausdruck zur Zeit der Kompilierung ausgewertet. Dies gilt auch dann, wenn der Ausdruck ein Unterausdruck eines größeren Ausdrucks ist, der nicht konstante Konstrukte enthält.
Für die Kompilierzeitauswertung von konstanten Ausdrücken gelten dieselben Regeln wie für die Laufzeitauswertung von nicht konstanten Ausdrücken, mit der Ausnahme, dass dort, wo die Laufzeitauswertung eine Ausnahme ausgelöst hätte, bei der Kompilierzeitauswertung ein Kompilierfehler auftritt.
Sofern ein konstanter Ausdruck nicht explizit in einem unchecked
-Kontext platziert wird, führen Überläufe, die bei arithmetischen Operationen und Typkonvertierungen im integralen Bereich während der Kompilierungszeitauswertung des Ausdrucks auftreten, immer zu Kompilierungsfehlern (§12.8.20).
Konstantenausdrücke sind in den unten aufgeführten Kontexten erforderlich, und dies wird in der Grammatik mithilfe von constant_expression angegeben. In diesen Kontexten tritt ein Kompilierfehler auf, wenn ein Ausdruck bei der Kompilierung nicht vollständig ausgewertet werden kann.
- Konstantendeklarationen (§15.4)
- Enumerationsmemberdeklarationen (§19.4)
- Standardargumente von Parameterlisten (§15.6.2)
-
case
-Bezeichnungen einerswitch
-Anweisung (§13.8.3). -
goto case
-Anweisungen (§13.10.4) - Dimensionslängen in einem Ausdruck zur Erstellung eines Arrays (§12.8.17.5), der einen Initialisierer enthält.
- Attribute (§22)
- In einem constant_pattern (§11.2.3)
Eine implizite Konversion von konstanten Ausdrücken (§10.2.11) erlaubt die Konvertierung eines konstanten Ausdrucks vom Typ int
in sbyte
, byte
, short
, ushort
, uint
oder ulong
, sofern der Wert des konstanten Ausdrucks im Bereich des Zieltyps liegt.
12.24 Boolesche Ausdrücke
Ein boolean_expression ist ein Ausdruck, der ein Ergebnis vom Typ bool
liefert; entweder direkt oder über die Anwendung von operator true
in bestimmten Kontexten, wie weiter unten angegeben:
boolean_expression
: expression
;
Der steuernder bedingter Ausdruck eines if_statement (§13.8.2), while_statement (§13.9.2), do_statement (§13.9.3) oder for_statement (§13.9.4) ist ein boolean_expression. Der steuernde bedingte Ausdruck des ?:
-Operators (§12.18) folgt den gleichen Regeln wie ein boolean_expression, aber aus Gründen der Operatorrangfolge, wird er als null_coalescing_expression klassifiziert.
boolean_expressionE
ist erforderlich, um einen Wert vom Typ bool
wie folgt zu erzeugen:
- Wenn E implizit in
bool
konvertierbar ist, wird zur Laufzeit diese implizite Konvertierung angewendet. - Andernfalls wird die Überladungsauflösung für unäre Operatoren (§12.4.4) verwendet, um eine eindeutig beste Implementierung von
operator true
inE
zu finden, die dann zur Laufzeit angewendet wird. - Wenn kein solcher Operator gefunden wird, tritt ein Bindungszeitfehler auf.
ECMA C# draft specification