Freigeben über


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 durch this 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 +, -, *, / und new. 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 Methode F mit dem alten Wert von i aufgerufen, dann wird Methode G mit dem alten Wert von i aufgerufen und schließlich wird Methode H 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 als x + (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.yx?.yf(x)a[x]a?[x]x++x--x!newtypeofdefaultcheckeduncheckeddelegatestackalloc
§12.9 Unär +-!x~++x--x(T)xawait x
§12.10 Multiplikativ */%
§12.10 Additiv +-
§12.11 Shift <<>>
§12.12 Relational und Typtest <><=>=isas
§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) + zbewertet. 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 als x = (y = z)bewertet. Ende des Beispiels

Priorität und Assoziativität können mithilfe von Klammern gesteuert werden.

Beispiel: x + y * z multipliziert zuerst y mit z und addiert dann das Ergebnis zu x, aber (x + y) * z addiert zuerst x und y und multipliziert dann das Ergebnis mit z. 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 und false 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 oder as 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 entsprechendes bool-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 Vorgang operator «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 und Y bereitgestellte Satz benutzerdefinierter Kandidatenoperatoren für den Vorgang operator «op»(x, y) ist bestimmt. Die Gruppe besteht aus der Vereinigung der von X und von Y 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 und Y identitätskonvertierbar sind oder X und Y von einem gemeinsamen Basistyp abgeleitet werden, treten gemeinsame Kandidatenoperatoren nur einmal in der kombinierten Menge auf.
    • Wenn es eine Identitätskonvertierung zwischen X und Y gibt, weist ein Operator «op»Y, der von Y bereitgestellt wird, denselben Rückgabetyp wie ein von «op»X bereitgestellter X 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 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₀. Wenn T ein nullbarer Werttyp ist, ist T₀ sein zugrunde liegender Typ; andernfalls ist T₀ gleich T.
  • Für alle operator «op»-Deklarationen in T₀ 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 Argumentliste T₀.
  • Andernfalls, wenn es sich bei T₀ um object 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 von T₀bereitstellt, oder der Satz der effektiven Basisklasse von T₀, wenn T₀ 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, wobei b ein byte ist und s ein short ist, wählt die Überladungsauflösung operator *(int, int) als besten Operator aus. Die Auswirkung ist daher, dass b und s in intkonvertiert werden. Der Typ des Ergebnisses ist int. Ebenso im Falle des Vorgangs i * d, wobei i ein int und d ein double ist, overload wird von der Auflösung operator *(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, ushortoder char in den Typ intzu 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 Typ decimal konvertiert, oder es tritt ein Bindungszeitfehler auf, wenn der andere Operand vom Typ float oder double ist.
  • Andernfalls, wenn einer der beiden Operanden vom Typ double ist, wird der andere Operand in den Typ double umgewandelt.
  • Andernfalls, wenn einer der beiden Operanden vom Typ float ist, wird der andere Operand in den Typ float umgewandelt.
  • Andernfalls, wenn ein Operand vom Typ ulongist, wird der andere Operand in den Typ ulongkonvertiert oder ein Bindungszeitfehler tritt auf, falls der andere Operand vom Typ type sbyte, short, intoder longist.
  • Andernfalls, wenn einer der beiden Operanden vom Typ long ist, wird der andere Operand in den Typ long umgewandelt.
  • Andernfalls, wenn einer der Operanden vom Typ uint ist und der andere Operand den Typ sbyte, shortoder inthat, werden beide Operanden in den Typ longkonvertiert.
  • Andernfalls, wenn einer der beiden Operanden vom Typ uint ist, wird der andere Operand in den Typ uint umgewandelt.
  • Andernfalls werden beide Operanden in den Typ int konvertiert.

Anmerkung: Die erste Regel verbietet alle Operationen, die den Typ decimal mit den Typen double und float vermischen. Die Regel ergibt sich aus der Tatsache, dass es keine impliziten Konversionen zwischen dem Typ decimal und den Typen double und float 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 von ulong 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 mit double multipliziert werden kann. Der Fehler wird behoben, indem der zweite Operand explizit in decimal 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 einen null-Wert, wenn der Operand null 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 einen null-Wert, wenn ein oder beide Operanden null sind (eine Ausnahme sind die Operatoren & und | des bool?-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 Ergebnistyp boolist. Das „Lifted” Formular wird erstellt, indem jedem Operanden ein einzelner ?-Modifizierer hinzugefügt wird. Der „Lifted” Operator betrachtet zwei null-Werte als gleich und einen null-Wert als ungleich zu jedem Wert ohne null. Wenn beide Operanden nicht null sind, öffnet der „Lifted” Operator die Operanden und wendet den zugrunde liegenden Operator an, um das Ergebnis bool 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 Ergebnistyp boolist. Das „Lifted” Formular wird erstellt, indem jedem Operanden ein einzelner ?-Modifizierer hinzugefügt wird. Der „Lifted” Operator erzeugt den Wert false, wenn ein oder beide Operanden null sind. Andernfalls entpackt der transformierte Operator die Operanden und wendet den zugrunde liegenden Operator an, um das Ergebnis bool 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 namens N in jedem der Typen, die als primäre Einschränkung oder sekundäre Einschränkung (§15.2.5) für T angegeben sind, zusammen mit der Gruppe der zugänglichen Member namens N in object.
    • Andernfalls besteht der Satz aus allen zugänglichen (§7.5) Membern mit dem Namen N in T, einschließlich geerbter Member und der zugänglichen Member mit dem Namen N in object. Wenn T ein konstruierter Typ ist, wird das Set der Mitglieder durch Ersetzen von Typargumenten wie in §15.3.3 beschrieben abgerufen. Member, die einen override-Modifizierer enthalten, werden aus der Gruppe ausgeschlossen.
  • Wenn K Null ist, werden als nächstes alle eingebetteten Typen, deren Deklarationen Typparameter enthalten, entfernt. Wenn K nicht Null ist, werden alle Mitglieder mit einer anderen Anzahl von Typparametern entfernt. Wenn K 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 (wobei S der Typ ist, in dem der Member M 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 von S deklariert sind, aus dem Set entfernt.
    • Wenn M eine Typdeklaration ist, werden alle in einem Basistyp von S deklarierten Nichttypen aus der Menge entfernt, und alle Typdeklarationen mit derselben Anzahl von Typparametern wie M in einem Basistyp von S werden ebenfalls aus der Menge entfernt.
    • Wenn M eine Methode ist, werden alle Nicht-Methoden-Member, die in einem Basistyp von S deklariert sind, aus der Gruppe entfernt.
  • Als nächstes werden Schnittstellenmember, die von Klassenmembern verdeckt sind, aus der Gruppe entfernt. Dieser Schritt hat nur Auswirkungen, wenn T ein Typparameter ist und T sowohl eine effektive Basisklasse als object als auch einen nicht leeren effektiven Schnittstellensatz aufweist (§15.2.5). Für jedes Element S.M in der Menge, wobei S der Typ ist, in dem das Element M deklariert wird, werden die folgenden Regeln angewendet, falls S eine Klassendeklaration ist, die nicht objectist:
    • 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 wie M, die in einer Schnittstellendeklaration deklariert sind, aus der Menge entfernt.
  • 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 oder dynamic ist, dann hat T keinen Basistyp.
  • Wenn T ein enum_type ist, sind die Basistypen von T die Klassentypen System.Enum, System.ValueType und object.
  • Wenn T ein struct_type ist, sind die Basistypen von T die Klassentypen System.ValueType und object.

    Hinweis: Ein nullable_value_type ist ein struct_type (§8.3.1). Hinweisende

  • Wenn T ein class_typeist, sind die Basistypen von T die Basisklassen von T, einschließlich des Klassentyps object.
  • Wenn T ein interface_type-Element ist, sind die Basistypen von T die Basisschnittstellen von T, und der Klassentyp ist object.
  • Wenn T ein array_type ist, sind die Basistypen von T die Klassentypen System.Array und object.
  • Wenn T ein Delegate-Typ ist, dann sind die Basistypen von T die Klassentypen System.Delegate und object.

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 und value Ausdrücke, die als Variablen oder Werte klassifiziert sind, T einen Ausdruck, der als Typ klassifiziert ist, F ist der einfache Name einer Methode und P 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 nicht static ist, ist der Instanzausdruck this.
T.F(x, y) Die Überladungsauflösung wird angewendet, um die beste Methode F in der enthaltenden Klasse oder Struktur Tauszuwählen. Ein Bindungszeitfehler tritt auf, wenn die Methode nicht static 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 Typ e angegeben wird. Ein Bindungszeitfehler tritt auf, wenn die Methode static ist. Die Methode wird mit dem Instanzausdruck e und der Argumentliste (x, y) aufgerufen.
Eigenschaftenzugriff P Der get-Accessor der Eigenschaft P in der enthaltenden Klasse oder Struktur wird aufgerufen. Wenn P nur einen Schreibzugriff hat, tritt ein Kompilierungszeitfehler auf. Wenn P nicht static ist, ist der Instanzausdruck this.
P = value Der set-Accessor der Eigenschaft P in der enthaltenden Klasse oder Struktur wird mit der Argumentliste (value) aufgerufen. Wenn P schreibgeschützt ist, tritt ein Kompilierungszeitfehler auf. Wenn P nicht static ist, ist der Instanzausdruck this.
T.P Der get-Accessor der Eigenschaft P in der enthaltenden Klasse oder Struktur T wird aufgerufen. Ein Kompilierfehler tritt auf, wenn P nicht static ist oder wenn P schreibgeschützt ist.
T.P = value Der set-Accessor der Eigenschaft P in der enthaltenden Klasse oder Struktur T wird mit der Argumentliste (value) aufgerufen. Wenn P nicht static ist oder P schreibgeschützt ist, tritt ein Kompilierungsfehler auf.
e.P Der Accessor der Eigenschaft P in der Klasse, Struktur oder Schnittstelle, die vom Typ der E angegeben wird, wird mit dem Instanzausdruck e aufgerufen. Wenn Pstatic ist oder P 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 von E angegeben ist, wird mit dem Instanz-Ausdruck e und der Argumentliste (value) aufgerufen. Wenn Pstatic ist oder P schreibgeschützt ist, tritt ein Bindungszeitfehler auf.
Ereigniszugriff E += value Der add-Accessor des Ereignisses E in der enthaltenden Klasse oder Struktur wird aufgerufen. Wenn E nicht static ist, ist der Instanzausdruck this.
E -= value Der remove-Accessor des Ereignisses E in der enthaltenden Klasse oder Struktur wird aufgerufen. Wenn E nicht static ist, ist der Instanzausdruck this.
T.E += value Der add-Accessor des Ereignisses E in der enthaltenden Klasse oder Struktur T wird aufgerufen. Ein Bindungszeitfehler tritt auf, wenn E nicht static ist.
T.E -= value Der remove-Accessor des Ereignisses E in der enthaltenden Klasse oder Struktur T wird aufgerufen. Ein Bindungszeitfehler tritt auf, wenn E nicht static ist.
e.E += value Der add-Accessor des Ereignisses E in der Klasse, Struktur oder Schnittstelle, die vom Typ der E angegeben wird, wird mit dem Instanzausdruck e aufgerufen. Ein Bindungszeitfehler tritt auf, wenn E static ist.
e.E -= value Der remove-Accessor des Ereignisses E in der Klasse, Struktur oder Schnittstelle, die vom Typ der E angegeben wird, wird mit dem Instanzausdruck e aufgerufen. Ein Bindungszeitfehler tritt auf, wenn E 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 Instanzausdruck e 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 Instanzausdruck e 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 und y 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); von M(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 Namen c 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 wird i selbst als Eingabeargument übergeben, da es als Variable klassifiziert ist und den gleichen Typ int hat wie der Eingabeparameter. In dem M1(i + 5)-Methodenaufruf wird eine unbenannte int-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 eine System.ArrayTypeMismatchException ausgelöst wird, da der tatsächliche Elementtyp von bstring ist und nicht object.

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 und string 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 Typ U hat und der entsprechende Parameter ein Wertparameter (§15.6.2.2) ist, erfolgt eine Ableitung für die Untergrenze (§12.6.3.10) vonUzuTᵢ.
  • Andernfalls, wenn Eᵢ einen Typ U 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) vonUzuTᵢ.
  • Andernfalls, wenn Eᵢ einen Typ U hat und der entsprechende Parameter ein Eingabeparameter ist (§15.6.2.3.2) und Eᵢ ein Eingabeargument ist, dann erfolgt eine genaue Ableitung (§12.6.3.9) vonUzuTᵢ.
  • Andernfalls, wenn Eᵢ einen Typ U hat und der entsprechende Parameter ein Eingabeparameter (§15.6.2.3.2) ist, erfolgt eine Ableitung für die Untergrenze (§12.6.3.10) vonUzuTᵢ.
  • 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
  • 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 Parametertyp Tᵢ, in dem die Ausgabetypen (§12.6.3.5) unfixed-Typvariablen Xₑ, 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 vonEmit 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 vonEmit 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 Ein einem Typ T auf die folgende Weise:

  • Wenn E eine anonyme Funktion mit abgeleitetem Rückgabetyp U (§12.6.3.13) ist, und T ein Delegattyp oder Ausdrucksstrukturtyp mit Rückgabetyp Tₓist, dann wird eine Untergrenzenableitung (§12.6.3.10) vonUzuTₓ vorgenommen.
  • Andernfalls, wenn E eine Methodengruppe ist und T ein Delegattyp oder Ausdrucksstrukturtyp mit Parametertypen T₁...Tᵥ und Rückgabetyp Tₓ ist, und die Überladungsauflösung von E mit den Typen T₁...Tᵥ eine einzelne Methode mit Rückgabetyp U ergibt, dann wird eine Ableitung für die UntergrenzevonUzuTₓ vorgenommen.
  • Andernfalls, wenn E ein Ausdruck vom Typ U ist, wird eine Ableitung für die UntergrenzevonUzuT vorgenommen.
  • Andernfalls werden keine Ableitungen vorgenommen.

12.6.3.8 Explizite Typableitungen für Parameter

Eine explizite Parametertypableitung wird aus einem Ausdruck Ein einen Typ T auf die folgende Weise vorgenommen:

  • Wenn E eine explizit eingegebene anonyme Funktion mit Parametertypen U₁...Uᵥ ist und T ein Delegattyp oder Ausdrucksstrukturtyp mit Parametertypen V₁...Vᵥ ist, erfolgt für jede Uᵢ eine genaue Ableitung (§12.6.3.9) vonUᵢin entsprechenden Vᵢ.

12.6.3.9 Exakte Ableitungen

Eine genaue Ableitungaus einem Typ Uin einen Typ V erfolgt wie folgt:

  • Wenn V einer der nicht korrigiertenXᵢ ist, wird U dem Satz exakter Grenzen für Xᵢ hinzugefügt.
  • Andernfalls werden V₁...Vₑ und U₁...Uₑ bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft:
    • V ist ein Arraytyp V₁[...] und U ist ein Arraytyp U₁[...] desselben Rangs.
    • V ist der Typ V₁? und U ist der Typ U₁
    • V ist ein konstruierter Typ C<V₁...Vₑ> und U ist ein konstruierter Typ C<U₁...Uₑ>
      Wenn eines dieser Fälle zutrifft, wird eine genaue Ableitung von jedem Uᵢ auf die entsprechende Vᵢ vorgenommen.
  • Andernfalls werden keine Ableitungen vorgenommen.

12.6.3.10 Untergrenzableitungen

Eine Untergrenzableitung von einem Typ Uin einen Typ V wird wie folgt vorgenommen:

  • Wenn V einer der nicht korrigiertenXᵢ ist, wird U dem Satz unterer Grenzen für Xᵢ hinzugefügt.
  • Andernfalls, wenn V der Typ V₁? und U der Typ U₁? sind, wird eine Untergrenzenableitung von U₁ in V₁ vorgenommen.
  • Andernfalls werden U₁...Uₑ und V₁...Vₑ bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft:
    • V ist ein Arraytyp V₁[...], und U ist ein Arraytyp U₁[...]desselben Rangs.
    • V ist einer von IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> oder IList<V₁> und U ist ein eindimensionales Array vom Typ U₁[]
    • V ist ein konstruierter class-, struct-, interface- oder delegate-Typ C<V₁...Vₑ>, und es gibt einen eindeutigen Typ C<U₁...Uₑ>, sodass für das U-Element (oder, wenn U von Typ parameter 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 von U nach C<T> vorgenommen wird, da U₁ auch X oder Y sein könnte).
      Wenn einer dieser Fälle zutrifft, wird wie folgt ein Rückschluss von jedem Uᵢ-Element in das entsprechende Vᵢ-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 VC<V₁...Vₑ> ist, hängt die Ableitung von dem i-th-Typparameter von C 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 Uin einen Typ V wird wie folgt vorgenommen:

  • Wenn V einer der unbestimmtenXᵢ ist, wird U der Menge der oberen Begrenzungen für Xᵢ hinzugefügt.
  • Andernfalls werden V₁...Vₑ und U₁...Uₑ bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft:
    • U ist ein Arraytyp U₁[...], und V ist ein Arraytyp V₁[...]desselben Rangs.
    • U ist einer von IEnumerable<Uₑ>, ICollection<Uₑ>, IReadOnlyList<Uₑ>, IReadOnlyCollection<Uₑ> oder IList<Uₑ> und V ist ein eindimensionales Array vom Typ Vₑ[]
    • U ist der Typ U1? und V ist der Typ V1?
    • U ist ein Konstruktor für die Klasse, Struktur, Schnittstelle oder Delegattyp C<U₁...Uₑ>, und V ist ein Typ class, struct, interface oder delegate, der mit identical zu, inherits von (direkt oder indirekt) verwandt ist oder (direkt oder indirekt) einen eindeutigen Typ C<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 von C<U₁> auf V<Q> erfolgt. Es werden keine Rückschlüsse von U₁ auf X<Q> oder Y<Q> gezogen.)
      Wenn einer dieser Fälle zutrifft, wird wie folgt ein Rückschluss von jedem Uᵢ-Element in das entsprechende Vᵢ-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 UC<U₁...Uₑ> ist, hängt die Ableitung von dem i-th-Typparameter von C 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 KandidatentypenUₑ beginnt als Menge aller Typen in der Begrenzungsgruppe für Xᵢ.
  • Jede Bindung für Xᵢ wird wiederum untersucht: Für jede genaue Grenze U von Xᵢ werden alle Typen Uₑ, die nicht mit U identisch sind, aus der Kandidatenmenge entfernt. Für jede untere Grenze U von Xᵢ werden alle Typen Uₑ, in die keine implizite Konvertierung aus U vorhanden ist, aus der Kandidatenmenge entfernt. Für jede obere Grenze U von Xᵢ werden alle Typen Uₑ, in die keine implizite Konvertierung in U vorhanden ist, aus der Kandidatenmenge entfernt.
  • Wenn es unter den verbleibenden Kandidatentypen Uₑ einen eindeutigen Typ V gibt, für den es eine implizite Konvertierung von allen anderen Kandidatentypen gibt, wird Xᵢ auf Vfestgelegt.
  • 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 von F der Typ dieses Ausdrucks.
  • Wenn der Textkörper von F ein Block ist und der Satz von Ausdrücken in den return-Anweisungen des Blocks einen am häufigsten verwendeten Typ T (§12.6.3.15) aufweist, ist der abgeleitete effektive Rückgabetyp F 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 von F entweder ein Ausdruck ist, der als nichts klassifiziert ist (§12.2), oder ein Block, bei dem keine return-Anweisungen Ausdrücke aufweisen, ist der abgeleitete Rückgabetyp «TaskType» (§15.15.1).
  • Wenn F asynchron ist und einen abgeleiteten effektiven Rückgabetyp T hat, ist der abgeleitete Rückgabetyp «TaskType»<T>»(§15.15.1).
  • Wenn F nicht asynchron ist und einen abgeleiteten effektiven Rückgabetyp T hat, ist der abgeleitete Rückgabetyp T.
  • 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 der System.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 einer using namespace-Anweisung importiert, und es ist eine Klasse Customer mit der Name-Eigenschaft vom Typ string vorhanden, kann die Select-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 auf Customer abgeleitet wird. Dann wird mit Hilfe des oben beschriebenen anonymen Funktionstyp-Ableitungsprozesses c der Typ Customer gegeben, und der Ausdruck c.Name wird mit dem Rückgabetyp des Selektorparameters in Beziehung gesetzt, was TResult zu string macht. Daher ist der Aufruf gleichbedeutend mit

Sequence.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 auf string geschlossen wird. Dann erhält der Parameter der ersten anonymen Funktion, s, den abgeleiteten Typ string, und der Ausdruck TimeSpan.Parse(s) wird mit dem Rückgabetyp von f1 in Beziehung gesetzt, wodurch Y zu System.TimeSpan wird. Schließlich wird dem Parameter der zweiten anonymen Funktion, t, der abgeleitete Typ System.TimeSpanzugewiesen. Der Ausdruck t.TotalHours steht in Beziehung zum Rückgabetyp von f2, wodurch Z als doubleabgeleitet wird. Daher ist das Ergebnis des Aufrufs vom Typ double.

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 Din 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 in X 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 dem Eᵢ als Argumente und dem Ableiten von X. 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. Wenn A 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.

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 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ᵥ nach Qᵥ nicht besser als die implizite Konversion von Eᵥ nach Pᵥ, und
  • für mindestens ein Argument ist die Konversion von Eᵥ nach Pᵥ besser als die Konversion von Eᵥ nach Qᵥ.

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 und Mₑ eine generische Methode, dann ist Mᵢ besser als Mₑ.
  • Andernfalls, wenn Mᵢ in seiner normalen Form anwendbar ist und Mₑ ein Parameterarray hat und nur in seiner erweiterten Form anwendbar ist, dann ist Mᵢ besser als Mₑ.
  • 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 von Mₑ, dann ist Mᵢ besser als Mₑ.
  • Andernfalls, wenn Mᵥ spezifischere Parametertypen hat als Mₓ, dann ist Mᵥ besser als Mₓ. Lassen Sie {R1, R2, ..., Rn} und {S1, S2, ..., Sn} die nicht instanziierten und nicht erweiterten Parametertypen von Mᵥ und Mₓdarstellen. Die Parametertypen von Mᵥ sind spezifischer als die von Mₓ, wenn für jeden Parameter Rx nicht weniger spezifisch ist als Sx und für mindestens einen Parameter Rx spezifischer ist als Sx:
    • 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 in MₓStandardargumente eingesetzt werden müssen, ist Mᵥ besser als Mₓ.
  • Wenn für mindestens einen Parameter Mᵥ die die bessere Wahl für die Parameterübergabe (§12.6.4.4) als der entsprechende Parameter in Mₓ verwendet und keine der Parameter in Mₓ bessere Wahl für die Parameterübergabe als Mᵥ verwenden, ist Mᵥ besser als Mₓ.
  • 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 genau T₁ und E entspricht nicht genau T₂ (§12.6.4.6)
  • E stimmt genau mit den beiden Elementen T₁ und T₂ oder keinem dieser Elemente überein, und T₁ ist ein besseres Konvertierungsziel als T₂ (§12.6.4.7).
  • E ist eine Methodengruppe (§12.2), T₁ ist kompatibel (§20.4) mit der einzigen besten Methode aus der Methodengruppe zur Konvertierung C₁, und T₂ ist nicht kompatibel mit der einzigen besten Methode aus der Methodengruppe zur Konvertierung C₂

12.6.4.6 Genau übereinstimmender Ausdruck

Bei einem Ausdruck E und einem Typ T, liegt bei Eeine genaue ÜbereinstimmungT vor, wenn eine der folgenden Bedingungen erfüllt ist:

  • E weist einen Typ S auf, und eine Identitätskonvertierung von S in T ist vorhanden.
  • E ist eine anonyme Funktion, T ist entweder ein Delegattyp D oder ein Ausdrucksbaumtyp Expression<D> und einer der folgenden Faktoren trifft zu:
    • Es gibt einen abgeleiteten Rückgabetyp X für E im Kontext der Parameterliste von D (§12.6.3.12), und es ist eine Identitätsumwandlung von X zu dem Rückgabetyp von D vorhanden.
    • E ist eine async-Lambda ohne Rückgabewert und D hat einen Rückgabetyp, der ein nicht generischer «TaskType» ist
    • Entweder ist E nicht asynchron und D hat einen Rückgabetyp Y, oder E ist asynchron und D 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 mit Y übereinstimmt.
      • Der Textkörper von E ist ein Block, in dem jede Rückgabeanweisung einen Ausdruck zurückgibt, der genau mit Y übereinstimmt.

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₁ nach T₂ existiert und keine implizite Konversion von T₂ nach T₁ existiert
  • T₁ ist «TaskType»<S₁>(§15.15.1), T₂ ist «TaskType»<S₂>, und S₁ ist ein besseres Konvertierungsziel als S₂.
  • T₁ ist «TaskType»<S₁>(§15.15.1), T₂ ist «TaskType»<S₂>, und T₁ ist spezieller als T₂
  • T₁ ist S₁ oder S₁?, wobei S₁ ein Integraltypen mit Vorzeichen ist, und T₂ ist S₂ oder S₂?, wobei S₂ ein Integraltypen ohne Vorzeichen ist. Konkret:
    • S₁ ist sbyte und S₂ ist byte, ushort, uintoder ulong
    • S₁ ist short und S₂ ist ushort, uintoder ulong
    • S₁ ist int und S₂ ist uint oder ulong
    • S₁ ist long, und S₂ ist ulong.

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 Werttyp V ist und M in V 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 wird E als Variable klassifiziert.
    • Wenn E nicht als Variable eingestuft ist oder V kein readonly-Strukturtyp ist (§16.2.2) und E 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 von E wird dieser Variablen zugewiesen. E wird dann als Referenz auf diese temporäre lokale Variable neu klassifiziert. Auf die temporäre Variable kann als this innerhalb von Mzugegriffen werden, jedoch nicht auf andere Weise. Nur wenn E geschrieben werden kann, kann der Aufrufer die Änderungen beobachten, die M an this vornimmt.

    • Die Argumentliste wird wie in §12.6.2 beschrieben ausgewertet.
    • M wird aufgerufen. Die Variable, auf die E verweist, wird zu der Variable, auf die this 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, um E in einen class_type zu konvertieren, und E wird in den folgenden Schritten als dieser class_type betrachtet. Wenn die value_type ein enum_type ist, ist der class_type andernfalls System.Enum;, ist er System.ValueType.
    • Der Wert von E wird auf seine Gültigkeit geprüft. Wenn der Wert von E Null ist, wird ein System.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 von M, die vom Laufzeittyp der Instanz bereitgestellt wird, auf die von E verwiesen wird. Dieses Funktionsmitglied wird durch Anwendung der Regeln für die Zuordnung von Schnittstellen (§18.6.5) bestimmt, um die Implementierung von M zu ermitteln, die durch den Laufzeittyp der von E referenzierten Instanz bereitgestellt wird.
      • Andernfalls, wenn M ein virtuelles Funktionsmember ist, ist das aufzurufende Funktionsmember die Implementierung von M, die vom Laufzeittyp der durch E referenzierten Instanz bereitgestellt wird. Dieses Funktionsmitglied wird durch Anwendung der Regeln zur Bestimmung der am weitesten abgeleiteten Implementierung (§15.6.4) von M in Bezug auf den Laufzeittyp der Instanz, die von E referenziert wird, bestimmt.
      • Andernfalls ist M ein nicht-virtuelles Funktionsmitglied, und das aufzurufende Funktionsmitglied ist M selbst.
    • 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 oder System.EnumHinweisende

  • 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 mit n-Elementen ist, ist das Ergebnis der Dekonstruktion der Ausdruck E selbst.
  • Andernfalls, wenn E einen Tupeltyp (T1, ..., Tn) mit n-Elementen aufweist, wird E 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 und pointer_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) und System.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) und System.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ür System.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 Zahl I von 0 bis N-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 (})
    • Die Zeichen der Interpolated_Regular_String_Mid oder Interpolated_Verbatim_String_Mid unmittelbar nach der entsprechenden Interpolation, falls vorhanden
  • 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 Namen I 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 Namen I 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 von T einen Typparameter mit dem Namen I enthält, dann bezieht sich simple_name auf diesen Typparameter.
    • Andernfalls, wenn eine Membersuche (§12.5) von I in T mit e-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 von this. 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 Form this.I. Dies kann nur geschehen, wenn e Null ist.
      • Andernfalls entspricht das Ergebnis einem Memberzugriff (§12.8.7) im Format T.I oder T.I<A₁, ..., Aₑ>.
  • 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 und I der Name eines Namespace in N 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 Namen I 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 in N.
    • Andernfalls, wenn N einen zugänglichen Typ mit Namen I und e-Typparametern enthält, dann:
      • Wenn e Null ist und der Speicherort, an dem simple_name auftritt, durch eine Namespacedeklaration für N umschlossen wird und die Namespacedeklaration ein extern_alias_directive- oder using_alias_directive-Element enthält, das den Namen I 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.
    • 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 Namen I 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 und e-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 Typparametern e enthalten, wird der simple_name mehrdeutig und es tritt ein Kompilierfehler auf.

    Hinweis: Dieser gesamte Schritt ist genau parallel zu dem entsprechenden Schritt bei der Verarbeitung eines namespace_or_type_name (§7.8). Hinweisende

  • Andernfalls, wenn e Null ist und I 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 Nihat, dann muss das Element des Tupeltyps Ti Nisein.
  • Andernfalls ist Ei der Form Ni oder E.Ni oder E?.Ni, dann muss das Tupeltypelement Ti 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 oder E?.Ni auf.
    • Ni hat die Form ItemX, wobei X eine Sequenz von nicht mit0beginnenden Dezimalziffern ist, die möglicherweise die Position eines Tupelelements darstellen, und wobei X die Position des Elements nicht darstellt.
  • 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 und t2, verwenden nicht den Typ des Tupelausdrucks, sondern wenden stattdessen eine implizite Tupelkonvertierung an. Bei t2 basiert die implizite Tupelkonvertierung auf den impliziten Konvertierungen von 2 zu long und von null zu string. Der dritte Tupelausdruck weist einen Typ (int i, string) auf und kann daher als Wert dieses Typs neu klassifiziert werden. Die Deklaration von t4 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 und E ein Namespace ist und E einen geschachtelten Namespace mit dem Namen Ienthält, dann ist das Ergebnis dieser Namespace.
  • Andernfalls, wenn E ein Namespace ist und E einen zugänglichen Typ mit dem Namen I und den K-Typparametern enthält, ist das Ergebnis dieser Typ, der mit den angegebenen Typargumenten erstellt wurde.
  • Wenn E als Typ klassifiziert wird, wenn E kein Typparameter ist und eine Membersuche (§12.5) von I in E mit den K-Typparametern eine Übereinstimmung ergibt, wird E.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 in E.
      • Andernfalls ist das Ergebnis eine Variable, nämlich das statische Feld I in E.
    • 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äre I ein statisches Feld.
      • Andernfalls ist das Ergebnis ein Ereigniszugriff ohne zugeordneten Instanzausdruck.
    • 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 E ein Eigenschaftszugriff, ein Indexerzugriff, eine Variable oder ein Wert ist, dessen Typ T ist, und eine Membersuche (§12.5) von I in T mit K Typargumenten übereinstimmt, dann wird E.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 von E.
    • Wenn I eine Instanzeigenschaft identifiziert, dann ist das Ergebnis ein Eigenschaftszugriff mit einem verbundenen Instanzausdruck von E und einem verbundenen Typ, der dem Typ der Eigenschaft entspricht. Wenn T ein Klassentyp ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Eigenschaft gewählt, die gefunden wird, indem bei T begonnen und die Basisklassen durchsucht werden.
    • Wenn T ein -Klassentyp ist und I ein Instanzfeld dieses -Klassentypsidentifiziert:
      • Wenn der Wert von Enull ist, wird ein System.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 von E verwiesen wird.
      • Andernfalls ist das Ergebnis eine Variable, nämlich das Feld I in dem von E referenzierten Objekt.
    • Wenn T ein Strukturtyp ist und I 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 Felds I in der Strukturinstanz, die durch E angegeben wird.
      • Andernfalls ist das Ergebnis eine Variable, nämlich das Feld I in der Strukturinstanz, die von E angegeben wird.
    • 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, wird E.I genau so verarbeitet, als wäre I ein Instanzfeld.
      • Andernfalls ist das Ergebnis ein Ereigniszugriff mit einem zugeordneten Instanzausdruck von E.
  • Andernfalls wird versucht, E.I als Erweiterungsmethodenaufruf zu verarbeiten (§12.8.10.3). Wenn dies fehlschlägt, ist E.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 Bezeichners Color, die auf den Typ Color verweisen, durch «...» begrenzt, während diejenigen, die auf das Feld Color 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 von P.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 von E T? und die Bedeutung von E 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 von E 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 Ausdrucks P.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 von E T? und die Bedeutung von E 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 von E 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 zu null ausgewertet wird, werden weder A₀ noch A₁ 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 IsValidtruezurückgibt, kann p sicher dereferenziert werden, um auf seine Name-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. Wenn x jedoch null ist, wird zur Laufzeit eine Ausnahme ausgelöst, da null nicht in int 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, sind string?, wobei lv ein Ausgabeparameter ist und eine einfache Zuordnung durchführt.

