Freigeben über


13 Aussagen

13.1 Allgemein

C# stellt eine Vielzahl von Anweisungen bereit.

Hinweis: Die meisten dieser Anweisungen sind Entwicklern vertraut, die in C und C++ programmiert haben. Hinweisende

statement
    : labeled_statement
    | declaration_statement
    | embedded_statement
    ;

embedded_statement
    : block
    | empty_statement
    | expression_statement
    | selection_statement
    | iteration_statement
    | jump_statement
    | try_statement
    | checked_statement
    | unchecked_statement
    | lock_statement
    | using_statement
    | yield_statement
    | unsafe_statement   // unsafe code support
    | fixed_statement    // unsafe code support
    ;

unsafe_statement (§23.2) und fixed_statement (§23.7) sind nur im unsicheren Code (§23) verfügbar.

Das Nichtterminal embedded_statement wird für Anweisungen verwendet, die innerhalb anderer Anweisungen erscheinen. Die Anwendung von embedded_statement anstelle von statement schließt die Anwendung von Deklarationsanweisungen und bezeichneten Anweisungen in diesen Kontexten aus.

Beispiel: Der Code

void F(bool b)
{
   if (b)
      int i = 44;
}

führt zu einem Kompilierfehler, weil eine ifAnweisung für ihren Branch ein embedded_statement und nicht ein if benötigt. Wenn dieser Code zulässig wäre, würde die Variable i deklariert, aber sie konnte nie verwendet werden. Beachten Sie jedoch, dass das Beispiel gültig ist, indem die Deklaration in einem Block platziert iwird.

Endbeispiel

13.2 Endpunkte und Reichweite

Jede Anweisung hat einen Endpunkt. Intuitiv gesehen ist der Endpunkt einer Anweisung die Position, die unmittelbar auf die Anweisung folgt. Die Ausführungsregeln für zusammengesetzte Anweisungen (Anweisungen, die eingebettete Anweisungen enthalten) geben die Aktion an, die ausgeführt wird, wenn das Steuerelement den Endpunkt einer eingebetteten Anweisung erreicht.

Beispiel: Wenn das Steuerelement den Endpunkt einer Anweisung in einem Block erreicht, wird das Steuerelement in die nächste Anweisung im Block übertragen. Endbeispiel

Wenn eine Anweisung möglicherweise durch Ausführung erreicht werden kann, wird die Anweisung als erreichbar bezeichnet. Wenn dagegen keine Möglichkeit besteht, dass eine Anweisung ausgeführt wird, wird die Anweisung als nicht erreichbar bezeichnet.

Beispiel: Im folgenden Code

void F()
{
    Console.WriteLine("reachable");
    goto Label;
    Console.WriteLine("unreachable");
  Label:
    Console.WriteLine("reachable");
}

der zweite Aufruf von Console.WriteLine ist nicht erreichbar, da es keine Möglichkeit gibt, dass die Anweisung ausgeführt wird.

Endbeispiel

Eine Warnung wird gemeldet, wenn eine andere Anweisung als throw_statement, Block oder empty_statement nicht erreichbar ist. Es handelt sich insbesondere nicht um einen Fehler, wenn eine Aussage nicht erreichbar ist.

Hinweis: Um zu bestimmen, ob eine bestimmte Anweisung oder ein Endpunkt erreichbar ist, führt ein Compiler Eine Flussanalyse gemäß den für jede Anweisung definierten Reichweitenregeln durch. Die Flussanalyse berücksichtigt die Werte konstanter Ausdrücke (§12.23), die das Verhalten von Anweisungen steuern, aber die möglichen Werte von nicht konstanten Ausdrücken werden nicht berücksichtigt. Anders ausgedrückt wird bei der Steuerungsflussanalyse ein nicht konstanter Ausdruck eines bestimmten Typs als ein Ausdruck betrachtet, der jeden möglichen Wert dieses Typs annehmen kann.

Im Beispiel

void F()
{
    const int i = 1;
    if (i == 2)
        Console.WriteLine("unreachable");
}

Der boolesche Ausdruck der if Anweisung ist ein konstanter Ausdruck, da beide Operanden des == Operators Konstanten sind. Wenn der konstante Ausdruck während der Kompilierung ausgewertet wird, wird der Wert false erzeugt, und der Console.WriteLine-Aufruf wird als nicht erreichbar betrachtet. Allerdings, wenn i in eine lokale Variable geändert wird

void F()
{
    int i = 1;
    if (i == 2)
        Console.WriteLine("reachable");
}

der Console.WriteLine Aufruf gilt als erreichbar, obwohl es in Wirklichkeit niemals ausgeführt wird.

Hinweisende

Der Block eines Funktionselements oder einer anonymen Funktion gilt immer als erreichbar. Durch die aufeinanderfolgende Auswertung der Reichweitenregeln jeder Anweisung in einem Block kann die Erreichbarkeit einer gegebenen Anweisung ermittelt werden.

Beispiel: Im folgenden Code

void F(int x)
{
    Console.WriteLine("start");
    if (x < 0)
        Console.WriteLine("negative");
}

die Reichweite des zweiten Console.WriteLine wird wie folgt bestimmt:

  • Die erste Console.WriteLine Ausdrucksanweisung ist erreichbar, da der Block der F Methode erreichbar ist (§13.3).
  • Der Endpunkt der ersten Console.WriteLine Ausdrucksanweisung ist erreichbar, da diese Anweisung erreichbar ist (§13.7 und §13.3).
  • Die if Anweisung ist erreichbar, da der Endpunkt der ersten Console.WriteLine Ausdrucksanweisung erreichbar ist (§13.7 und §13.3).
  • Die zweite Console.WriteLine Ausdrucksanweisung ist erreichbar, da der boolesche Ausdruck der if Anweisung nicht den Konstantenwert falseaufweist.

Endbeispiel

Es gibt zwei Situationen, in denen es ein Kompilierfehler ist, wenn der Endpunkt einer Anweisung nicht erreicht werden kann:

  • Da die switch-Anweisung es nicht zulässt, dass ein switch-Abschnitt zum nächsten switch-Abschnitt „durchfällt“, ist es ein Kompilierfehler, wenn der Endpunkt der Anweisungsliste eines switch-Abschnitts erreichbar ist. Wenn dieser Fehler auftritt, ist es in der Regel ein Hinweis darauf, dass eine break Anweisung fehlt.

  • Es handelt sich um einen Kompilierungszeitfehler für den Endpunkt des Blocks eines Funktionselements oder einer anonymen Funktion, die einen zu erreichenden Wert berechnet. Wenn dieser Fehler auftritt, ist es in der Regel ein Hinweis darauf, dass eine return Anweisung fehlt (§13.10.5).

13.3 Blöcke

13.3.1 Allgemein

Ein Block ermöglicht, mehrere Anweisungen in Kontexten zu schreiben, in denen eine einzelne Anweisung zulässig ist.

block
    : '{' statement_list? '}'
    ;

Ein Block besteht aus einer optionalen Anweisungsliste (§13.3.2), die in geschweiften Klammern eingeschlossen ist. Wenn die Anweisungsliste weggelassen wird, gilt der Block als leer.

Ein Block kann Deklarationsanweisungen enthalten (§13.6). Der Bereich einer lokalen Variablen oder Konstante, die in einem Block deklariert ist, ist der Block.

Ein Block wird wie folgt ausgeführt:

  • Wenn der Block leer ist, wird das Steuerelement an den Endpunkt des Blocks übertragen.
  • Wenn der Block nicht leer ist, wird die Kontrolle auf die Anweisungsliste übertragen. Wenn und falls die Kontrolle den Endpunkt der Anweisungsliste erreicht, wird die Kontrolle an den Endpunkt des Blocks übertragen.

Die Anweisungsliste eines Blocks ist erreichbar, wenn der Block selbst erreichbar ist.

Der Endpunkt eines Blocks ist erreichbar, wenn der Block leer ist oder der Endpunkt der Anweisungsliste erreichbar ist.

Ein Block , der eine oder yield mehrere Anweisungen (§13.15) enthält, wird als Iteratorblock bezeichnet. Iteratorblöcke werden verwendet, um Funktionsmber als Iteratoren (§15.15) zu implementieren. Einige zusätzliche Einschränkungen gelten für Iteratorblöcke:

  • Es handelt sich um einen Kompilierungszeitfehler für eine return Anweisung, die in einem Iteratorblock angezeigt wird (aber yield return Anweisungen sind zulässig).
  • Es handelt sich um einen Kompilierungszeitfehler für einen Iteratorblock, der einen unsicheren Kontext (§23.2) enthält. Ein Iteratorblock definiert immer einen sicheren Kontext, auch wenn seine Deklaration in einem unsicheren Kontext geschachtelt ist.

13.3.2 Erklärungslisten

Eine Anweisungsliste besteht aus einer oder mehreren Anweisungen, die in Sequenz geschrieben wurden. Anweisungslisten kommen in Blocks (§13.3) und in Switch_blocks (§13.8.3) vor.

statement_list
    : statement+
    ;

Eine Anweisungsliste wird ausgeführt, indem die Kontrolle auf die erste Anweisung übertragen wird. Wenn und wenn das Steuerelement den Endpunkt einer Anweisung erreicht, wird das Steuerelement in die nächste Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt der letzten Anweisung erreicht, wird das Steuerelement an den Endpunkt der Anweisungsliste übertragen.

Eine Anweisung in einer Anweisungsliste ist erreichbar, wenn mindestens eine der folgenden Bedingungen erfüllt ist:

  • Die Anweisung ist die erste Anweisung und die Anweisungsliste selbst ist erreichbar.
  • Der Endpunkt der vorangehenden Anweisung ist erreichbar.
  • Die Anweisung ist eine gekennzeichnete Anweisung und die Kennzeichnung wird durch eine erreichbare goto-Anweisung referenziert.

Der Endpunkt einer Anweisungsliste ist erreichbar, wenn der Endpunkt der letzten Anweisung in der Liste erreichbar ist.

13.4 Die leere Anweisung

Ein empty_statement bewirkt nichts.

empty_statement
    : ';'
    ;

Eine leere Anweisung wird verwendet, wenn es keine Vorgänge gibt, die in einem Kontext ausgeführt werden müssen, in dem eine Anweisung erforderlich ist.

Die Ausführung einer leeren Anweisung überträgt einfach die Steuerung an den Endpunkt der Anweisung. Daher ist der Endpunkt einer leeren Anweisung erreichbar, wenn die leere Anweisung erreichbar ist.

Beispiel: Eine leere Anweisung kann beim Schreiben einer while Anweisung mit einem NULL-Text verwendet werden:

bool ProcessMessage() {...}
void ProcessMessages()
{
    while (ProcessMessage())
        ;
}

Eine leere Anweisung kann auch verwendet werden, um eine Kennzeichnung kurz vor dem abschließenden „}“ eines Blocks zu deklarieren:

void F(bool done)
{
    ...
    if (done)
    {
        goto exit;
    }
    ...
  exit:
    ;
}

Endbeispiel

13.5 Anweisungen mit Bezeichnung

Ein labeled_statement erlaubt es, einer Anweisung eine Kennzeichnung voranzustellen. Beschriftete Anweisungen sind in Blöcken zulässig, sind jedoch nicht als eingebettete Anweisungen zulässig.

labeled_statement
    : identifier ':' statement
    ;

Eine gekennzeichnete Anweisung deklariert eine Kennzeichnung mit dem durch den Identifikator angegebenen Namen. Der Bereich einer Bezeichnung ist der gesamte Block, in dem die Bezeichnung deklariert wird, einschließlich aller geschachtelten Blöcke. Es ist ein Kompilierungsfehler, wenn zwei Labels mit demselben Namen überlappende Gültigkeitsbereiche haben.

Eine Kennzeichnung kann von goto Anweisungen (§13.10.4) innerhalb des Bereichs der Kennzeichnung referenziert werden.

Hinweis: Dies bedeutet, dass goto Anweisungen die Steuerung innerhalb von Blöcken und aus Blöcken übertragen können, aber nie in Blöcke. Hinweisende

Bezeichnungen verfügen über einen eigenen Deklarationsbereich und beeinträchtigen keine anderen Bezeichner.

Beispiel: Das Beispiel

int F(int x)
{
    if (x >= 0)
    {
        goto x;
    }
    x = -x;
  x:
    return x;
}

ist gültig und verwendet den Namen "x" sowohl als Parameter als auch als Bezeichnung.

Endbeispiel

Die Ausführung einer beschrifteten Anweisung entspricht exakt der Ausführung der Anweisung nach der Bezeichnung.

Neben der Reichweite, die durch einen normalen Kontrollfluss bereitgestellt wird, ist eine beschriftete Anweisung erreichbar, wenn auf die Bezeichnung durch eine erreichbare goto Anweisung verwiesen wird, es sei denn, die goto Anweisung befindet sich innerhalb des try Blocks oder eines catch Blocks eines try_statement , der einen finally Block enthält, dessen Endpunkt nicht erreichbar ist, und die beschriftete Anweisung liegt außerhalb des try_statement.

13.6 Deklarationsanweisungen

13.6.1 Allgemein

Ein declaration_statement deklariert eine oder mehrere lokale Variablen, eine oder mehrere lokale Konstanten oder eine lokale Funktion. Deklarationsanweisungen sind in Blockierungen und Switch-Blöcken erlaubt, aber nicht als eingebettete Anweisungen.

declaration_statement
    : local_variable_declaration ';'
    | local_constant_declaration ';'
    | local_function_declaration
    ;

Eine lokale Variable wird mithilfe einer local_variable_declaration (§13.6.2) deklariert. Eine lokale Konstante wird mithilfe einer local_constant_declaration (§13.6.3) deklariert. Eine lokale Funktion wird mithilfe einer local_function_declaration (§13.6.4) deklariert.

Die deklarierten Namen werden in den nächstgelegenen einschließenden Deklarationsbereich eingefügt (§7.3).

13.6.2 Lokale Variablendeklarationen

13.6.2.1 Allgemein

Ein local_variable_declaration deklariert eine oder mehrere lokale Variablen.

local_variable_declaration
    : implicitly_typed_local_variable_declaration
    | explicitly_typed_local_variable_declaration
    | explicitly_typed_ref_local_variable_declaration
    ;

Implizit eingegebene Deklarationen enthalten das kontextbezogene Schlüsselwort (§6.4.4), var was zu einer syntaktischen Mehrdeutigkeit zwischen den drei Kategorien führt, die wie folgt aufgelöst werden:

  • Wenn es keinen Typ namens var im Bereich gibt und die Eingabe mit implicitly_typed_local_variable_declaration übereinstimmt, dann wird dieser gewählt;
  • Andernfalls, wenn ein Typ mit dem Namen var im Bereich liegt, wird implicitly_typed_local_variable_declaration nicht als mögliche Übereinstimmung betrachtet.

Innerhalb eines local_variable_declaration wird jede Variable von einem Deklarator eingeführt, der einer von implicitly_typed_local_variable_declarator ist, explicitly_typed_local_variable_declarator oder ref_local_variable_declarator für implizit typierte, explizit eingegebene und referenzierte lokale Variablen. Der Deklarator definiert den Namen (Bezeichner) und den Anfangswert (sofern vorhanden) der eingeführten Variablen.

Wenn es mehrere Deklaratoren in einer Deklaration gibt, werden diese von links nach rechts verarbeitet, einschließlich der Initialisierungsausdrücke (§9.4.4.5).

Anmerkung: Für eine local_variable_declaration, die nicht als for_initializer (§13.9.4) oder resource_acquisition (§13.14) auftritt, ist diese Reihenfolge von links nach rechts äquivalent zu jedem Deklarator innerhalb einer separaten local_variable_declaration. Zum Beispiel:

void F()
{
    int x = 1, y, z = x * 2;
}

entspricht:

void F()
{
    int x = 1;
    int y;
    int z = x * 2;
}

Hinweisende

Der Wert einer lokalen Variablen wird in einem Ausdruck mithilfe eines simple_name (§12.8.4) abgerufen. An jedem Ort, an dem der Wert abgerufen wird, muss definitiv eine lokale Variable (§9.4) zugewiesen werden. Jede lokale Variable, die von einem local_variable_declaration eingeführt wird, ist zunächst nicht zugewiesen (§9.4.3). Wenn ein Deklarator über einen Initialisierungsausdruck verfügt, wird die eingeführte lokale Variable am Ende des Deklarators als zugewiesen klassifiziert (§9.4.4.5).

Der Bereich einer lokalen Variablen, die von einem local_variable_declaration eingeführt wird, ist wie folgt definiert (§7.7):

  • Wenn die Deklaration als for_initializer erfolgt, umfasst der Bereich die for_initializer, for_condition, for_iterator und embedded_statement (§13.9.4);
  • Wenn die Deklaration als Resource_acquisition auftritt, dann ist der Bereich der äußerste Block der semantisch äquivalenten Erweiterung der using_statement (§13.14);
  • Andernfalls ist der Gültigkeitsbereich der Block, in dem die Deklaration erfolgt.

Es ist ein Fehler, auf eine lokale Variable anhand des Namens in einer Textposition zu verweisen, die dem Deklarator vorausgeht, oder innerhalb eines initialisierenden Ausdrucks innerhalb des Deklarators. Innerhalb des Bereichs einer lokalen Variablen ist es ein Kompilierungszeitfehler, um eine andere lokale Variable, lokale Funktion oder Konstante mit demselben Namen zu deklarieren.

Der ref-safe-Kontext (§9.7.2) einer lokalen ref-Variable ist der ref-safe-Kontext ihrer initialisierenden variable_referenz. Der ref-safe-Kontext von nicht-ref lokalen Variablen ist declaration-block.

13.6.2.2 Implizit typierte lokale Variablendeklarationen

implicitly_typed_local_variable_declaration
    : 'var' implicitly_typed_local_variable_declarator
    | ref_kind 'var' ref_local_variable_declarator
    ;

implicitly_typed_local_variable_declarator
    : identifier '=' expression
    ;

Ein implicitly_typed_local_variable_declaration führt eine einzelne lokale Variable, einen Bezeichner ein. Der Ausdruck oder variable_reference muss einen Typ bei der Kompilierung haben, T. Die erste Alternative deklariert eine Variable mit einem Anfangswert von Ausdruck; ihr Typ ist T?, wenn T ein nicht-nullbarer Bezugstyp ist, andernfalls ist ihr Typ T. Die zweite Alternative deklariert eine Ref-Variable mit einem Anfangswert von refvariable_reference. Ihr Typ ist ref T?, wenn T ein Nicht-Null-Bezugstyp ist, andernfalls ist ihr Typ ref T. (ref_kind wird in §15.6.1 beschrieben.)

Beispiel:

var i = 5;
var s = "Hello";
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary<int,Order>();
ref var j = ref i;
ref readonly var k = ref i;

Die oben aufgeführten implizit typierten lokalen Variablendeklarationen entsprechen genau den folgenden explizit typierten Deklarationen:

int i = 5;
string s = "Hello";
double d = 1.0;
int[] numbers = new int[] {1, 2, 3};
Dictionary<int,Order> orders = new Dictionary<int,Order>();
ref int j = ref i;
ref readonly int k = ref i;

Es folgen falsch implizit typierte lokale Variablendeklarationen:

var x;                  // Error, no initializer to infer type from
var y = {1, 2, 3};      // Error, array initializer not permitted
var z = null;           // Error, null does not have a type
var u = x => x + 1;     // Error, anonymous functions do not have a type
var v = v++;            // Error, initializer cannot refer to v itself

Endbeispiel

13.6.2.3 Explizit typierte lokale Variablendeklarationen

explicitly_typed_local_variable_declaration
    : type explicitly_typed_local_variable_declarators
    ;

explicitly_typed_local_variable_declarators
    : explicitly_typed_local_variable_declarator
      (',' explicitly_typed_local_variable_declarator)*
    ;

explicitly_typed_local_variable_declarator
    : identifier ('=' local_variable_initializer)?
    ;

local_variable_initializer
    : expression
    | array_initializer
    ;

Ein explicity_typed_local_variable_declaration führt eine oder mehrere lokale Variablen mit dem angegebenen Typ ein.

Wenn ein local_variable_initializer vorhanden ist, ist sein Typ gemäß den Regeln der einfachen Zuordnung (§12.21.2) oder arrayinitialisierung (§17.7) angemessen, und sein Wert wird als Anfangswert der Variablen zugewiesen.

13.6.2.4 Explizit typisierte ref lokale Variablendeklarationen

explicitly_typed_ref_local_variable_declaration
    : ref_kind type ref_local_variable_declarators
    ;

ref_local_variable_declarators
    : ref_local_variable_declarator (',' ref_local_variable_declarator)*
    ;

ref_local_variable_declarator
    : identifier '=' 'ref' variable_reference
    ;

Die initialisierende variable_reference muss vom Typ type sein und die gleichen Anforderungen erfüllen wie für eine ref Zuweisung (§12.21.3).

Wenn ref_kindref readonly ist, sind die deklarierten Identifier Referenzen auf Variablen, die als schreibgeschützt behandelt werden. Andernfalls, wenn ref_kind ist ref, sind die deklarierten BezeichnerVerweise auf Variablen, die schreibbar sein müssen.

Es ist ein Kompilierfehler, eine lokale ref-Variable oder eine Variable vom Typ ref struct innerhalb einer Methode, die mit dem method_modifierasync deklariert wurde, oder innerhalb eines Iterators (§15.15) zu deklarieren.

13.6.3 Lokale Konstantendeklarationen

Ein local_constant_declaration deklariert eine oder mehrere lokale Konstanten.

local_constant_declaration
    : 'const' type constant_declarators
    ;

constant_declarators
    : constant_declarator (',' constant_declarator)*
    ;

constant_declarator
    : identifier '=' constant_expression
    ;

Der Typ eines local_constant_declaration gibt den Typ der durch die Deklaration eingeführten Konstanten an. Auf den Typ folgt eine Liste von constant_declarators, von denen jede eine neue Konstante einführt. Ein constant_declarator besteht aus einem Bezeichner , der die Konstante benennt, gefolgt von einem "="-Token, gefolgt von einem constant_expression (§12.23), der den Wert der Konstante angibt.

Der Typ und constant_expression einer lokalen Konstantendeklaration entsprechen den gleichen Regeln wie die einer konstanten Memberdeklaration (§15.4).

Der Wert einer lokalen Konstante wird in einem Ausdruck mithilfe einer simple_name (§12.8.4) abgerufen.

Der Bereich einer lokalen Konstante ist der Block, in dem die Deklaration auftritt. Es ist ein Fehler, auf eine lokale Konstante in einer Textposition zu verweisen, die vor dem Ende der constant_declarator liegt.

Eine lokale Konstantendeklaration, die mehrere Konstanten deklariert, entspricht mehreren Deklarationen einzelner Konstanten mit demselben Typ.

13.6.4 Lokale Funktionsdeklarationen

Ein local_function_declaration deklariert eine lokale Funktion.

local_function_declaration
    : local_function_modifier* return_type local_function_header
      local_function_body
    | ref_local_function_modifier* ref_kind ref_return_type
      local_function_header ref_local_function_body
    ;

local_function_header
    : identifier '(' parameter_list? ')'
    | identifier type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

local_function_modifier
    : ref_local_function_modifier
    | 'async'
    ;

ref_local_function_modifier
    : 'static'
    | unsafe_modifier   // unsafe code support
    ;

local_function_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    ;

ref_local_function_body
    : block
    | '=>' 'ref' variable_reference ';'
    ;

Grammatikalische Anmerkung: Wenn bei der Erkennung eines local_function_body sowohl die null_conditional_invocation_expression als auch die expression Alternativen anwendbar sind, muss die erste gewählt werden. (§15.6.1)

Beispiel: Es gibt zwei häufige Anwendungsfälle für lokale Funktionen: Iteratormethoden und asynchrone Methoden. Bei Iteratormethoden werden Ausnahmen nur festgestellt, wenn Code aufgerufen wird, der die zurückgegebene Sequenz auflistet. In asynchronen Methoden werden Ausnahmen nur erkannt, wenn auf die zurückgegebene Task gewartet wird. Im folgenden Beispiel wird veranschaulicht, wie mithilfe einer lokalen Funktion die Überprüfung der Parameter von der Iteratorimplementierung getrennt wird:

public static IEnumerable<char> AlphabetSubset(char start, char end)
{
    if (start < 'a' || start > 'z')
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(start),
            message: "start must be a letter");
    }
    if (end < 'a' || end > 'z')
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(end),
            message: "end must be a letter");
    }
    if (end <= start)
    {
        throw new ArgumentException(
            $"{nameof(end)} must be greater than {nameof(start)}");
    }
    return AlphabetSubsetImplementation();

    IEnumerable<char> AlphabetSubsetImplementation()
    {
        for (var c = start; c < end; c++)
        {
            yield return c;
        }
    }
}

Endbeispiel

Sofern nicht anders unten angegeben, ist die Semantik aller Grammatikelemente identisch mit method_declaration (§15.6.1), die im Kontext einer lokalen Funktion anstelle einer Methode gelesen wird.

Der Bezeichner einer lokalen Funktionsdeklaration muss in seinem deklarierten Blockbereich eindeutig sein, einschließlich aller umschließenden lokalen Variablendeklarationsräume. Eine Folge davon ist, dass überladene local_function_declarations nicht zulässig sind.

Ein local_function_declaration kann einen async (§15.14)-Modifizierer und einen unsafe (§23.1)-Modifizierer enthalten. Wenn die Deklaration den async Modifizierer enthält, muss der Rückgabetyp void oder ein «TaskType» Typ sein (§15.14.1). Wenn die Deklaration den static Modifizierer enthält, handelt es sich bei der Funktion um eine statische lokale Funktion. Andernfalls handelt es sich um eine nicht statische lokale Funktion. Es ist ein Kompilierfehler, wenn type_parameter_list oder parameter_listattributes enthalten. Wenn die lokale Funktion in einem unsicheren Kontext (§23.2) deklariert wird, kann die lokale Funktion unsicheren Code enthalten, auch wenn die lokale Funktionsdeklaration den Modifizierer nicht enthält unsafe .

Eine lokale Funktion wird im Bereich der Blockierung deklariert. Eine nicht-statische lokale Funktion kann Variablen aus dem einschließenden Bereich erfassen, während eine statische lokale Funktion dies nicht darf (sie hat also keinen Zugriff auf einschließende lokale Funktionen, Parameter, nicht-statische lokale Funktionen oder this). Es handelt sich um einen Kompilierungsfehler, wenn eine erfasste Variable vom Textkörper einer nicht statischen lokalen Funktion gelesen wird, aber nicht unbedingt vor jedem Aufruf der Funktion zugewiesen wird. Ein Compiler muss bestimmen, welche Variablen definitiv bei der Rückgabe zugewiesen werden (§9.4.4.33).

Wenn der Typ eines this ein Strukturtyp ist, ist es ein Kompilierungszeitfehler, wenn der Rumpf einer lokalen Funktion auf this zugreift. 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 nur diesen Zugriff und wirkt sich nicht darauf aus, ob die Elementsuche zu einem Mitglied der Struktur führt.

Es ist ein Kompilierungszeitfehler, wenn der Textkörper der lokalen Funktion eine goto-Anweisung, eine break-Anweisung oder eine continue-Anweisung enthält, deren Ziel außerhalb des Textkörpers der lokalen Funktion liegt.

Hinweis: Die oben genannten Regeln für und this spiegeln die Regeln für anonyme Funktionen in goto wider. Hinweisende