Die Methode M übergibt die Variable s vom Typ string als Ausgabewert von Assign. Der verwendete Compiler gibt eine Warnung aus, da s keine Nullwertvariable ist. Da das zweite Argument von Assign 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 mit T 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 mit T 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 Methodengruppe Mzugeordnet ist:
    • Wenn F nicht generisch ist, ist F ein Kandidat, wenn:
      • M keine Typargumentliste hat, und
      • F ist in Bezug auf A anwendbar (§12.6.4.2).
    • Wenn F generisch ist und M keine Liste mit Typargumenten hat, ist F 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 von F ist in Bezug auf A anwendbar (§12.6.4.2).
    • Wenn F generisch ist und M eine Typargumentliste enthält, ist F 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 von F gilt mit Bezug auf A (§12.6.4.2).
  • 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, wobei C der Typ ist, in dem die Methode F deklariert wird, werden alle Methoden, die in einem Basistyp von C deklariert sind, aus der Menge entfernt. Außerdem werden, wenn C ein anderer Klassentyp als object 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 Erweiterungsmethoden Mₑ 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 Erweiterungsmethoden Mₑ enthalten, wird die Menge dieser Erweiterungsmethoden zur Kandidatenmenge.
  • 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 BVorrang vor der ersten Erweiterungsmethode, und die Methode von Chat 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 vor C.G, und E.F hat Vorrang vor D.F und C.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 von Dnull ist, wird ein System.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 Typ short ist, wird eine implizite Konversion nach int durchgeführt, da implizite Konversionen von short nach int und von short nach long 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 von Pnull ist, wird ein System.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 ein System.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 Tist und A eine argument_list ist, besteht aus den folgenden Schritten:

  • Die von T bereitgestellte Indexergruppe wird konstruiert. Der Satz besteht aus allen in T deklarierten Indexern oder einem Basistyp von T, 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, wobei S der Typ ist, in dem der Indexer I deklariert ist:
    • Falls I nicht auf A zutrifft (§12.6.4.2), wird I aus der Menge entfernt.
    • Falls I in Bezug auf A (§12.6.4.2) anwendbar ist, werden alle in einem Basistyp von S deklarierten Indexer aus der Gruppe entfernt.
    • Wenn I in Bezug auf A (§12.6.4.2) gilt und S ein anderer Klassentyp als object ist, werden alle in einer Schnittstelle deklarierten Indexer aus der Menge entfernt.
  • 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 von A sowie einen zugehörigen Typ, der dem Typ des Indexers entspricht. Wenn T ein Klassentyp ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung des Indexers gewählt, der gefunden wird, indem bei T 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 Ausdrucks P.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 von E T? und die Bedeutung von E 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 von E 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 Ausdrucks P[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 von E T? und die Bedeutung von E 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 von E 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 zu null ausgewertet wird, werden weder A₀ noch A₁ 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 ein ref-Parameter des Struct-Typs. Dies bedeutet insbesondere, dass die Variable anfänglich zugewiesen ist.
  • 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 die this-Variable genau wie ein Eingabeparameter des Struct-Typs.
      • Andernfalls verhält sich die this-Variable genau wie ein ref-Parameter des Struct-Typs.
    • 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.

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 thisist base 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 von x 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 nicht static ist) und die Argumentliste (wenn x ein Indexerzugriff ist), die mit x 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 von x wird mit diesem Wert als Wertargument aufgerufen.
    • Der gespeicherte Wert von x wird das Ergebnis der Operation.