Eine lokale Funktion kann vor der Deklaration von einem lexikalischen Punkt aufgerufen werden. Es ist jedoch ein Kompilierfehler, wenn die Funktion lexikalisch vor der Deklaration einer in der lokalen Funktion verwendeten Variable deklariert wird (§7.7).

Es ist ein Kompilierungsfehler, wenn eine lokale Funktion einen Parameter, Typparameter oder eine lokale Variable mit demselben Namen deklariert wie eine, die in einem umgebenden lokalen Variablenbereich deklariert wurde.

Lokale Funktionskörper sind immer erreichbar. Der Endpunkt einer lokalen Funktionsdeklaration ist erreichbar, wenn der Anfangspunkt der lokalen Funktionsdeklaration erreichbar ist.

Beispiel: Im folgenden Beispiel ist der Textkörper L erreichbar, obwohl der Anfangspunkt L nicht erreichbar ist. Da der Anfangspunkt L nicht erreichbar ist, ist die Anweisung nach dem Endpunkt L nicht erreichbar:

class C
{
    int M()
    {
        L();
        return 1;

        // Beginning of L is not reachable
        int L()
        {
            // The body of L is reachable
            return 2;
        }
        // Not reachable, because beginning point of L is not reachable
        return 3;
    }
}

Anders ausgedrückt: Die Position einer lokalen Funktionsdeklaration wirkt sich nicht auf die Reichweite von Anweisungen in der enthaltenden Funktion aus. Endbeispiel

Wenn der Typ des Arguments für eine lokale Funktion lautet dynamic, wird die aufgerufene Funktion zur Kompilierungszeit und nicht zur Laufzeit aufgelöst.

Eine lokale Funktion darf nicht in einem Ausdrucksbaum verwendet werden.

Eine statische lokale Funktion

  • Kann auf statische Elemente, Typparameter, Konstantendefinitionen und statische lokale Funktionen aus dem eingeschlossenen Bereich verweisen.
  • Sie darf weder auf this oder base noch auf Instanzmitglieder aus einer impliziten this-Referenz, noch auf lokale Variablen, Parameter oder nicht-statische lokale Funktionen aus dem einschließenden Bereich verweisen. Alle diese sind jedoch in einem nameof() Ausdruck zulässig.

13.7 Ausdrücke als Anweisungen

Ein expression_statement wertet einen bestimmten Ausdruck aus. Der vom Ausdruck berechnete Wert (falls vorhanden) wird verworfen.

expression_statement
    : statement_expression ';'
    ;

statement_expression
    : null_conditional_invocation_expression
    | invocation_expression
    | object_creation_expression
    | assignment
    | post_increment_expression
    | post_decrement_expression
    | pre_increment_expression
    | pre_decrement_expression
    | await_expression
    ;

Nicht alle Ausdrücke sind als Anweisungen zulässig.

Hinweis: Insbesondere Ausdrücke wie x + y und x == 1, die lediglich einen Wert berechnen (der verworfen wird), sind nicht als Anweisungen zulässig. Hinweisende

Die Ausführung eines expression_statement wertet den enthaltenen Ausdruck aus und überträgt dann die Steuerung an den Endpunkt der expression_statement. Der Endpunkt einer expression_statement ist erreichbar, wenn diese expression_statement erreichbar ist.

13.8 Auswahl-Anweisungen

13.8.1 Allgemein

Auswahlanweisungen wählen auf der Grundlage des Wertes eines Ausdrucks eine von mehreren möglichen Anweisungen zur Ausführung aus.

selection_statement
    : if_statement
    | switch_statement
    ;

13.8.2 Die If-Anweisung

Die if Anweisung wählt eine Anweisung für die Ausführung basierend auf dem Wert eines booleschen Ausdrucks aus.

if_statement
    : 'if' '(' boolean_expression ')' embedded_statement
    | 'if' '(' boolean_expression ')' embedded_statement
      'else' embedded_statement
    ;

Ein else-Element ist mit dem lexikalisch nächstgelegenen if verknüpft, das von der Syntax her möglich ist.

Beispiel: So kann eine if-Anweisung der Form

if (x) if (y) F(); else G();

entspricht

if (x)
{
    if (y)
    {
        F();
    }
    else
    {
        G();
    }
}

Endbeispiel

Eine if Anweisung wird wie folgt ausgeführt:

  • Die boolean_expression (§12.24) wird ausgewertet.
  • Wenn der boolesche Ausdruck ergibt true, wird das Steuerelement an die erste eingebettete Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt dieser Anweisung erreicht, wird die Steuerung an den Endpunkt der if Anweisung übertragen.
  • Wenn der boolesche Ausdruck den Wert false ergibt und ein else Element vorhanden ist, wird die Kontrolle auf die zweite eingebettete Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt dieser Anweisung erreicht, wird die Steuerung an den Endpunkt der if Anweisung übertragen.
  • Wenn der boolesche Ausdruck false ergibt und ein else-Teil nicht vorhanden ist, wird die Kontrolle an den Endpunkt der if-Anweisung übertragen.

Die erste eingebettete Anweisung einer if Anweisung ist erreichbar, wenn die if Anweisung erreichbar ist und der boolesche Ausdruck nicht den Konstantenwert falseaufweist.

Die zweite eingebettete Anweisung einer if Anweisung, falls vorhanden, ist erreichbar, wenn die if Anweisung erreichbar ist und der boolesche Ausdruck nicht den Konstantenwert trueaufweist.

Der Endpunkt einer if Anweisung ist erreichbar, wenn der Endpunkt mindestens einer der eingebetteten Anweisungen erreichbar ist. Darüber hinaus ist der Endpunkt einer if Anweisung ohne else Teil erreichbar, wenn die if Anweisung erreichbar ist und der boolesche Ausdruck nicht den Konstantenwert trueaufweist.

13.8.3 Die Switch-Anweisung

Die switch Anweisung wählt eine Anweisungsliste zur Ausführung aus, die mit einem Switch-Label verknüpft ist, das dem Wert des Switch-Ausdrucks entspricht.

switch_statement
    : 'switch' '(' expression ')' switch_block
    ;

switch_block
    : '{' switch_section* '}'
    ;

switch_section
    : switch_label+ statement_list
    ;

switch_label
    : 'case' pattern case_guard?  ':'
    | 'default' ':'
    ;

case_guard
    : 'when' expression
    ;

Ein switch_statement besteht aus dem Schlüsselwortswitch, gefolgt von einem Klammerausdruck (als Switchausdruck bezeichnet), gefolgt von einer switch_block. Der switch_block besteht aus null oder mehr switch_sections, eingeschlossen in geschweiften Klammern. Jede switch_section besteht aus einem oder mehreren switch_labelgefolgt von einem statement_list (§13.3.2). Jedes switch_label, das case enthält, hat ein zugehöriges Muster (§11), gegen das der Wert des switch-Ausdrucks getestet wird. Wenn case_guard vorhanden ist, muss ihr Ausdruck implizit in den Typ bool konvertierbar sein, und dieser Ausdruck wird als zusätzliche Bedingung ausgewertet, damit der Fall als erfüllt angesehen wird.

Der kontrollierende Typ einer switchAnweisung wird durch den switch-Ausdruck festgelegt.

  • Wenn der Typ des switch-Ausdrucks sbyte, byte, short, ushort, int, uint, long, ulong, char, bool, string oder ein enum_type ist, oder wenn es sich um den Typ eines nullbaren Wertes handelt, der einem dieser Typen entspricht, dann ist dies der bestimmende Typ der switch-Anweisung.
  • Andernfalls, wenn genau eine benutzerdefinierte implizite Konvertierung vom Typ des Switch-Ausdrucks in einen der folgenden möglichen Regeltypen vorhanden ist: sbyte, byte, short, ushort, int, uint, long, ulong, char, string oder einem Nullwerttyp, der einem dieser Typen entspricht, dann ist der konvertierte Typ der Regeltyp der switch-Anweisung.
  • Andernfalls ist der bestimmende Typ der switch-Anweisung der Typ des switch-Ausdrucks. Es handelt sich um einen Fehler, wenn kein solcher Typ vorhanden ist.

In einer default-Anweisung darf höchstens eine switch Kennzeichnung vorkommen.

Es ist ein Fehler, wenn das Muster einer Switchbezeichnung nicht anwendbar ist (§11.2.1) auf den Typ des Eingabeausdrucks.

Es ist ein Fehler, wenn das Muster einer beliebigen switch-Kennzeichnung subsumiert wird durch (§11.3) das Set der Muster früherer switch-Kennzeichnungen der switch-Anweisung, die keinen Case Guard haben oder deren Case Guard ein konstanter Ausdruck mit dem Wert true ist.

Beispiel:

switch (shape)
{
    case var x:
        break;
    case var _: // error: pattern subsumed, as previous case always matches
        break;
    default:
        break;  // warning: unreachable, all possible values already handled.
}

Endbeispiel

Eine switch Anweisung wird wie folgt ausgeführt:

  • Der switch-Ausdruck wird ausgewertet und in den kontrollierenden Typ umgewandelt.
  • Die Steuerung wird entsprechend dem Wert des umgewandelten switch-Ausdrucks übertragen:
    • Das lexikalisch erste Muster im Set von case Kennzeichnungen in derselben switch Anweisung, das mit dem Wert des switch-Ausdrucks übereinstimmt und für das der Guard-Ausdruck entweder nicht vorhanden ist oder den Wert TRUE hat, bewirkt, dass die Kontrolle auf die Anweisungsliste übertragen wird, die auf die übereinstimmende case Kennzeichnung folgt.
    • Andernfalls, wenn eine default Kennzeichnung vorhanden ist, wird die Kontrolle auf die Anweisungsliste übertragen, die auf die default Kennzeichnung folgt.
    • Andernfalls wird die Steuerung an den Endpunkt der switch Anweisung übertragen.

Hinweis: Die Reihenfolge, in der Muster zur Laufzeit abgeglichen werden, ist nicht definiert. Ein Compiler darf (muss aber nicht) Muster außerhalb der Reihenfolge abgleichen und die Ergebnisse bereits abgeglichener Muster wiederverwenden, um das Ergebnis des Abgleichs mit anderen Mustern zu berechnen. Dennoch ist ein Compiler erforderlich, um das lexikalisch erste Muster zu ermitteln, das mit dem Ausdruck übereinstimmt und für das die Guard-Klausel entweder nicht vorhanden ist oder zu true ausgewertet wird. Hinweisende

Wenn der Endpunkt der Anweisungsliste eines Switchabschnitts erreichbar ist, tritt ein Kompilierungszeitfehler auf. Dies wird als „no fall through“-Regel bezeichnet.

Beispiel: Das Beispiel

switch (i)
{
    case 0:
        CaseZero();
        break;
    case 1:
        CaseOne();
        break;
    default:
        CaseOthers();
        break;
}

ist gültig, da kein Schalterabschnitt einen erreichbaren Endpunkt aufweist. Im Gegensatz zu C und C++ darf die Ausführung eines switch-Abschnitts nicht zum nächsten switch-Abschnitt „durchfallen“, und das Beispiel

switch (i)
{
    case 0:
        CaseZero();
    case 1:
        CaseZeroOrOne();
    default:
        CaseAny();
}

führt zu einem Kompilierungszeitfehler. Wenn die Ausführung eines Schalterabschnitts auf die Ausführung eines anderen Schalterabschnitts folgt, muss eine explizite goto case oder goto default erklärung verwendet werden:

switch (i)
{
    case 0:
        CaseZero();
        goto case 1;
    case 1:
        CaseZeroOrOne();
        goto default;
    default:
        CaseAny();
        break;
}

Endbeispiel

In einer switch_section sind mehrere Labels zulässig.

Beispiel: Das Beispiel

switch (i)
{
    case 0:
        CaseZero();
        break;
    case 1:
        CaseOne();
        break;
    case 2:
    default:
        CaseTwo();
        break;
}

ist gültig. Das Beispiel verstößt nicht gegen die Regel "kein Durchfallen", da die Markierungen case 2: und default: Teil derselben switch_section sind.

Endbeispiel

Hinweis: Die "Kein-Durchfallen"-Regel verhindert eine häufige Fehlerklasse, die in C und C++ auftritt, wenn break Anweisungen versehentlich weggelassen werden. Zum Beispiel können die Abschnitte der obigen switch-Anweisung umgekehrt werden, ohne das Verhalten der Anweisung zu beeinflussen:

switch (i)
{
    default:
        CaseAny();
        break;
    case 1:
        CaseZeroOrOne();
        goto default;
    case 0:
        CaseZero();
        goto case 1;
}

Hinweisende

Hinweis: Die Anweisungsliste eines switch-Abschnitts endet typischerweise mit einer break, goto case oder goto default Anweisung, aber jedes Konstrukt, das den Endpunkt der Anweisungsliste unerreichbar macht, ist erlaubt. Beispielsweise ist eine while Anweisung, die vom booleschen Ausdruck true gesteuert wird, bekannt, dass sie nie ihren Endpunkt erreicht. Ebenso überträgt eine throw oder return Anweisung immer die Kontrolle an anderer Stelle und erreicht nie ihren Endpunkt. Daher ist das folgende Beispiel gültig:

switch (i)
{
     case 0:
         while (true)
         {
             F();
         }
     case 1:
         throw new ArgumentException();
     case 2:
         return;
}

Hinweisende

Beispiel: Der Regeltyp einer switch Anweisung kann der Typ stringsein. Zum Beispiel:

void DoCommand(string command)
{
    switch (command.ToLower())
    {
        case "run":
            DoRun();
            break;
        case "save":
            DoSave();
            break;
        case "quit":
            DoQuit();
            break;
        default:
            InvalidCommand(command);
            break;
    }
}

Endbeispiel

Hinweis: Ähnlich den Gleichheitsoperatoren für Zeichenketten (§12.12.8) ist die switch-Anweisung schreibempfindlich und führt einen gegebenen Switch-Abschnitt nur aus, wenn der Ausdruck der Switch-Zeichenkette exakt mit einer case-Beschriftungskonstante übereinstimmt. end note Wenn der Governance-Typ einer switch Anweisung string oder ein nullbarer Wertetyp ist, ist der Wert null als case gekennzeichnete Konstante zulässig.

Die statement_list eines switch_block kann Deklarationsanweisungen enthalten (§13.6). Der Bereich einer lokalen Variablen oder Konstante, die in einem Switchblock deklariert ist, ist der Switchblock.

Eine Schalterbezeichnung ist erreichbar, wenn mindestens eine der folgenden Bedingungen zutrifft:

  • Der switch-Ausdruck ist ein konstanter Wert und entweder
    • Die Kennzeichnung ist ein case, dessen Muster mit (§11.2.1) diesem Wert übereinstimmen würde, und der guard von Kennzeichnung ist entweder nicht vorhanden oder kein konstanter Ausdruck mit dem Wert false; oder
    • Es handelt sich um eine default Kennzeichnung, und kein Switch-Abschnitt enthält eine Case-Kennzeichnung, deren Muster mit diesem Wert übereinstimmen würde und deren Guard entweder nicht vorhanden oder ein konstanter Ausdruck mit dem Wert true ist.
  • Der switch-Ausdruck ist kein konstanter Wert und entweder
    • Die Kennzeichnung ist ein case ohne Guard oder mit einem Guard, dessen Wert nicht die Konstante false ist; oder
    • Es ist eine default Kennzeichnung und
      • das Set der Muster, die unter den Fällen der Switch-Anweisung auftreten, die keine Guards haben oder Guards haben, deren Wert die Konstante true ist, ist nicht vollständig (§11.4) für den Switch-Regulationstyp; oder
      • der switch governing Typ ist ein nullable Typ und das Set der Muster, die in den Fällen der switch-Anweisung vorkommen, die keine guard haben oder die guard haben, deren Wert die Konstante true ist, enthält kein Muster, das auf den Wert null passen würde.
  • Die Kennzeichnung switch wird durch eine erreichbare goto case oder goto default Anweisung referenziert.

Die Anweisungsliste eines bestimmten Switchabschnitts ist erreichbar, wenn die switch Anweisung erreichbar ist und der Switch-Abschnitt eine erreichbare Switchbezeichnung enthält.

Der Endpunkt einer switch Anweisung ist erreichbar, wenn die Switch-Anweisung erreichbar ist und mindestens eine der folgenden Werte zutrifft:

  • Die switch Anweisung enthält eine erreichbare break Anweisung, die die switch Anweisung verlässt.
  • Es ist keine default Kennzeichnung vorhanden und entweder
    • Der switch-Ausdruck ist ein nicht konstanter Wert und das Set der Muster, die in den Fällen der switch-Anweisung auftreten, die keine Guards haben oder Guards haben, deren Wert die Konstante true ist, ist nicht vollständig (§11.4) für den Steuerungstyp switch.
    • Der switch-Ausdruck ist ein nicht-konstanter Wert eines löschbaren Typs, und kein Muster, das unter den Fällen der switch-Anweisung auftaucht, die keine Guards haben oder Guards haben, deren Wert die Konstante true ist, würde auf den Wert null passen.
    • Der switch-Ausdruck ist ein konstanter Wert und keine case Kennzeichnung ohne Guard oder deren Guard die Konstante true ist, würde auf diesen Wert passen.

Beispiel: Der folgende Code zeigt eine prägnante Verwendung der when Klausel:

static object CreateShape(string shapeDescription)
{
   switch (shapeDescription)
   {
        case "circle":
            return new Circle(2);
        …
        case var o when string.IsNullOrWhiteSpace(o):
            return null;
        default:
            return "invalid shape description";
    }
}

Der Var-Fall stimmt mit nullder leeren Zeichenfolge oder einer beliebigen Zeichenfolge überein, die nur Leerzeichen enthält. Endbeispiel

13.9 Iterationsanweisungen

13.9.1 Allgemein

Iterationsanweisungen führen eine eingebettete Anweisung wiederholt aus.

iteration_statement
    : while_statement
    | do_statement
    | for_statement
    | foreach_statement
    ;

13.9.2 Die While-Anweisung

Die while Anweisung führt abhängig von der Bedingung eine eingebettete Anweisung null- oder mehrmals aus.

while_statement
    : 'while' '(' boolean_expression ')' embedded_statement
    ;

Eine while Anweisung wird wie folgt ausgeführt:

  • Die boolean_expression (§12.24) wird ausgewertet.
  • Wenn der boolesche Ausdruck ergibt true, wird das Steuerelement an die eingebettete Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt der eingebetteten Anweisung erreicht (möglicherweise von der Ausführung einer continue Anweisung), wird das Steuerelement an den Anfang der while Anweisung übertragen.
  • Wenn der boolesche Ausdruck ergibt false, wird das Steuerelement an den Endpunkt der while Anweisung übertragen.

Innerhalb der eingebetteten Anweisung einer while Anweisung kann eine break Anweisung (§13.10.2) verwendet werden, um die Steuerung an den Endpunkt der while Anweisung zu übertragen (damit die Iteration der eingebetteten Anweisung endet), und eine continue Anweisung (§13.10.3) kann verwendet werden, um die Steuerung an den Endpunkt der eingebetteten Anweisung zu übertragen (wodurch eine weitere Iteration der while Anweisung ausgeführt wird).

Die eingebettete Anweisung einer while Anweisung kann erreicht werden, wenn die while Anweisung erreichbar ist und der boolesche Ausdruck nicht den Konstantenwert falseaufweist.

Der Endpunkt einer while Anweisung ist erreichbar, wenn mindestens einer der folgenden Werte zutrifft:

  • Die while Anweisung enthält eine erreichbare break Anweisung, die die while Anweisung verlässt.
  • Die while Anweisung ist erreichbar, und der boolesche Ausdruck hat nicht den Konstantenwert true.

13.9.3 Die Do-Anweisung

Die do Anweisung führt eine eingebettete Anweisung bedingt ein- oder mehrmals aus.

do_statement
    : 'do' embedded_statement 'while' '(' boolean_expression ')' ';'
    ;

Eine do Anweisung wird wie folgt ausgeführt:

  • Die Steuerung wird an die eingebettete Anweisung übertragen.
  • Wenn und wenn die Steuerung den Endpunkt der eingebetteten Anweisung (möglicherweise aus ausführung einer continue Anweisung) erreicht, wird die boolean_expression (§12.24) ausgewertet. Wenn der boolesche Ausdruck ergibt true, wird das Steuerelement an den Anfang der do Anweisung übertragen. Andernfalls wird die Steuerung an den Endpunkt der do Anweisung übertragen.

Innerhalb der eingebetteten Anweisung einer do Anweisung kann eine break Anweisung (§13.10.2) verwendet werden, um die Steuerung an den Endpunkt der do Anweisung zu übertragen (damit die Iteration der eingebetteten Anweisung endet), und eine continue Anweisung (§13.10.3) kann verwendet werden, um die Steuerung an den Endpunkt der eingebetteten Anweisung zu übertragen (wodurch eine weitere Iteration der do Anweisung ausgeführt wird).

Die eingebettete Anweisung einer do-Anweisung ist erreichbar, wenn die do-Anweisung erreichbar ist.

Der Endpunkt einer do Anweisung ist erreichbar, wenn mindestens einer der folgenden Werte zutrifft:

  • Die do Anweisung enthält eine erreichbare break Anweisung, die die do Anweisung verlässt.
  • Der Endpunkt der eingebetteten Anweisung ist erreichbar, und der boolesche Ausdruck hat nicht den Konstantenwert true.

13.9.4 Die For-Anweisung

Die for Anweisung wertet eine Abfolge von Initialisierungsausdrücken aus und führt dann, während eine Bedingung wahr ist, wiederholt eine eingebettete Anweisung aus und wertet eine Abfolge von Iterationsausdrücken aus.

for_statement
    : 'for' '(' for_initializer? ';' for_condition? ';' for_iterator? ')'
      embedded_statement
    ;

for_initializer
    : local_variable_declaration
    | statement_expression_list
    ;

for_condition
    : boolean_expression
    ;

for_iterator
    : statement_expression_list
    ;

statement_expression_list
    : statement_expression (',' statement_expression)*
    ;

Die for_initializer besteht, sofern vorhanden, entweder aus einem local_variable_declaration (§13.6.2) oder einer Liste von statement_expression(§13.7) getrennt durch Kommas. Der Bereich einer lokalen Variablen, die von einem for_initializer deklariert wird, ist die for_initializer, for_condition, for_iterator und embedded_statement.

Die for_condition, sofern vorhanden, ist eine boolean_expression (§12.24).

Die for_iterator besteht, sofern vorhanden, aus einer Liste von statement_expressions (§13.7), die durch Kommas getrennt sind.

Eine for Anweisung wird wie folgt ausgeführt:

  • Wenn ein for_initializer vorhanden ist, werden die Variableninitialisierer oder Anweisungsausdrücke in der Reihenfolge ausgeführt, in der sie geschrieben werden. Dieser Schritt wird nur einmal ausgeführt.
  • Wenn ein for_condition vorhanden ist, wird es ausgewertet.
  • Wenn die for_condition nicht vorhanden ist oder wenn die Auswertung true ergibt, wird die Kontrolle an die eingebettete Anweisung übertragen. Wenn und wenn das Steuerelement den Endpunkt der eingebetteten Anweisung (möglicherweise von der Ausführung einer continue Anweisung) erreicht, werden die Ausdrücke des for_iterator (falls vorhanden) in Sequenz ausgewertet, und dann wird eine weitere Iteration durchgeführt, beginnend mit der Auswertung der for_condition im obigen Schritt.
  • Wenn die for_condition vorhanden ist und die Auswertung ergibt false, wird die Kontrolle an den Endpunkt der for Anweisung übertragen.

Innerhalb der eingebetteten Anweisung einer for-Anweisung kann eine break-Anweisung (§13.10.2) verwendet werden, um die Kontrolle an den Endpunkt der for-Anweisung zu übertragen (und damit die Iteration der eingebetteten Anweisung zu beenden), und eine continue-Anweisung (§13. 10.3) kann verwendet werden, um die Kontrolle an den Endpunkt der eingebetteten Anweisung zu übertragen (und damit den for_iterator auszuführen und eine weitere Iteration der for-Anweisung durchzuführen, beginnend mit der for_condition).

Die eingebettete Anweisung einer for-Anweisung ist erreichbar, wenn eine der folgenden Bedingungen erfüllt ist:

  • Die forAnweisung ist erreichbar und es ist keine for_condition vorhanden.
  • Die for Anweisung ist erreichbar, und es ist eine for_condition vorhanden, die nicht den konstanten Wert false aufweist.

Der Endpunkt einer for Anweisung ist erreichbar, wenn mindestens einer der folgenden Werte zutrifft:

  • Die for Anweisung enthält eine erreichbare break Anweisung, die die for Anweisung verlässt.
  • Die for Anweisung ist erreichbar, und es ist eine for_condition vorhanden, die nicht den konstanten Wert true aufweist.

13.9.5 Die Foreach-Anweisung

13.9.5.1 Allgemein

Die foreach Anweisung listet die Elemente einer Auflistung auf und führt eine eingebettete Anweisung für jedes Element der Auflistung aus.

foreach_statement
    : 'await'? 'foreach' '(' ref_kind? local_variable_type identifier
      'in' expression ')' embedded_statement
    ;

Die local_variable_type und der Bezeichner einer Foreach-Anweisung deklarieren die Iterationsvariable der Anweisung. Wenn der var Bezeichner als local_variable_type angegeben wird und kein benannter var Typ im Bereich enthalten ist, wird die Iterationsvariable als implizit typierte Iterationsvariable bezeichnet, und der Typ wird als Elementtyp der foreach Anweisung verwendet, wie unten angegeben.

Es stellt einen Kompilierungsfehler dar, wenn sowohl await als auch ref_kind in einem foreach statement vorhanden sind.

Wenn foreach_statement beide oder keine ref und readonly enthält, bezeichnet die Iterationsvariable eine Variable, die als schreibgeschützt behandelt wird. Andernfalls, wenn foreach_statementref ohne readonly enthält, bezeichnet die Iterationsvariable eine Variable, die beschreibbar sein soll.

Die Iterationsvariable entspricht einer lokalen Variablen mit einem Bereich, der sich über die eingebettete Anweisung erstreckt. Während der Ausführung einer foreach Anweisung stellt die Iterationsvariable das Auflistungselement dar, für das derzeit eine Iteration ausgeführt wird. Wenn die Iterationsvariable eine schreibgeschützte Variable bezeichnet, tritt ein Kompilierfehler auf, wenn die eingebettete Anweisung versucht, sie zu ändern (durch Zuweisung oder die ++- und ---Operatoren) oder sie als Referenz oder Ausgabeparameter zu übergeben.

Die Kompilierungszeitverarbeitung einer foreach Anweisung bestimmt zuerst den Auflistungstyp, den Enumerator-Typ und den Iterationstyp des Ausdrucks. Die Verarbeitung für eine foreach-Erklärung ist in §13.9.5.2 detailliert beschrieben, und der Prozess für eine await foreach-Erklärung ist in §13.9.5.3 beschrieben.

Anmerkung: Wenn Ausdruck den Wert null hat, wird zur Laufzeit ein System.NullReferenceException ausgelöst. Hinweisende

Eine Implementierung darf eine bestimmte foreach_statement anders implementieren, z. B. aus Leistungsgründen, solange das Verhalten mit der oben genannten Erweiterung konsistent ist.

13.9.5.2 Synchrone Foreach