Die Operatoren ++ und -- unterstützen auch Präfixnotation (§12.9.6). Das Ergebnis von x++ oder x-- ist der Wert von xvor der Operation, während das Ergebnis von ++x oder --x dagegen der Wert von xnach 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 und A 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ür T, wie in §8.3.3definiert.
  • Andernfalls, wenn T ein type_parameter ist und A 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.
  • 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 Tdeklarierten zugänglichen Instanzenkonstruktoren, die für A 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.

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 ein System.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.
  • 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.

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 eingebetteten Point-Instanzen zuordnet, können sie verwendet werden, um die eingebetteten Point-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 Typ int[,], und der Array-Erstellungsausdruck new int[10][,] erzeugt eine Arrayinstanz vom Typ int[][,]. 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 ist null. Es ist nicht möglich, dass ein und derselbe Ausdruck zum Erstellen eines Arrays auch die Unterarrays erzeugt, und die Anweisung

int[][] 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 noch string 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 Typ object[] 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) von E in D verarbeitet.

  • Wenn E eine anonyme Funktion ist, wird der Delegaterstellungsausdruck auf dieselbe Weise wie eine anonyme Funktionskonvertierung (§10.7) von E in D verarbeitet.

  • Wenn E ein Wert ist, muss E kompatibel sein (§20.2) mit D, und das Ergebnis ist ein Verweis auf einen neu erstellten Delegat mit einer Aufrufliste, die nur einen Eintrag enthält und E 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) von E in D ausgewertet.
  • Wenn E eine anonyme Funktion ist, wird die Delegatenerstellung als anonyme Funktionskonversion von E nach D 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 Enull ist, wird ein System.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 ein System.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 zweite Square-Methode verweist, da diese Methode exakt mit der Parameterliste und dem Rückgabetyp von DoubleFunc übereinstimmt. Wäre die zweite Square 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 und p2 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 dynamicsein.

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ßlich void-Methoden, dazu verwendet werden, mit einer Instanz von System.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 und System.Int32 derselbe Typ sind. Das Ergebnis von typeof(X<>) hängt nicht vom Typargument ab, aber das Ergebnis von typeof(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 oder double 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, eine System.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-Methode System.OverflowException aus, und die G-Methode gibt –727379968 zurück (die unteren 32 Bit des Ergebnisses vom Typ „Wert außerhalb des gültigen Bereichs“). Das Verhalten der H-Methode hängt vom standardmäßigen Kontext der Überlaufprüfung für die Kompilierung ab, ist jedoch entweder identisch mit F oder identisch mit G.

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 und H auftreten, verursachen Kompilierungsfehler, da die Ausdrücke in einem checked-Kontext ausgewertet werden. Ein Überlauf tritt auch beim Evaluieren des konstanten Ausdrucks in G auf, aber da die Evaluierung in einem unchecked-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 von x * y in Multiply aus. Daher wird x * 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 des int-Bereichs befinden, würden die Umwandlungen in unchecked ohne den int-Operator zu Kompilierungsfehlern führen.

Ende des Beispiels

Hinweis: Die checked und unchecked 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 span8führt stackalloc zu einer Span<int>, die von einem impliziten Operator in ReadOnlySpan<int> konvertiert wird. Ebenso wird für span9 die resultierende Span<double> mithilfe der Umwandlung in den benutzerdefinierten Typ Widget<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 Typ List<T> im System.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 von nameof(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 von X der kleinste darstellbare Wert des Operandentyps ist (-2³¹ für int oder -2⁶³ für long), dann ist die mathematische Negation von X innerhalb des Operandentyps nicht darstellbar. Wenn dies innerhalb eines checked-Kontexts erfolgt, wird ein System.OverflowException ausgelöst; tritt es innerhalb eines unchecked-Kontexts auf, ist das Ergebnis der Wert des Operanden und der Überlauf wird nicht gemeldet.

    Wenn der Operand des Negationsoperators vom Typ uintist, wird er in den Typ longkonvertiert, und der Typ des Ergebnisses ist long. Eine Ausnahme ist die Regel, die es erlaubt, den int 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 Wert long−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. Wenn xNaN ist, ist das Ergebnis NaN.

  • 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 Typ System.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 Uist, 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 von x gegeben ist und wird zum Ergebnis der Operation.
  • Wenn x als Zugriff auf eine Eigenschaft oder einen Indexer klassifiziert wird:
    • Der Instanzausdruck (wenn x nicht static ist) und die Argumentliste (wenn x ein Indexerzugriff ist), die mit x 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 von x wird mit diesem Wert als Wertargument aufgerufen.
    • Dieser Wert wird ebenfalls das Ergebnis der Operation.

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 Typ x) oder ein additive_expression kombiniert mit einem parenthesized_expression (der den Wert x – 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ßer as und is.

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 und y Bezeichner sind, dann ist x.y eine korrekte Grammatik für einen Typ, auch wenn x.y nicht wirklich einen Typ bezeichnet. Ende des Beispiels

Hinweis: Aus der Regel zur Sicherstellung der Eindeutigkeit folgt, dass, wenn x und y Bezeichner sind, (x)y, (x)(y) und (x)(-y)cast_expressions, jedoch kein (x)-y ist, auch wenn x einen Typ bezeichnet. Wenn jedoch x 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 Kompilierungszeittyp dynamic
  • t verfügt über eine zugängliche Instanz- oder Erweiterungsmethode namens GetAwaiter ohne Parameter und Typparameter und einen Rückgabetyp A, für den alle folgenden Bedingungen gelten:
    • A implementiert die Schnittstelle System.Runtime.CompilerServices.INotifyCompletion (nachfolgend unter INotifyCompletion zur Vereinfachung bezeichnet)
    • A verfügt über eine zugängliche, lesbare Instanz-Eigenschaft IsCompleted vom Typ bool
    • A verfügt über eine zugängliche Instanzmethode GetResult 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 boolb wird durch Auswerten des Ausdrucks (a).IsCompleted erzielt.
  • Wenn b false ist, hängt die Auswertung davon ab, ob a die Schnittstelle System.Runtime.CompilerServices.ICriticalNotifyCompletion implementiert (im Folgenden der Kürze halber als ICriticalNotifyCompletion bezeichnet). Diese Überprüfung erfolgt zur Bindungszeit; d. h. zur Laufzeit, wenn a den Kompilierungszeittyp dynamic aufweist, und andernfalls zur Kompilierungszeit. Lassen Sie r den Wiederaufnahmedelegat bezeichnen (§15.15):
    • Wenn aICriticalNotifyCompletion nicht implementiert, wird der Ausdruck ((a) as INotifyCompletion).OnCompleted(r) ausgewertet.
    • Wenn aICriticalNotifyCompletion 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.
  • Entweder unmittelbar nach (wenn btrue war) oder nach einem späteren Aufruf des Wiederaufnahmedelegats (wenn bfalse 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 ein System.OverflowException ausgelöst. In einem unchecked-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 und y positive endliche Werte. z ist das Ergebnis von x * y, gerundet auf den nächstliegenden darstellbaren Wert. Wenn die Größe des Ergebnisses zu groß für den Zieltyp ist, ist z unendlich. Aufgrund der Rundung kann z Null sein, auch wenn weder x noch y 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 Typ System.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 oder long ist und der rechte Operand –1 ist, tritt ein Überlauf auf. In einem checked-Kontext verursacht dies das Auslösen einer System.ArithmeticException (oder einer Unterklasse davon). In einem unchecked-Kontext wird durch die Implementierung bestimmt, ob eine System.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 und y positive endliche Werte. z ist das Ergebnis von x / 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 ein System.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 von x minus der Maßstab von y.

    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 durch x – (x / y) * y erzeugte Wert. Wenn y null ist, wird ein System.DivideByZeroException ausgelöst.

    Wenn der linke Operand der kleinste int- oder long-Wert ist und der rechte Operand –1 ist, wird ein System.OverflowException nur dann ausgelöst, wenn x / 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 und y positive endliche Werte. z ist das Ergebnis von x % y und wird berechnet als x – n * y, wobei n die größtmögliche ganze Zahl ist, die kleiner oder gleich x / 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 der n die Ganzzahl ist, die x / 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 ein System.ArithmeticException (oder eine Unterklasse davon) ausgelöst wird. Eine konforme Implementierung darf in keinem Fall eine Ausnahme für x % y auslösen, wenn x / 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 von x.

    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 ein System.OverflowException ausgelöst. In einem unchecked-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 und y endliche Werte ungleich Null, und z ist das Ergebnis aus x + y. Wenn x und y die gleiche Größe jedoch andere Vorzeichen haben, ist z eine positive Null. Wenn x + y zu groß ist, um im Zieltyp darzustellen, ist z eine Unendlichkeit mit demselben Zeichen wie x + 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 und U der zugrunde liegende Typ von E 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 Zeichenfolgenverkettung null ist, wird eine leere Zeichenfolge ersetzt. Andernfalls wird jeder Nicht-string-Operand in seine Zeichenfolgendarstellung konvertiert, indem die virtuelle ToString-Methode, die vom Typ object geerbt wurde, aufgerufen wird. Wenn ToStringnull 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 einen null-Wert zurück. Eine System.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 ebenfalls null ist). Andernfalls, wenn der zweite Operand nullist, 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 ein System.OverflowException ausgelöst. In einem unchecked-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 und y endliche Werte ungleich Null, und z ist das Ergebnis aus x – y. Wenn x und y gleich sind, ist z eine positive Null. Wenn x – y zu groß ist, um im Zieltyp darzustellen, ist z eine Unendlichkeit mit demselben Vorzeichen wie x – 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 von y, 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 und U der zugrunde liegende Typ von E 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 von x und y, 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 nullist, ist das Ergebnis der Operation null.
    • Andernfalls, wenn der zweite Operand nullist, 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.

    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

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 verschiebt x 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 verschiebt x um eine Anzahl von Bits nach rechts, die wie unten beschrieben berechnet wird.

    Wenn x vom Typ int oder longist, werden die niederwertigen Bits von x verworfen, die verbleibenden Bits nach rechts verschoben, und die höherwertigen leeren Bitpositionen auf Null gesetzt, wenn x nicht negativ ist, oder auf Eins gesetzt, wenn x negativ ist.

    Wenn x vom Typ uint oder ulongist, werden die niedrigwertigen Bits von x 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 xint oder uint ist, wird die Anzahl der Verschiebungen durch die niederwertigen fünf Bits von count bestimmt. Mit anderen Worten, die Anzahl der Verschiebungen wird aus count & 0x1F berechnet.
  • Wenn der Typ von xlong oder ulong ist, wird die Anzahl der Verschiebungen durch die niederwertigen sechs Bits von count bestimmt. Mit anderen Worten, die Anzahl der Verschiebungen wird aus count & 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 Typ intist, führt der Vorgang unchecked ((int)((uint)x >> y)) eine logische Verschiebung nach rechts von xaus. 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 ==, !=, <, >, <=, >=, isund 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 trueist. 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 und y NaN ist, dann ist x < yfalse, aber !(x >= y) ist true. 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 Usind 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 Cstring 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 Typ T, wobei T 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 der HasValue-Eigenschaft des Operanden berechnet, wie in (§12.12.10) beschrieben.
    • Sollte T zur Laufzeit ein Verweistyp sein, ist das Ergebnis true, wenn der Operand null ist, andernfalls false.

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 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. 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: 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, obwohl T einen nicht-nullable Werttyp darstellen kann, und das Ergebnis wird einfach als false definiert, wenn T 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 und t beziehen sich auf zwei verschiedene Instanzen von Zeichenfolgen, die dieselben Zeichen enthalten. Die erste Vergleichsausgabe ist True, weil der vordefinierte Zeichenfolgen-Gleichheitsoperator (§12.12.8) ausgewählt wird, wenn beide Operanden vom Typ string sind. Die restlichen Vergleiche ergeben alle False, da die Überladung von operator == im Typ string nicht anwendbar ist, wenn ein Operand einen Bindungszeittyp von object 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 geschachtelten int-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 beide null 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 und yi in der lexikalischen Reihenfolge:
    • Der Operator xi == yi wird ausgewertet, und ein Ergebnis vom Typ bool 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 Operator false dynamisch darauf angewendet, und der daraus resultierende bool-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 falsehat, wird dieser Operator aufgerufen und der resultierende bool-Wert mit dem logischen Negationsoperator (!) negiert.
    • Wenn das resultierende boolfalse ist, findet keine weitere Auswertung statt, und das Ergebnis des Tupelgleichheitsoperators ist false.
  • Wenn alle Elementvergleiche true ergeben haben, ist das Ergebnis des Tupelgleichheitsoperators true.

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 und yi in der lexikalischen Reihenfolge:
    • Der Operator xi != yi wird ausgewertet, und ein Ergebnis vom Typ bool 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 Operator true dynamisch darauf angewendet, und der resultierende Wert bool 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 resultierende bool-Wert ist das Ergebnis.
    • Wenn das resultierende booltrue ist, findet keine weitere Auswertung statt, und das Ergebnis des Tupelgleichheitsoperators ist true.
  • Wenn alle Elementvergleiche false ergeben haben, ist das Ergebnis des Tupelgleichheitsoperators false.

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:

  1. Wenn E eine anonyme Funktion oder Methodengruppe ist, tritt ein Kompilierungszeitfehler auf.
  2. Wenn E das Literal null ist, oder der Wert von Enull ist, wird das Ergebnis false sein.
  3. Andernfalls:
  4. Lassen Sie R der Laufzeittyp von E sein.
  5. Lassen Sie D wie folgt von R ableiten:
  6. Wenn R ein Nullwerttyp ist, ist D der zugrunde liegende Typ von R.
  7. Andernfalls lautet DR.
  8. Das Ergebnis hängt von D und T wie folgt ab:
  9. Wenn T ein Verweistyp ist, ist das Ergebnis true, wenn gilt:
    • es eine Identitätskonvertierung zwischen D und T gibt,
    • D ein Verweistyp uist und eine implizite Verweiskonvertierung von D in T existiert, oder
    • Entweder: D ist ein Werttyp und eine Boxing-Konvertierung von D in T ist vorhanden.
      Oder: D ist ein Wertetyp und T ist ein Schnittstellentyp, der durch D implementiert wird.
  10. Wenn T ein nicht löschbarer Wertetyp ist, ist das Ergebnis true, wenn D der zugrunde liegende Typ von T ist.
  11. Wenn T ein nicht-nullbarer Wertetyp ist, ist das Ergebnis true, wenn D und T der gleiche Typ sind.
  12. 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, wobei C der Typ bei der Kompilierung von E ist:

  • Wenn der Kompilierungszeittyp von e mit Tidentisch ist oder eine implizite Verweiskonvertierung (§10.2.8), Boxing-Konvertierung (§10.2.9), Umbruchkonvertierung (§10.6) oder eine explizite Entpackungskonvertierung (§10.6) vom Kompilierungszeittyp E in T existiert:
    • Wenn C von einem nicht-nullbaren Werttyp ist, ist das Ergebnis des Vorgangs true.
    • 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 in T vorhanden ist oder C oder T 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 Typ T möglich, und das Ergebnis des Vorgangs ist false. 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 Typ T.

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 in T ist vorhanden.
  • Der Typ von E oder T ist ein offener Typ.
  • E ist das null-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 Edynamic 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 von G ist bekanntlich ein Referenztyp, da er die Klassenbeschränkung hat. Der Typparameter U von H ist es jedoch nicht; daher ist die Verwendung des as-Operators in H 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 Usind 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, falseund nulldarstellen.

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 Operation x & y, mit der Ausnahme, dass y nur ausgewertet wird, wenn x nicht false ist.
  • Die Operation x || y entspricht der Operation x | y, mit der Ausnahme, dass y nur ausgewertet wird, wenn x nicht true 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 und operator 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 als x ? y : false ausgewertet. Mit anderen Worten, x wird zuerst ausgewertet und in Typ bool konvertiert. Wenn xtrue ist, wird y ausgewertet und in Typ bool konvertiert, und dies wird zum Ergebnis des Vorgangs. Anderenfalls ist das Ergebnis des Vorgangs false.
  • Der Vorgang x || y wird als x ? true : y ausgewertet. Mit anderen Worten, x wird zuerst ausgewertet und in Typ bool konvertiert. Wenn xtrue ist, wird das Ergebnis des Vorgangs true. Andernfals wirdy ausgewertet und in Typ bool 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 Typ T und gibt ein Ergebnis vom Typ T zurück.
  • T soll Deklarationen von operator true und operator 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 als T.false(x) ? x : T.&(x, y) ausgewertet, wobei T.false(x) ein Aufruf des in T deklarierten operator false ist, und T.&(x, y) ein Aufruf des ausgewählten operator & ist. Mit anderen Worten, x wird zuerst ausgewertet, und operator false wird auf das Ergebnis angewendet, um zu prüfen, ob x definitiv falsch ist. Wenn x dann definitiv falsch ist, ist das Ergebnis der Operation der zuvor für x berechnete Wert. Andernfalls wird y ausgewertet, und der ausgewählte operator & wird auf den berechneten Wert für x und für y angewendet, um das Ergebnis des Vorgangs zu erzielen.
  • Die Operation x || y wird als T.true(x) ? x : T.|(x, y) ausgewertet, wobei T.true(x) ein Aufruf des in T deklarierten operator true ist, und T.|(x, y) ein Aufruf des ausgewählten operator | ist. Mit anderen Worten, x wird zuerst ausgewertet und operator true wird auf das Ergebnis angewendet, um zu prüfen, ob x definitiv wahr ist. Wenn dann x definitiv wahr ist, ist das Ergebnis der Operation der zuvor für x berechnete Wert. Andernfalls wird y ausgewertet, und der ausgewählte operator | wird auf den berechneten Wert für x und für y 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 anull 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 als a ?? (b ?? c) ausgewertet. Im Allgemeinen gibt ein Ausdruck der Form E1 ?? E2 ?? ... ?? EN den ersten Operanden zurück, der nichtnullist, oder null, wenn alle Operanden nullsind. 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 ?? bA₀, 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 und b ein dynamischer Ausdruck ist, ist der Ergebnistyp dynamic. Während der Laufzeit wird zunächst a ausgewertet. Wenn a nicht null ist, wird a in dynamic umgewandelt und dies wird das Ergebnis. Andernfalls wird b ausgewertet, und dies ergibt das Ergebnis.
  • Andernfalls, wenn A vorhanden ist und ein Nullwerttyp ist und eine implizite Konvertierung von b in A₀ vorhanden ist, wird der Ergebnistyp A₀. Während der Laufzeit wird zunächst a ausgewertet. Wenn a nicht null ist, wird a in den Typ A₀ entpackt, und dies ergibt das Ergebnis. Andernfalls wird b ausgewertet und in den Typ A₀ konvertiert, und dies ergibt das Ergebnis.
  • Andernfalls, wenn A existiert und eine implizite Umwandlung von b zu Aexistiert, ist der Ergebnistyp A. Während der Laufzeit wird zunächst a ausgewertet. Wenn a nicht nullist, wird a zum Ergebnis. Andernfalls wird b ausgewertet und in den Typ A konvertiert, und dies ergibt das Ergebnis.
  • Andernfalls, wenn A existiert und ein Nullwerttyp ist, b den Typ B hat und eine implizite Konvertierung von A₀ zu Bexistiert, dann ist der Ergebnistyp B. Während der Laufzeit wird zunächst a ausgewertet. Wenn a nicht null ist, wird a zu Typ A₀ entpackt und in Typ B konvertiert, und dies ergibt das Ergebnis. Andernfalls wird b ausgewertet und dies ergibt das Ergebnis.
  • Andernfalls, wenn b einen Typ B hat und eine implizite Konvertierung von a in B aufweist, ist das Ergebnistyp B. Während der Laufzeit wird zunächst a ausgewertet. Wenn a nicht null ist, wird a in den Typ B umgewandelt und dies wird das Ergebnis. Andernfalls wird b 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 outArgumentwert 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 von b1 ist bool, denn das ist der Typ des entsprechenden Ausgabeparameters in M1. Die nachfolgende WriteLine kann auf i1 und b1 zugreifen, die in den eingeschlossenen Bereich eingeführt wurden.

Die Deklaration von s2 zeigt einen Versuch, i2 im geschachtelten Aufruf von M zu verwenden, was unzulässig ist, da der Verweis in der Argumentliste auftritt, in der i2 deklariert wurde. Andererseits ist der Verweis auf b2 im letzten Argument zulässig, weil er nach dem Ende der eingebetteten Argumentliste erfolgt, in der b2 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_ entspricht var _, 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 btrue 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 als a ? 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 Typableitung dynamic (§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 Typ X und y den Typ Y hat, dann,
    • Wenn eine Identitätskonvertierung zwischen X und Y vorhanden ist, dann ist das Ergebnis der beste gemeinsame Typ einer Gruppe von Ausdrücken (§12.6.3.15). Wenn einer der Typen dynamic ist, bevorzugt die Typableitung dynamic (§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 nach Y, aber nicht von Y nach X existiert, dann ist Y der Typ des bedingten Ausdrucks.
    • Andernfalls, wenn eine implizite Enumerationskonvertierung (§10.2.4) von X nach Y vorhanden ist, ist Y der Typ des bedingten Ausdrucks.
    • Andernfalls, wenn eine implizite Enumerationskonvertierung (§10.2.4) von Y nach X vorhanden ist, ist X der Typ des bedingten Ausdrucks.
    • Andernfalls, wenn eine implizite Konversion (§10.2) von Y nach X, aber nicht von X nach Y existiert, dann ist X der Typ des bedingten Ausdrucks.
    • Andernfalls kann kein Typ des Ausdrucks bestimmt werden und es tritt ein Kompilierfehler auf.
  • Wenn nur eines von x und y einen Typ hat und sowohl x als auch y 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 der bool Wert von b wird bestimmt:
    • Wenn eine implizite Konversion vom Typ b nach bool existiert, dann wird diese implizite Konversion durchgeführt, um einen bool Wert zu erzeugen.
    • Andernfalls wird operator true, der vom Typ b definiert ist, aufgerufen, um einen bool-Wert zu erzeugen.
  • Wenn der Wert bool, der durch den obigen Schritt erzeugt wurde, true ist, wird x 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 der bool Wert von b wird bestimmt:
    • Wenn eine implizite Konversion vom Typ b nach bool existiert, dann wird diese implizite Konversion durchgeführt, um einen bool Wert zu erzeugen.
    • Andernfalls wird operator true, der vom Typ b definiert ist, aufgerufen, um einen bool-Wert zu erzeugen.
  • Wenn der im obigen Schritt erzeugte bool-Wert true ist, wird x 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 von Mvoid ist (§12.8.13). Bei der Behandlung als null_conditional_invocation_expression darf der Ergebnistyp jedoch void sein. Hinweisende

Beispiel: Der Ergebnistyp von List<T>.Reverse ist void. 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 auf this. Dies gilt unabhängig davon, ob der Zugriff explizit (wie in this.x) oder implizit (wie in x, wobei x 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 oder continue 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 zwei Sum Methoden. Jede akzeptiert ein selector-Argument, das den Wert extrahiert, der aus einem Listenelement addiert werden soll. Der extrahierte Wert kann entweder ein int oder ein double sein und die resultierende Summe ist ebenfalls entweder ein int oder ein double.

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 beide Sum-Methoden anwendbar, da die anonyme Funktion d => d.UnitCount sowohl mit Func<Detail,int> als auch Func<Detail,double> kompatibel ist. Die Überladungsauflösung wählt jedoch die erste Sum-Methode aus, da die Konvertierung in Func<Detail,int> besser ist als die Konvertierung in Func<Detail,double>.

Beim zweiten Aufruf von orderDetails.Sum ist nur die zweite Methode Sum anwendbar, weil die anonyme Funktion d => d.UnitPrice * d.UnitCount einen Wert vom Typ double erzeugt. Daher wählt die Überladungsauflösung die zweite Methode Sum 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 von x wird mindestens verlängert, bis der von F zurückgegebene Delegat für die Garbage Collection bereit ist. Da jeder Aufruf der anonymen Funktion auf derselben Instanz von x 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 von x:

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 wird

static 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 von y, 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 __Locals1verweist. 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 „fromidentifer“ 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 generische IEnumerable<T>-Schnittstelle. Im obigen Beispiel wäre dies der Fall, wenn Kunden vom Typ ArrayList 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 von Select 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 und y 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 und y 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> und O<T>, die sicherstellt, dass die Methoden ThenBy und ThenByDescending nur auf dem Ergebnis eines OrderBy oder OrderByDescending verfügbar sind. Hinweisende

Hinweis: Die empfohlene Form des Ergebnisses von GroupBy– eine Sequenz von Sequenzen, wobei jede innere Sequenz eine zusätzliche Key-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 die System.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 als a = (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 dynamichat, 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 und y in einen Tupelausdruck (y1, ..., yn) mit n-Elementen (§12.7) zerlegt werden kann, und wenn jede Zuweisung an xi von yi den Typ Ti hat, dann hat die Zuweisung den Typ (T1, ..., Tn).
  • Andernfalls, wenn x als Variable klassifiziert wird, die Variable nicht readonly ist, x den Typ T hat, und y eine implizite Konvertierung in T hat, dann hat die Zuweisung den Typ T.
  • Andererseits, wenn x als implizit typierte Variable klassifiziert wird (d. h. ein implizit typierter Deklarationsausdruck) und y den Typ That, ist der abgeleitete Typ der Variablen T, und die Zuweisung hat den Typ T.
  • Andernfalls, wenn x als Eigenschafts- oder Indexerzugriff klassifiziert ist, die Eigenschaft oder der Indexer einen zugänglichen set-Accessor hat, x über den Typ T verfügt und y eine implizite Konvertierung in T aufweist, hat die Zuweisung den Typ T.
  • 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, wird y ausgewertet und, falls erforderlich, durch eine implizite Konversion (§10.2) in T 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ür y berechnete Wert mit der Arrayinstanz kompatibel ist, von der x ein Element ist. Die Überprüfung ist erfolgreich, wenn ynullist oder wenn eine implizite Verweiskonvertierung (§10.2.8) vom Typ der Instanz, auf die y verweist, auf den tatsächlichen Elementtyp der Arrayinstanz existiert, die xenthält. Andernfalls wird eine System.ArrayTypeMismatchException ausgelöst.
    • Der Wert, der sich aus der Auswertung und Umwandlung von y ergibt, wird an der durch die Auswertung von x bestimmten Position gespeichert und als Resultat der Zuweisung zurückgegeben.
  • Wenn x als Zugriff auf eine Eigenschaft oder einen Indexer klassifiziert wird:
    • y wird ausgewertet und, falls erforderlich, implizit in T umgewandelt (§10.2).
    • Der set-Accessor von x wird mit dem Wert aufgerufen, der aus der Bewertung und Umwandlung von y 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ät n hat:
    • y wird mit n Elementen zu einem Tupelausdruck e dekonstruiert.
    • ein Resultat-Tupel t wird durch die implizite Konvertierung von e in T erstellt.
    • für jeden xi in der Reihenfolge von links nach rechts wird eine Zuweisung zu xi von t.Itemi ausgeführt, mit der Ausnahme, dass die xi 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 von y nach dynamic 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 Arraytyps B[] ist, vorausgesetzt, eine implizite Verweiskonvertierung ist von B in A 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 Beispiel

string[] 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 ein ArrayList nicht in einem Element eines string[] 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 und r.B zulässig, da p und r Variablen sind. Jedoch im Beispiel

Rectangle 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 und r.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, den ref-Teil als Teil des Operanden zu betrachten. Dies ist besonders verwirrend, wenn der Operand ein bedingter ?:-Ausdruck ist. Wenn Sie beispielsweise ref int a = ref b ? ref x : ref y; lesen, sollte = ref als Operator und b ? ref x : ref y als der rechte Operand betrachtet werden: ref int a = ref (b ? ref x : ref y);. Wichtig ist, dass der Ausdruck ref bkein 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 xkonvertierbar ist, wird der Vorgang als x = x «op» yausgewertet, außer dass x 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 und y implizit in den Typ x konvertierbar ist oder der Operator ein Shift-Operator ist, wird der Vorgang als x = (T)(x «op» y)ausgewertet, wobei T der Typ von xist, jedoch wird x 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» ydie Ergebnisse aller Teilausdrücke von x vorübergehend gespeichert und dann bei der Ausführung der Zuordnung zu xwiederverwendet werden.

Beispiel: In der Zuweisung A()[B()] += C(), wobei A eine Methode ist, die int[] zurückgibt, und B und C Methoden sind, die int zurückgeben, werden die Methoden nur einmal in der Reihenfolge A, 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 als x = x «op» y oder x = (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- und unchecked-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 die sizeof 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 von str 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 einer switch-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 in E zu finden, die dann zur Laufzeit angewendet wird.
  • Wenn kein solcher Operator gefunden wird, tritt ein Bindungszeitfehler auf.