Die Kompilierungszeitverarbeitung einer foreach Anweisung bestimmt zuerst den Auflistungstyp, den Enumerator-Typ und den Iterationstyp des Ausdrucks. Diese Festlegung erfolgt wie folgt:

  • Wenn der Ausdruckstyp X ein Arraytyp ist, gibt es eine implizite Verweiskonvertierung von X in die IEnumerable Schnittstelle (da System.Array diese Schnittstelle implementiert wird). Der Sammlungstyp ist die IEnumerable Schnittstelle, der Enumerationstyp ist die IEnumerator Schnittstelle und der Iterationstyp ist der Elementtyp des Arraytyps X.
  • Wenn der Typ X von Ausdruckdynamic ist, dann gibt es eine implizite Konvertierung von Ausdruck in die IEnumerable Schnittstelle (§10.2.10). Der Sammlungstyp ist die IEnumerable Schnittstelle, und der Enumerationstyp ist die IEnumerator Schnittstelle. Wenn der var Bezeichner als local_variable_type angegeben wird, ist der Iterationstyp dynamic. Andernfalls ist er object.
  • Ermitteln Sie andernfalls, ob der Typ X über eine geeignete GetEnumerator Methode verfügt:
    • Führen Sie die Mitgliedersuche für den Typ X mit dem Bezeichner GetEnumerator und ohne Typargumente durch. Wenn die Mitgliederabfrage entweder keine Übereinstimmung ergibt oder zu einer Mehrdeutigkeit führt oder zu einer Übereinstimmung führt, die keine Methodengruppe ist, sollten Sie nach einer aufzählbaren Schnittstelle suchen, wie im Folgenden beschrieben. Es wird empfohlen, eine Warnung auszugeben, wenn die Mitgliedersuche etwas anderes als eine Methodengruppe oder keine Übereinstimmung ergibt.
    • Führen Sie die Überladungsauflösung mithilfe der resultierenden Methodengruppe und einer leeren Argumentliste aus. Führt die Überladungsauflösung zu keiner anwendbaren Methode, führt zu einer Mehrdeutigkeit oder führt zu einer einzigen besten Methode, aber diese Methode ist entweder statisch oder nicht öffentlich, überprüfen Sie, wie unten beschrieben, auf eine aufzählbare Schnittstelle. Es wird empfohlen, dass eine Warnung ausgegeben wird, wenn die Überladungsauflösung nichts anderes als eine eindeutige öffentliche Instanzmethode oder keine anwendbaren Methoden erzeugt.
    • Wenn der Rückgabetyp E der GetEnumerator Methode keine Klasse, Struktur oder Schnittstellentyp ist, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Membersuche wird bei E mit dem Bezeichner Current und ohne Typargumente durchgeführt. Wenn die Membersuche keine Übereinstimmung ergibt, das Ergebnis ein Fehler ist oder es sich bei dem Ergebnis nicht um eine öffentliche Instanz-Eigenschaft handelt, die das Lesen zulässt, wird ein Fehler erzeugt, und es werden keine weiteren Schritte unternommen.
    • Membersuche wird bei E mit dem Bezeichner MoveNext und ohne Typargumente durchgeführt. Wenn die Membersuche keine Übereinstimmung produziert, führt dies zu einem Fehler oder wenn das Ergebnis etwas anderes als eine Methodengruppe ist, führt dies zu einem Fehler, und es werden keine weiteren Schritte unternommen.
    • Die Auflösung von Überladungen wird für die Methodengruppe mit einer leeren Argumentliste durchgeführt. Wenn die Überladungsauflösung zu keiner anwendbaren Methode führt, zu Mehrdeutigkeit oder einer einzigen besten Methode führt, aber diese Methode entweder statisch oder nicht öffentlich ist, oder ihr Rückgabewert nicht bool ist, wird ein Fehler erzeugt und es werden keine weiteren Schritte ausgeführt.
    • Der Auflistungstyp ist X, der Enumerationstyp ist E, und der Iterationstyp ist der Typ der Current Eigenschaft. Die Current-Eigenschaft kann den ref-Modifikator enthalten. In diesem Fall ist der zurückgegebene Ausdruck eine variable_reference (§9.5), die optional schreibgeschützt ist.
  • Suchen Sie andernfalls nach einer aufzählbaren Schnittstelle:
    • Wenn unter allen Typen Tᵢ, für die es eine implizite Konvertierung von X zu IEnumerable<Tᵢ> gibt, ein eindeutiger Typ T existiert, wobei T nicht dynamic ist, und für alle anderen Tᵢ eine implizite Konvertierung von IEnumerable<T> zu IEnumerable<Tᵢ> besteht, dann ist der Sammlungstyp die Schnittstelle IEnumerable<T>, der Enumeratortyp die Schnittstelle IEnumerator<T> und der Iterationstyp T.
    • Andernfalls wird, wenn mehr als ein solcher Typ Tvorhanden ist, ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Andernfalls, wenn es eine implizite Konversion von X zur Schnittstelle System.Collections.IEnumerable gibt, ist der Typ der Collection diese Schnittstelle, der Typ des Aufzählers ist die Schnittstelle System.Collections.IEnumerator und der Typ der Iteration ist object.
    • Andernfalls wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.

Die obigen Schritte erzeugen, falls erfolgreich, eindeutig einen Sammlungstyp, Enumeratortyp CEund Iterationstyp T, ref Toder ref readonly T. Eine foreach-Anweisung der Form

foreach (V v in x) «embedded_statement»

ist dann gleichbedeutend mit:

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            «embedded_statement»
        }
    }
    finally
    {
        ... // Dispose e
    }
}

Die Variable e ist für den Ausdruck x oder die eingebettete Anweisung oder einen anderen Quellcode des Programms nicht sichtbar oder zugänglich. Die Variable v ist in der eingebetteten Anweisung schreibgeschützt. Wenn keine explizite Konvertierung (§10.3) von T (dem Iterationstyp) in V (die local_variable_type in der foreach Anweisung) vorliegt, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.

Wenn die Iterationsvariable eine Referenzvariable ist (§9.7), eine foreach-Anweisung der Form

foreach (ref V v in x) «embedded_statement»

ist dann gleichbedeutend mit:

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            ref V v = ref e.Current;
            «embedded_statement»
        }
    }
    finally
    {
        ... // Dispose e
    }
}

Die Variable e ist für den Ausdruck x oder die eingebettete Anweisung oder einen anderen Quellcode des Programms nicht sichtbar oder zugänglich. Die Referenzvariable v ist in der eingebetteten Anweisung schreibgeschützt, aber v darf nicht neu zugewiesen werden (§12.21.3). Wenn keine Identitätskonvertierung (§10.2.2) von T (dem Iterationstyp) zu V (dem local_variable_type in der foreach Anweisung) vorhanden ist, wird ein Fehler erzeugt und es werden keine weiteren Schritte ausgeführt.

Eine foreach-Anweisung der Form foreach (ref readonly V v in x) «embedded_statement» hat eine ähnliche Form, aber die Referenzvariable v ist ref readonly in der eingebetteten Anweisung und kann daher nicht neu zugewiesen oder neu zugewiesen werden.

Die Platzierung von v innerhalb der while-Schleife ist wichtig für die Art und Weise, wie sie von jeder anonymen Funktion, die in der embedded_statement auftritt, erfasst wird (§12.19.6.2).

Beispiel:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null)
    {
        f = () => Console.WriteLine("First value: " + value);
    }
}
f();

Wenn v in der erweiterten Form außerhalb der while Schleife deklariert würde, würde sie für alle Iterationen freigegeben, und ihr Wert nach der for Schleife wäre der endwert, 13was der Aufruf der f Schleife drucken würde. Da jede Iteration über ihre eigene Variable v verfügt, wird die in der ersten Iteration erfasste Variable f weiterhin den Wert 7 halten, der ausgedruckt wird. (Beachten Sie, dass frühere Versionen von C# v außerhalb der while-Schleife deklarierten.)

Endbeispiel

Der Inhalt des finally Blocks wird in den folgenden Schritten gemäß erstellt:

  • Wenn es eine implizite Konvertierung von E zur System.IDisposable Schnittstelle gibt, dann

    • Wenn E ein nicht-nullbarer Werttyp ist, wird die finally-Klausel zum semantischen Äquivalent von erweitert:

      finally
      {
          ((System.IDisposable)e).Dispose();
      }
      
    • Andernfalls wird die Klausel finally auf das semantische Äquivalent des Folgenden erweitert:

      finally
      {
          System.IDisposable d = e as System.IDisposable;
          if (d != null)
          {
              d.Dispose();
          }
      }
      

      Wenn E jedoch ein Werttyp ist oder ein Typparameter, der zu einem Werttyp instanziiert wurde, verursacht die Konvertierung von e in System.IDisposable kein Boxing.

  • Wenn es sich bei E um einen versiegelten Typ handelt, wird die finally-Klausel andernfalls auf einen leeren Block erweitert.

    finally {}
    
  • Andernfalls wird die finally Klausel erweitert auf:

    finally
    {
        System.IDisposable d = e as System.IDisposable;
        if (d != null)
        {
            d.Dispose();
        }
    }
    

Die lokale Variable d ist für Benutzercode nicht sichtbar oder zugänglich. Insbesondere steht es nicht im Konflikt mit einer anderen Variablen, deren Bereich den finally Block enthält.

Die Reihenfolge, foreach in der die Elemente eines Arrays durchlaufen werden, lautet wie folgt: Bei eindimensionalen Arrays werden Elemente in zunehmender Indexreihenfolge durchlaufen, beginnend mit Index 0 und enden mit Index Length – 1. Bei mehrdimensionalen Arrays werden die Elemente so durchlaufen, dass zuerst die Indizes der ganz rechten Dimension erhöht werden, dann die der nächsten linken Dimension und so weiter nach links.

Beispiel: Im folgenden Beispiel werden die einzelnen Werte in einer zweidimensionalen Matrix in der Elementreihenfolge gedruckt:

class Test
{
    static void Main()
    {
        double[,] values =
        {
            {1.2, 2.3, 3.4, 4.5},
            {5.6, 6.7, 7.8, 8.9}
        };
        foreach (double elementValue in values)
        {
            Console.Write($"{elementValue} ");
        }
        Console.WriteLine();
    }
}

Die Ausgabe sieht folgendermaßen aus:

1.2 2.3 3.4 4.5 5.6 6.7 7.8 8.9

Endbeispiel

Beispiel: Im folgenden Beispiel

int[] numbers = { 1, 3, 5, 7, 9 };
foreach (var n in numbers)
{
    Console.WriteLine(n);
}

Der Typ von n wird auf int abgeleitet, der Iterationstyp von numbers.

Endbeispiel

13.9.5.3 await foreach

Die Kompilierungszeitverarbeitung einer foreach Anweisung bestimmt zuerst den Auflistungstyp, den Enumerator-Typ und den Iterationstyp des Ausdrucks. Die Verarbeitung für eine foreach-Erklärung ist in §13.9.5.2 detailliert beschrieben, und der Prozess für eine await foreach-Erklärung ist in §13.9.5.3 beschrieben.

Diese Festlegung erfolgt wie folgt:

  • Ermitteln Sie, ob der Typ X über eine geeignete GetAsyncEnumerator Methode verfügt:
    • Führen Sie die Mitgliedersuche für den Typ X mit dem Bezeichner GetAsyncEnumerator und ohne Typargumente durch. Wenn die Mitgliederabfrage entweder keine Übereinstimmung ergibt oder zu einer Mehrdeutigkeit führt oder zu einer Übereinstimmung führt, die keine Methodengruppe ist, sollten Sie nach einer aufzählbaren Schnittstelle suchen, wie im Folgenden beschrieben. Es wird empfohlen, eine Warnung auszugeben, wenn die Mitgliedersuche etwas anderes als eine Methodengruppe oder keine Übereinstimmung ergibt.
    • Führen Sie die Überladungsauflösung mithilfe der resultierenden Methodengruppe und einer leeren Argumentliste aus. Führt die Überladungsauflösung zu keiner anwendbaren Methode, führt zu einer Mehrdeutigkeit oder führt zu einer einzigen besten Methode, aber diese Methode ist entweder statisch oder nicht öffentlich, überprüfen Sie, wie unten beschrieben, auf eine aufzählbare Schnittstelle. Es wird empfohlen, dass eine Warnung ausgegeben wird, wenn die Überladungsauflösung nichts anderes als eine eindeutige öffentliche Instanzmethode oder keine anwendbaren Methoden erzeugt.
    • Wenn der Rückgabetyp E der GetAsyncEnumerator Methode keine Klasse, Struktur oder Schnittstellentyp ist, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Membersuche wird bei E mit dem Bezeichner Current und ohne Typargumente durchgeführt. Wenn die Membersuche keine Übereinstimmung ergibt, das Ergebnis ein Fehler ist oder es sich bei dem Ergebnis nicht um eine öffentliche Instanz-Eigenschaft handelt, die das Lesen zulässt, wird ein Fehler erzeugt, und es werden keine weiteren Schritte unternommen.
    • Membersuche wird bei E mit dem Bezeichner MoveNextAsync und ohne Typargumente durchgeführt. Wenn die Membersuche keine Übereinstimmung produziert, führt dies zu einem Fehler oder wenn das Ergebnis etwas anderes als eine Methodengruppe ist, führt dies zu einem Fehler, und es werden keine weiteren Schritte unternommen.
    • Die Auflösung von Überladungen wird für die Methodengruppe mit einer leeren Argumentliste durchgeführt. Führt die Überladungsauflösung zu keiner anwendbaren Methode, führt zu einer Mehrdeutigkeit oder führt zu einer einzigen besten Methode, aber diese Methode ist entweder statisch oder nicht öffentlich, oder der Rückgabetyp kann nicht erwartet werden (§12.9.8.2), wenn die await_expression als ein bool (§12.9.8.3) klassifiziert wird, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Der Auflistungstyp ist X, der Enumerationstyp ist E, und der Iterationstyp ist der Typ der Current Eigenschaft.
  • Suchen Sie andernfalls nach einer asynchronen enumerationsfähigen Schnittstelle:
    • Wenn unter allen Typen Tᵢ, für die es eine implizite Konvertierung von X zu IAsyncEnumerable<Tᵢ> gibt, ein eindeutiger Typ T existiert, wobei T nicht dynamic ist, und für alle anderen Tᵢ eine implizite Konvertierung von IAsyncEnumerable<T> zu IAsyncEnumerable<Tᵢ> besteht, dann ist der Sammlungstyp die Schnittstelle IAsyncEnumerable<T>, der Enumeratortyp die Schnittstelle IAsyncEnumerator<T> und der Iterationstyp T.
    • Andernfalls wird, wenn mehr als ein solcher Typ Tvorhanden ist, ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Andernfalls wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.

Die oben genannten Schritte führen, wenn erfolgreich, eindeutig zu einem Sammlungstyp C, Enumeratortyp E und Iterationstyp T. Eine await foreach Anweisung der Form

await foreach (V v in x) «embedded_statement»

ist dann gleichbedeutend mit:

{
    E e = ((C)(x)).GetAsyncEnumerator();
    try
    {
        while (await e.MoveNextAsync())
        {
            V v = (V)(T)e.Current;
            «embedded_statement»
        }
    }
    finally
    {
        ... // Dispose e
    }
}

Die Variable e ist für den Ausdruck x oder die eingebettete Anweisung oder einen anderen Quellcode des Programms nicht sichtbar oder zugänglich. Die Variable v ist in der eingebetteten Anweisung schreibgeschützt. Wenn keine explizite Konvertierung (§10.3) von T (dem Iterationstyp) in V (die local_variable_type in der await foreach Anweisung) vorliegt, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.

Ein asynchroner Enumerator kann optional eine DisposeAsync-Methode zur Verfügung stellen, die ohne Argumente aufgerufen werden kann und die etwas zurückgibt, das awaitgefragt werden kann und dessen GetResult()Rückgabe void ist.

Eine foreach-Anweisung der Form

await foreach (T item in enumerable) «embedded_statement»

wird erweitert auf:

var enumerator = enumerable.GetAsyncEnumerator();
try
{
    while (await enumerator.MoveNextAsync())
    {
       T item = enumerator.Current;
       «embedded_statement»
    }
}
finally
{
    await enumerator.DisposeAsync(); // omitted, along with the try/finally,
                            // if the enumerator doesn't expose DisposeAsync
}

13.10 Sprunganweisungen

13.10.1 Allgemein

Sprunganweisungen übertragen bedingungslos die Kontrolle.

jump_statement
    : break_statement
    | continue_statement
    | goto_statement
    | return_statement
    | throw_statement
    ;

Die Position, an die eine Sprung-Anweisung die Steuerung überträgt, wird als Ziel der Jump-Anweisung bezeichnet.

Wenn eine Sprung-Anweisung innerhalb eines Blocks auftritt und das Ziel dieser Sprung-Anweisung außerhalb dieses Blocks liegt, wird gesagt, dass die Sprung-Anweisung den Block verlässt. Während eine Sprung-Anweisung die Steuerung aus einem Block übertragen kann, kann sie niemals die Steuerung in einen Block übertragen.

Die Ausführung von Sprunganweisungen wird durch das Vorhandensein von zwischengeschalteten try-Anweisungen erschwert. In Ermangelung solcher try Anweisungen überträgt eine Sprunganweisung bedingungslos die Kontrolle von der Sprunganweisung auf ihr Ziel. In Anwesenheit solcher dazwischenliegenden try Anweisungen ist die Ausführung komplexer. Wenn die Sprunganweisung einen oder mehrere try-Blöcke mit zugehörigen finally-Blöcken verlässt, wird die Kontrolle zunächst an den finally-Block der innersten try-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.

Beispiel: Im folgenden Code

class Test
{
    static void Main()
    {
        while (true)
        {
            try
            {
                try
                {
                    Console.WriteLine("Before break");
                    break;
                }
                finally
                {
                    Console.WriteLine("Innermost finally block");
                }
            }
            finally
            {
                Console.WriteLine("Outermost finally block");
            }
        }
        Console.WriteLine("After break");
    }
}

werden die finally Blöcke, die mit zwei try Anweisungen verbunden sind, ausgeführt, bevor die Kontrolle an das Ziel der Sprunganweisung übertragen wird. Die Ausgabe sieht folgendermaßen aus:

Before break
Innermost finally block
Outermost finally block
After break

Endbeispiel

13.10.2 Die Break-Anweisung

Die break-Anweisung verlässt die nächste einschließende switch, while, do, for oder foreach-Anweisung.

break_statement
    : 'break' ';'
    ;

Das Ziel einer break-Anweisung ist der Endpunkt der nächstgelegenen einschließenden switch, while, do, for oder foreach-Anweisung. Wenn eine break Anweisung nicht von einem switch, while, do, for oder foreach Anweisung eingeschlossen ist, tritt ein Kompilierungszeitfehler auf.

Wenn mehrere switch, , while, do, foroder foreach Anweisungen miteinander verschachtelt sind, gilt eine break Anweisung nur für die innerste Anweisung. Um die Steuerung über mehrere verschachtelte Ebenen hinweg zu übertragen, wird eine goto-Anweisung (§13.10.4) verwendet.

Eine break Anweisung kann einen finally Block nicht beenden (§13.11). Wenn eine break Anweisung innerhalb eines finally Blocks auftritt, liegt das Ziel der break Anweisung innerhalb desselben finally Blocks; andernfalls tritt ein Kompilierungsfehler auf.

Eine break Anweisung wird wie folgt ausgeführt:

  • Wenn die break-Anweisung einen oder mehrere try-Blöcke mit zugeordneten finally-Blöcken verlässt, wird die Kontrolle zunächst an den finally-Block der innersten try-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
  • Die Steuerung wird an das Ziel der break Anweisung übertragen.

Da eine break Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer break Aussage nie erreichbar.

13.10.3 Die Continue-Anweisung

Die continue-Anweisung startet eine neue Iteration der nächstgelegenen einschließenden while, do, for oder foreach-Anweisung.

continue_statement
    : 'continue' ';'
    ;

Das Ziel einer continue-Anweisung ist der Endpunkt der eingebetteten Anweisung der nächstgelegenen einschließenden while, do, for oder foreach-Anweisung. Wenn eine continue Anweisung nicht durch eine while, do, for oder foreach Anweisung eingeschlossen ist, tritt ein Kompilierungszeitfehler auf.

Wenn mehrere while, , do, foroder foreach Anweisungen miteinander verschachtelt sind, gilt eine continue Anweisung nur für die innerste Anweisung. Um die Steuerung über mehrere verschachtelte Ebenen hinweg zu übertragen, wird eine goto-Anweisung (§13.10.4) verwendet.

Eine continue Anweisung kann einen finally Block nicht beenden (§13.11). Wenn eine continue Anweisung innerhalb eines finally Blocks auftritt, liegt das Ziel der continue Anweisung innerhalb desselben finally Blocks; andernfalls tritt ein Kompilierungsfehler auf.

Eine continue Anweisung wird wie folgt ausgeführt:

  • Wenn die continue-Anweisung einen oder mehrere try-Blöcke mit zugeordneten finally-Blöcken verlässt, wird die Kontrolle zunächst an den finally-Block der innersten try-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
  • Die Steuerung wird an das Ziel der continue Anweisung übertragen.

Da eine continue Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer continue Aussage nie erreichbar.

13.10.4 Die goto-Anweisung

Die goto Anweisung überträgt die Steuerung an eine Anweisung, die durch eine Bezeichnung gekennzeichnet ist.

goto_statement
    : 'goto' identifier ';'
    | 'goto' 'case' constant_expression ';'
    | 'goto' 'default' ';'
    ;

Das Ziel einer gotoKennzeichnung-Anweisung ist die gekennzeichnete Anweisung mit der angegebenen Kennzeichnung. Wenn eine Bezeichnung mit dem angegebenen Namen nicht im aktuellen Funktionselement vorhanden ist oder sich die goto Anweisung nicht im Bereich der Bezeichnung befindet, tritt ein Kompilierungszeitfehler auf.

Hinweis: Diese Regel ermöglicht die Verwendung einer goto Anweisung, die Kontrolle aus einem geschachtelten Bereich, aber nicht in einen geschachtelten Bereich zu übertragen. Im Beispiel

class Test
{
    static void Main(string[] args)
    {
        string[,] table =
        {
            {"Red", "Blue", "Green"},
            {"Monday", "Wednesday", "Friday"}
        };
        foreach (string str in args)
        {
            int row, colm;
            for (row = 0; row <= 1; ++row)
            {
                for (colm = 0; colm <= 2; ++colm)
                {
                    if (str == table[row,colm])
                    {
                        goto done;
                    }
                }
            }
            Console.WriteLine($"{str} not found");
            continue;
          done:
            Console.WriteLine($"Found {str} at [{row}][{colm}]");
        }
    }
}

eine goto-Anweisung wird verwendet, um die Kontrolle aus einem verschachtelten Bereich heraus zu übertragen.

Hinweisende

Das Ziel einer goto case-Anweisung ist die Anweisungsliste in der unmittelbar einschließenden switch-Anweisung (§13.8.3), die eine case-Kennzeichnung mit einem konstanten Muster des angegebenen konstanten Werts und keinen Guard enthält. Wenn die goto case Anweisung nicht von einer switch Anweisung eingeschlossen ist, wenn die nächstgelegene umgebende switch Anweisung keine solche case enthält oder die constant_expression nicht implizit (§10.2) in den Regeltyp der nächstgelegenen umgebenden switch Anweisung konvertierbar ist, tritt ein Kompilierfehler auf.

Das Ziel einer goto default Anweisung ist die Anweisungsliste in der unmittelbar eingeschlossenen switch Anweisung (§13.8.3), die eine default Bezeichnung enthält. Wenn die goto default Anweisung nicht von einer switch Anweisung eingeschlossen ist oder die nächste eingeschlossene switch Anweisung keine default Bezeichnung enthält, tritt ein Kompilierungszeitfehler auf.

Eine goto Anweisung kann einen finally Block nicht beenden (§13.11). Wenn eine goto Anweisung innerhalb eines finally Blocks auftritt, muss sich das Ziel der goto Anweisung innerhalb desselben finally Blocks befinden oder andernfalls tritt ein Kompilierungsfehler auf.

Eine goto Anweisung wird wie folgt ausgeführt:

  • Wenn die goto-Anweisung einen oder mehrere try-Blöcke mit zugeordneten finally-Blöcken verlässt, wird die Kontrolle zunächst an den finally-Block der innersten try-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
  • Die Steuerung wird an das Ziel der goto Anweisung übertragen.

Da eine goto Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer goto Aussage nie erreichbar.

13.10.5 Die Return-Anweisung

Die return Anweisung gibt die Steuerung an den aktuellen Aufrufer des Funktionsmitglieds zurück, in dem die Rückgabeanweisung vorkommt, indem sie optional einen Wert oder eine variable_reference (§9.5) zurückgibt.

return_statement
    : 'return' ';'
    | 'return' expression ';'
    | 'return' 'ref' variable_reference ';'
    ;

Ein return_statement ohne Ausdruck wird return-no-value genannt; eines, das refAusdruck enthält, wird return-by-ref genannt; und eines, das nur Ausdruck enthält, wird return-by-value genannt.

Es ist ein Kompilierfehler, einen return-no-value aus einer Methode zu verwenden, die als return-by-value oder return-by-ref deklariert ist (§15.6.1).

Es ist ein Kompilierfehler, einen return-by-ref von einer Methode zu verwenden, die als return-no-value oder return-by-value deklariert ist.

Es ist ein Kompilierfehler, einen return-by-value von einer Methode zu verwenden, die als returns-no-value oder return-by-ref deklariert ist.

Es handelt sich um einen Kompilierfehler bei der Verwendung von return-by-ref, wenn der Ausdruck keine Variablenreferenz ist oder es sich um einen Verweis auf eine Variable handelt, deren ref-safe-context nicht der Aufruferkontext ist (§9.7.2).

Es ist ein Kompilierungsfehler, eine Rückgabe per Referenz aus einer Methode zu verwenden, die mit dem method_modifierasync deklariert ist.

Von einem Funktionsmitglied wird gesagt, dass es einen Wert berechnet, wenn es eine Methode mit einer Return-by-Value-Methode (§15.6.11), ein Return-by-Value-Get-Accessor einer Eigenschaft oder eines Indexers oder ein benutzerdefinierter Operator ist. Funktionsmitglieder, die returns-no-value sind, berechnen keinen Wert und sind Methoden mit dem effektiven Rückgabetyp void, Set-Accessoren von Eigenschaften und Indexern, Add- und Remove-Accessoren von Ereignissen, Instanz-Konstruktoren, statische Konstruktoren und Finalizer. Funktionsmitglieder, die return-by-ref sind, berechnen keinen Wert.

Für eine Rückgabe durch Wert muss eine implizite Konvertierung (§10.2) vom Typ des Ausdrucks in den effektiven Rückgabetyp (§15.6.11) des enthaltenden Funktionselements bestehen. Bei einem return-by-ref muss eine Identitätskonversion (§10.2.2) zwischen dem Typ von Ausdruck und dem effektiven Rückgabetyp des enthaltenden Funktionsmitglieds bestehen.

return-Anweisungen können auch im Body von anonymen Funktionsausdrücken verwendet werden (§12.19) und sind an der Bestimmung der Konversionen beteiligt, die für diese Funktionen existieren (§10.7.1).

Es handelt sich um einen Kompilierungszeitfehler für eine return Anweisung, die in einem finally Block (§13.11) angezeigt wird.

Eine return Anweisung wird wie folgt ausgeführt:

  • Bei einem Return-by-Value wird Ausdruck ausgewertet und sein Wert durch eine implizite Konversion in den effektiven Rückgabetyp der enthaltenen Funktion umgewandelt. Das Ergebnis der Konvertierung wird zum Ergebniswert, der von der Funktion erzeugt wird. Bei einem return-by-ref wird der Ausdruck ausgewertet und das Ergebnis wird als Variable klassifiziert. Wenn der Return-by-Ref der einschließenden Methode readonly enthält, ist die resultierende Variable schreibgeschützt.
  • Wenn die return Anweisung von einem oder mehreren trycatch Blöcken mit zugeordneten finally Blöcken eingeschlossen wird, wird die Steuerung zunächst in den finally Block der innersten try Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird so lange wiederholt, bis die finally Blockierungen aller einschließenden try Anweisungen ausgeführt wurden.
  • Wenn die enthaltende Funktion keine asynchrone Funktion ist, wird das Steuerelement an den Aufrufer der enthaltenden Funktion zusammen mit dem Ergebniswert zurückgegeben, falls vorhanden.
  • Wenn die enthaltende Funktion eine asynchrone Funktion ist, wird das Steuerelement an den aktuellen Aufrufer zurückgegeben, und der Ergebniswert (falls vorhanden) wird in der Rückgabeaufgabe aufgezeichnet, wie in (§15.14.3) beschrieben.

Da eine return Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer return Aussage nie erreichbar.

13.10.6 Die Throw-Anweisung

Die Anweisung throw löst eine Ausnahme aus.

throw_statement
    : 'throw' expression? ';'
    ;

Eine throw-Anweisung mit einem Ausdruck löst eine Ausnahme aus, die durch die Auswertung des Ausdrucks erzeugt wird. 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. Wenn das Ergebnis der Konvertierung null ist, wird stattdessen ein System.NullReferenceException ausgelöst.

Eine throw-Anweisung ohne Ausdruck kann nur in einem catch-Block verwendet werden. In diesem Fall löst diese Anweisung die Ausnahme, die aktuell von diesem catch-Block behandelt wird, erneut aus.

Da eine throw Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer throw Aussage nie erreichbar.

Wenn eine Ausnahme ausgelöst wird, wird die Kontrolle an die erste catch-Klausel in einer einschließenden try-Anweisung übertragen, die die Ausnahme behandeln kann. Der Prozess, der vom Auftreten einer Ausnahme bis zur Übergabe der Kontrolle an einen geeigneten Ausnahme-Handler abläuft, wird als Ausbreitung der Ausnahme bezeichnet. Die Verteilung einer Ausnahme besteht aus wiederholter Auswertung der folgenden Schritte, bis eine catch Klausel gefunden wird, die der Ausnahme entspricht. In dieser Beschreibung ist der Wurfpunkt zunächst der Ort, an dem die Ausnahme ausgelöst wird. Dieses Verhalten wird in (§21.4) angegeben.

  • Im aktuellen Funktionsmitglied wird jede try-Anweisung untersucht, die den Wurfpunkt einschließt. Für jede Anweisung S, beginnend mit der innersten try Anweisung und endet mit der äußersten try Anweisung, werden die folgenden Schritte ausgewertet:

    • Wenn der try-Block von S den Übergabepunkt einschließt und wenn S eine oder mehrere catch-Klauseln enthält, werden die catch-Klauseln in der Reihenfolge ihres Erscheinens untersucht, um einen geeigneten Handler für die Ausnahme zu finden. Die erste catch-Klausel, die einen Ausnahmetyp T (oder einen Typparameter, der zur Laufzeit einen Ausnahmetyp T bezeichnet) angibt, sodass sich der Laufzeittyp E von T ableitet, wird als Übereinstimmung betrachtet. Wenn die Klausel einen Ausnahmefilter enthält, wird das Ausnahmeobjekt der Ausnahmevariablen zugewiesen, und der Ausnahmefilter wird ausgewertet. Wenn eine catch-Klausel einen Ausnahmefilter enthält, gilt die catch-Klausel als übereinstimmend, wenn der Ausnahmefilter zu true ausgewertet wird. Eine allgemeine catch (§13.11) Klausel wird für jeden Ausnahmetyp als übereinstimmend angesehen. Wenn eine übereinstimmende catch-Klausel gefunden wird, wird die Weitergabe der Ausnahme abgeschlossen, indem die Kontrolle auf den Block dieser catch-Klausel übertragen wird.
    • Andernfalls, wenn der try Block oder ein catch Block von S den Wurfpunkt umschließt und S über einen finally Block verfügt, wird die Kontrolle an den finally Block übergeben. Wenn der finally-Block eine weitere Ausnahme auslöst, wird die Verarbeitung der aktuellen Ausnahme beendet. Andernfalls wird die Verarbeitung der aktuellen Ausnahme fortgesetzt, wenn das Steuerelement den Endpunkt des finally Blocks erreicht.
  • Wenn sich ein Ausnahmehandler nicht im aktuellen Funktionsaufruf befindet, wird der Funktionsaufruf beendet, und einer der folgenden Aktionen tritt auf:

    • Wenn die aktuelle Funktion nicht asynchron ist, werden die obigen Schritte für den Aufrufer der Funktion mit einem Wurfpunkt wiederholt, der der Anweisung entspricht, aus der das Funktionselement aufgerufen wurde.

    • Wenn die aktuelle Funktion asynchron und task-returning ist, wird die Ausnahme in der Rückgabeaufgabe aufgezeichnet, die in einen fehlerhaften oder abgebrochenen Zustand versetzt wird, wie in §15.14.3 beschrieben.

    • Wenn die aktuelle Funktion asynchron ist und void-zurückkehrt, wird der Synchronisationskontext des aktuellen Threads wie in §15.14.4 beschrieben benachrichtigt.

  • Wenn die Ausnahmeverarbeitung alle Funktionsmemembeaufrufe im aktuellen Thread beendet, was angibt, dass der Thread keinen Handler für die Ausnahme hat, wird der Thread selbst beendet. Die Auswirkungen einer solchen Beendigung sind implementierungsabhängig.

13.11 Die try-Anweisung

Die try Anweisung stellt einen Mechanismus zum Abfangen von Ausnahmen bereit, die während der Ausführung eines Blocks auftreten. Darüber hinaus bietet die try Anweisung die Möglichkeit, einen Codeblock anzugeben, der immer ausgeführt wird, wenn das Steuerelement die try Anweisung verlässt.

try_statement
    : 'try' block catch_clauses
    | 'try' block catch_clauses? finally_clause
    ;

catch_clauses
    : specific_catch_clause+
    | specific_catch_clause* general_catch_clause
    ;

specific_catch_clause
    : 'catch' exception_specifier exception_filter? block
    | 'catch' exception_filter block
    ;

exception_specifier
    : '(' type identifier? ')'
    ;

exception_filter
    : 'when' '(' boolean_expression ')'
    ;

general_catch_clause
    : 'catch' block
    ;

finally_clause
    : 'finally' block
    ;

Ein try_statement besteht aus dem Schlüsselwort try gefolgt von einem Block, dann null oder mehr catch_clauses und dann einem optionalen finally_clause. Es muss mindestens eine catch_clause oder eine finally_clause vorhanden sein.

In einem exception_specifier muss der Typ oder seine effektive Basisklasse, wenn es sich um einen type_parameter handelt, System.Exception oder ein Typ sein, der von ihm abgeleitet wird.

Wenn eine catch Klausel sowohl eine class_type als auch einen Bezeichner angibt, wird eine Ausnahmevariable des angegebenen Namens und Typs deklariert. Die Ausnahmevariable wird in den Deklarationsbereich des specific_catch_clause (§7.3) eingeführt. Während der Ausführung des exception_filter und catch Blocks repräsentiert die Ausnahmevariable die Ausnahme, die aktuell behandelt wird. Für die Zwecke der endgültigen Zuordnungsprüfung gilt die Ausnahmevariable in ihrem gesamten Umfang als definitiv zugewiesen.

Sofern keine catch Klausel einen Ausnahmevariablennamen enthält, ist es unmöglich, auf das Ausnahmeobjekt im Filter und catch Block zuzugreifen.

Eine catch Klausel, die weder einen Ausnahmetyp noch einen Ausnahmevariablennamen angibt, wird als allgemeine catch Klausel bezeichnet. Eine try Erklärung kann nur eine allgemeine catch Klausel haben, und wenn eine klausel vorhanden ist, ist sie die letzte catch Klausel.

Hinweis: Einige Programmiersprachen unterstützen möglicherweise Ausnahmen, die nicht als von System.Exception abgeleitetes Objekt dargestellt werden können, obwohl solche Ausnahmen niemals durch C#-Code generiert werden könnten. Eine allgemeine catch Klausel kann verwendet werden, um solche Ausnahmen abzufangen. Daher unterscheidet sich eine allgemeine catch Klausel semantisch von einem, der den Typ System.Exceptionangibt, in dem der frühere auch Ausnahmen von anderen Sprachen abfangen kann. Hinweisende

Um einen Handler für eine Ausnahme zu finden, werden die catch-Klauseln in lexikalischer Reihenfolge untersucht. Wenn eine catch Klausel einen Typ, aber keinen Ausnahmefilter angibt, handelt es sich um einen Kompilierungszeitfehler für eine spätere catch Klausel derselben try Anweisung, um einen Typ anzugeben, der demselben Typ entspricht oder von diesem Typ abgeleitet wird.

Hinweis: Ohne diese Einschränkung wäre es möglich, nicht erreichbare catch Klauseln zu schreiben. Hinweisende

Innerhalb eines catch-Blocks kann eine throw-Anweisung (§13.10.6) ohne Ausdruck verwendet werden, um die Ausnahme, die durch den catch-Block abgefangen wurde, erneut auszulösen. Zuordnungen zu einer Ausnahmevariablen ändern nicht die ausnahme, die erneut ausgelöst wird.

Beispiel: Im folgenden Code

class Test
{
    static void F()
    {
        try
        {
            G();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception in F: " + e.Message);
            e = new Exception("F");
            throw; // re-throw
        }
    }

    static void G() => throw new Exception("G");

    static void Main()
    {
        try
        {
            F();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception in Main: " + e.Message);
        }
    }
}

Die Methode F fängt eine Ausnahme ab, schreibt einige Diagnoseinformationen in die Konsole, ändert die Ausnahmevariable und löst die Ausnahme erneut aus. Die neu ausgelöste Ausnahme ist die ursprüngliche Ausnahme, sodass die Ausgabe, die erzeugt wird, wie folgt lautet:

Exception in F: G
Exception in Main: G

Wenn der erste catch Block e ausgelöst hätte, anstatt die aktuelle Ausnahme erneut auszulösen, würde die erzeugte Ausgabe wie folgt aussehen:

Exception in F: G
Exception in Main: F

Endbeispiel

Es handelt sich um einen Kompilierungszeitfehler, wenn eine break, continue oder goto-Anweisung die Kontrolle aus einem finally-Block überträgt. Wenn eine break Anweisung, continue Anweisung oder goto Anweisung in einem finally Block auftritt, muss sich das Ziel der Anweisung innerhalb desselben finally Blocks befinden, oder andernfalls tritt ein Fehler zur Kompilierungszeit auf.

Es handelt sich um einen Kompilierungszeitfehler für eine return Anweisung, die in einem finally Block auftritt.

Wenn die Ausführung eine try Anweisung erreicht, wird das Steuerelement in den try Block übertragen. Wenn das Steuerelement den Endpunkt des try Blocks erreicht, ohne dass eine Ausnahme weitergegeben wird, wird das Steuerelement an den finally Block übertragen, falls vorhanden. Wenn kein finally Block vorhanden ist, wird das Steuerelement an den Endpunkt der try Anweisung übertragen.

Wenn eine Ausnahme weitergegeben wurde, werden die catch Klauseln (falls vorhanden) in lexikalischer Reihenfolge untersucht, in der die erste Übereinstimmung für die Ausnahme gesucht wird. Die Suche nach einer Abgleichsklausel catch wird mit allen eingeschlossenen Blöcken fortgesetzt, wie in §13.10.6 beschrieben. Eine catch Klausel stimmt überein, wenn der Ausnahmetyp mit irgendeinem Ausnahmespezifizierer übereinstimmt und irgendein Ausnahmefilter wahr ist. Eine catch Klausel ohne einen Ausnahmespezifizierer entspricht jedem Ausnahmetyp. Der Ausnahmetyp entspricht dem exception_specifier , wenn der exception_specifier den Ausnahmetyp oder einen Basistyp des Ausnahmetyps angibt. Wenn die Klausel einen Ausnahmefilter enthält, wird das Ausnahmeobjekt der Ausnahmevariablen zugewiesen, und der Ausnahmefilter wird ausgewertet.

Wenn eine Ausnahme weitergegeben wurde und eine Abgleichsklausel catch gefunden wird, wird das Steuerelement in den ersten übereinstimmenden catch Block übertragen. Wenn das Steuerelement den Endpunkt des catch Blocks erreicht, ohne dass eine Ausnahme weitergegeben wird, wird das Steuerelement an den finally Block übertragen, falls vorhanden. Wenn kein finally Block vorhanden ist, wird das Steuerelement an den Endpunkt der try Anweisung übertragen. Wenn eine Ausnahme aus dem catch Block weitergegeben wurde, wird die Steuerung an den finally Block übertragen, falls vorhanden. Die Ausnahme wird an die nächste einschließende try-Anweisung weitergegeben.

Wenn eine Ausnahme weitergegeben wurde und keine Übereinstimmende catch Klausel gefunden wird, wird die Steuerung an den finally Block übertragen, sofern vorhanden. Die Ausnahme wird an die nächste einschließende try-Anweisung weitergegeben.

Die Anweisungen eines finally-Blocks werden immer ausgeführt, wenn die Steuerung eine try-Anweisung verlässt. Dies gilt unabhängig davon, ob die Kontrollübergabe als Ergebnis einer normalen Ausführung erfolgt, durch die Ausführung eines break-, continue-, goto- oder return-Statements zustande kommt oder durch das Weitergeben einer Ausnahme aus einem try-Statement verursacht wird. Wenn das Steuerelement den Endpunkt des finally Blocks erreicht, ohne dass eine Ausnahme weitergegeben wird, wird das Steuerelement an den Endpunkt der try Anweisung übertragen.

Wenn während der Ausführung eines finally Blocks eine Ausnahme ausgelöst wird und nicht innerhalb desselben finally Blocks abgefangen wird, wird die Ausnahme an die nächste eingeschlossene try Anweisung weitergegeben. Wenn eine andere Ausnahme gerade im Begriff war, weitergeleitet zu werden, geht diese Ausnahme verloren. Der Prozess der Weitergabe einer Ausnahme wird weiter in der Beschreibung der throw Erklärung (§13.10.6) erläutert.

Beispiel: Im folgenden Code

public class Test
{
    static void Main()
    {
        try
        {
            Method();
        }
        catch (Exception ex) when (ExceptionFilter(ex))
        {
            Console.WriteLine("Catch");
        }

        bool ExceptionFilter(Exception ex)
        {
            Console.WriteLine("Filter");
            return true;
        }
    }

    static void Method()
    {
        try
        {
            throw new ArgumentException();
        }
        finally
        {
            Console.WriteLine("Finally");
        }
    }
}

die Methode Method löst eine Ausnahme aus. Die erste Aktion besteht darin, die eingeschlossenen catch Klauseln zu untersuchen und alle Ausnahmefilter auszuführen. ** Anschließend wird die finally-Klausel in Method ausgeführt, bevor die Steuerung an die umschließende passende catch-Klausel übertragen wird. Die resultierende Ausgabe ist:

Filter
Finally
Catch

Endbeispiel

Der try Block einer try Anweisung ist erreichbar, wenn die try Anweisung erreichbar ist.

Ein catch Block einer try Anweisung ist erreichbar, wenn die try Anweisung erreichbar ist.

Der finally Block einer try Anweisung ist erreichbar, wenn die try Anweisung erreichbar ist.

Der Endpunkt einer try Anweisung ist erreichbar, wenn beides zutrifft:

  • Der Endpunkt des try Blocks ist erreichbar oder der Endpunkt mindestens eines catch Blocks ist erreichbar.
  • Wenn ein finally Block vorhanden ist, ist der Endpunkt des finally Blocks erreichbar.

13.12 Die Anweisungen checked und unchecked

Die Anweisungen checked und unchecked werden verwendet, um den Überlaufprüfungskontext für arithmetische Operationen und Konversionen vom Typ Integraltypen zu steuern.

checked_statement
    : 'checked' block
    ;

unchecked_statement
    : 'unchecked' block
    ;

Die checked Anweisung bewirkt, dass alle Ausdrücke im Block in einem überprüften Kontext ausgewertet werden, und die unchecked Anweisung bewirkt, dass alle Ausdrücke im Block in einem deaktivierten Kontext ausgewertet werden.

Die checked und unchecked Anweisungen sind exakt gleichbedeutend mit den checked und unchecked Operatoren (§12.8.20), außer dass sie auf Blöcken statt auf Ausdrücken operieren.

13.13 Die Lock-Anweisung

Die lock Anweisung ruft die Sperre für den gegenseitigen Ausschluss für ein bestimmtes Objekt ab, führt eine Anweisung aus und gibt dann die Sperre frei.

lock_statement
    : 'lock' '(' expression ')' embedded_statement
    ;

Der Ausdruck einer lockAnweisung muss einen Wert eines Typs bezeichnen, von dem bekannt ist, dass er eine Referenz ist. Für den Ausdruck einer Anweisung wird niemals eine implizite Konversion in eine Box durchgeführt (lock). Daher ist es ein Kompilierfehler, wenn der Ausdruck einen Wert eines Werttyps bezeichnet.

Eine lock-Anweisung der Form

lock (x)

wobei x ein Ausdruck eines reference_type ist, ist genau äquivalent zu:

bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(x, ref __lockWasTaken);
    ...
}
finally
{
    if (__lockWasTaken)
    {
        System.Threading.Monitor.Exit(x);
    }
}

außer dass x nur einmal überprüft wird.

Während eine Sperre mit gegenseitigem Ausschluss gehalten wird, kann Code, der im selben Ausführungsthread ausgeführt wird, die Sperre auch abrufen und freigeben. Code, der in anderen Threads ausgeführt wird, wird jedoch daran gehindert, die Sperre abzurufen, bis die Sperre freigegeben wird.

13.14 Die using-Anweisung

Die using Anweisung ruft eine oder mehrere Ressourcen ab, führt eine Anweisung aus und entsorgt dann die Ressource.

using_statement
    : 'using' '(' resource_acquisition ')' embedded_statement
    ;

resource_acquisition
    : local_variable_declaration
    | expression
    ;

Eine Ressource ist eine Klasse oder Struktur, die die System.IDisposable Schnittstelle (IAsyncDisposable für asynchrone Datenströme) implementiert, die eine einzelne parameterlose Methode namens Dispose (DisposeAsync für asynchrone Datenströme) enthält. Code, das eine Ressource verwendet, kann Dispose aufrufen, um anzugeben, dass die Ressource nicht mehr benötigt wird.

Wenn die Form von resource_acquisitionlocal_variable_declaration ist, muss der Typ des local_variable_declaration entweder dynamic oder ein Typ sein, der implizit in (System.IDisposable für asynchrone IAsyncDisposable Datenströme) konvertiert werden kann. Wenn die Form von resource_acquisition ein Ausdruck ist, dann muss dieser Ausdruck implizit in System.IDisposable konvertierbar sein (IAsyncDisposable für asynchrone Datenströme).

Lokale Variablen, die in resource_acquisition deklariert sind, sind schreibgeschützt und müssen einen Initialisierer enthalten. Ein Kompilierungszeitfehler tritt auf, wenn die eingebettete Anweisung versucht, diese lokalen Variablen (über zuordnung oder die ++ Operatoren -- ) zu ändern, die Adresse davon zu übernehmen oder sie als Referenz- oder Ausgabeparameter zu übergeben.

Ein using Statement wird in drei Abschnitte übersetzt: Erwerb, Nutzung und Entsorgung. Die Verwendung der Ressource wird implizit in eine try Anweisung eingeschlossen, die eine finally Klausel enthält. Diese finally Klausel entsorgt die Ressource. Wenn eine null Ressource abgerufen wird, wird kein Aufruf von Dispose (DisposeAsync für asynchrone Datenströme) ausgeführt, und es wird keine Ausnahme ausgelöst. Wenn die Ressource vom Typ dynamic ist, wird sie während des Erwerbs dynamisch über eine implizite dynamische Konvertierung (§10.2.10) in IDisposable (IAsyncDisposable für asynchrone Datenströme) konvertiert, um sicherzustellen, dass die Konvertierung vor der Verwendung und Entsorgung erfolgreich ist.

Eine using-Anweisung der Form

using (ResourceType resource = «expression» ) «statement»

entspricht einer von drei möglichen Erweiterungen. Wenn ResourceType es sich um einen nicht nullablen Werttyp oder einen Typparameter mit der Werttypeinschränkung (§15.2.5) handelt, entspricht die Erweiterung semantisch dem

{
    ResourceType resource = «expression»;
    try
    {
        «statement»;
    }
    finally
    {
        ((IDisposable)resource).Dispose();
    }
}

mit der Ausnahme, dass die Umwandlung von resource in System.IDisposable nicht zu einem Boxing führen darf.

Andernfalls, wenn ResourceTypedynamic ist, lautet die Erweiterung

{
    ResourceType resource = «expression»;
    IDisposable d = resource;
    try
    {
        «statement»;
    }
    finally
    {
        if (d != null)
        {
            d.Dispose();
        }
    }
}

Andernfalls ist die Erweiterung

{
    ResourceType resource = «expression»;
    try
    {
        «statement»;
    }
    finally
    {
        IDisposable d = (IDisposable)resource;
        if (d != null)
        {
            d.Dispose();
        }
    }
}

In jeder Erweiterung ist die resource-Variable in der eingebetteten Anweisung schreibgeschützt und die d-Variable ist in der eingebetteten Anweisung unzugänglich und unsichtbar für sie.

Eine Implementierung darf eine bestimmte using_statement unterschiedlich implementieren, z. B. aus Leistungsgründen, solange das Verhalten mit der oben genannten Erweiterung konsistent ist.

Eine using-Anweisung in der Form:

using («expression») «statement»

hat die gleichen drei möglichen Erweiterungen. In diesem Fall ResourceType handelt es sich implizit um den Kompilierungszeittyp des Ausdrucks, sofern er über einen verfügt. Andernfalls wird die Schnittstelle IDisposable (IAsyncDisposable für asynchrone Datenströme) selbst verwendet als ResourceType. Auf die resource Variable kann nicht zugegriffen werden, und die eingebettete Anweisung ist nicht sichtbar.

Wenn ein resource_acquisition die Form eines local_variable_declaration verwendet, ist es möglich, mehrere Ressourcen eines bestimmten Typs zu erwerben. Eine using-Anweisung der Form

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) «statement»

entspricht genau einer Sequenz von verschachtelten using-Anweisungen:

using (ResourceType r1 = e1)
using (ResourceType r2 = e2)
...
using (ResourceType rN = eN)
«statement»

Beispiel: Im folgenden Beispiel wird eine Datei mit dem Namen log.txt erstellt und zwei Textzeilen in die Datei geschrieben. Anschließend wird die gleiche Datei zum Lesen geöffnet und die darin enthaltenen Textzeilen in die Konsole kopiert.

class Test
{
    static void Main()
    {
        using (TextWriter w = File.CreateText("log.txt"))
        {
            w.WriteLine("This is line one");
            w.WriteLine("This is line two");
        }
        using (TextReader r = File.OpenText("log.txt"))
        {
            string s;
            while ((s = r.ReadLine()) != null)
            {
                Console.WriteLine(s);
            }
        }
    }
}

Da die Klassen TextWriter und TextReader die Schnittstelle IDisposable implementieren, kann das Beispiel using Anweisungen verwenden, um sicherzustellen, dass die zugrunde liegende Datei nach den Schreib- oder Leseoperationen ordnungsgemäß abgeschlossen wird.

Endbeispiel

13.15 Die Renditeerklärung

Die yield Anweisung wird in einem Iteratorblock (§13.3) verwendet, um dem Enumerationsobjekt (§15.15.5) oder dem aufzählbaren Objekt (§15.15.6) eines Iterators einen Wert zu liefern oder das Ende der Iteration zu signalisieren.

yield_statement
    : 'yield' 'return' expression ';'
    | 'yield' 'break' ';'
    ;

yield ist ein kontextbezogenes Schlüsselwort (§6.4.4) und hat nur dann besondere Bedeutung, wenn es unmittelbar vor einem return oder break Schlüsselwort verwendet wird.

Es gibt mehrere Einschränkungen, wo eine yield-Anweisung erscheinen kann, wie im Folgenden beschrieben.

  • Es ist ein Kompilierzeitfehler, wenn eine yield Anweisung (in einer der beiden Formen) außerhalb eines method_body, operator_body oder accessor_body vorkommt.
  • Es ist ein Kompilierfehler, wenn eine yield-Anweisung (in irgendeiner Form) innerhalb einer anonymen Funktion erscheint.
  • Es ist ein Kompilierfehler, wenn eine yield-Anweisung (in beliebiger Form) in der finally-Klausel einer try-Anweisung erscheint.
  • Es ist ein Kompilierzeitfehler, wenn eine yield return Anweisung an einer beliebigen Stelle in einer try Anweisung erscheint, die irgendwelche catch_clauses enthält.

Beispiel: Das folgende Beispiel zeigt einige gültige und ungültige Verwendungen von yield Anweisungen.

delegate IEnumerable<int> D();

IEnumerator<int> GetEnumerator()
{
    try
    {
        yield return 1; // Ok
        yield break;    // Ok
    }
    finally
    {
        yield return 2; // Error, yield in finally
        yield break;    // Error, yield in finally
    }
    try
    {
        yield return 3; // Error, yield return in try/catch
        yield break;    // Ok
    }
    catch
    {
        yield return 4; // Error, yield return in try/catch
        yield break;    // Ok
    }
    D d = delegate
    {
        yield return 5; // Error, yield in an anonymous function
    };
}

int MyMethod()
{
    yield return 1;     // Error, wrong return type for an iterator block
}

Endbeispiel

Eine implizite Konvertierung (§10.2) muss vom Typ des Ausdrucks in der yield return Anweisung in den Ertragtyp (§15.15.4) des Iterators bestehen.

Eine yield return Anweisung wird wie folgt ausgeführt:

  • Der in der Anweisung angegebene Ausdruck wird ausgewertet, implizit in den Ertragtyp konvertiert und der Current Eigenschaft des Enumeratorobjekts zugewiesen.
  • Die Ausführung des Iterator-Blocks wird unterbrochen. Wenn sich die yield return Anweisung in einem oder try mehreren Blöcken befindet, werden die zugehörigen finally Blöcke zurzeit nicht ausgeführt.
  • Die Methode MoveNext des Aufzählungsobjekts gibt true an den Aufrufer zurück und zeigt an, dass das Aufzählungsobjekt erfolgreich zum nächsten Element weitergeschaltet wurde.

Der nächste Aufruf der Enumerator-Objektmethode MoveNext setzt die Ausführung des Iteratorblocks fort, von wo aus es zuletzt angehalten wurde.

Eine yield break Anweisung wird wie folgt ausgeführt:

  • Wenn die yield break Anweisung von einem oder try mehreren Blöcken mit zugeordneten finally Blöcken eingeschlossen wird, wird das Steuerelement zunächst in den finally Block der innersten try Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird so lange wiederholt, bis die finally Blockierungen aller einschließenden try Anweisungen ausgeführt wurden.
  • Die Kontrolle wird an den Aufrufer des Iteratorblocks zurückgegeben. Dies ist entweder die MoveNext Methode oder Dispose Methode des Enumeratorobjekts.

Da eine yield break Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer yield break Aussage nie erreichbar.