Rekursiver Musterabgleich
Anmerkung
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Zusammenfassung
Musterabgleichserweiterungen für C# ermöglichen viele der Vorteile von algebraischen Datentypen und Musterabgleich aus funktionalen Sprachen, jedoch auf eine Weise, die sich nahtlos in den Charakter der zugrunde liegenden Sprache einfügt. Elemente dieses Ansatzes sind von verwandten Funktionen in den Programmiersprachen F# und Scala inspiriert.
Detailliertes Design
Is-Ausdruck
Der is
-Operator wird erweitert, um einen Ausdruck für ein Muster zu testen.
relational_expression
: is_pattern_expression
;
is_pattern_expression
: relational_expression 'is' pattern
;
Diese Form von relational_expression ergänzt die bestehenden Formen in der C#-Spezifikation. Es handelt sich um einen Kompilierungsfehler, wenn die relational_expression links neben dem is
-Token keinen Wert angibt oder keinen Typ aufweist.
Jeder Bezeichner des Musters führt eine neue lokale Variable ein, die definitiv zugewiesen wird, nachdem der is
-Operator true
ist (d. h. definitiv zugewiesen, wenn wahr).
Hinweis: Es gibt technisch gesehen eine Mehrdeutigkeit zwischen type in einem
is-expression
-Element und constant_pattern, die beide eine gültige Analyse eines qualifizierten Bezeichners sein könnten. Wir versuchen, es als Typ zu binden, um die Kompatibilität mit früheren Versionen der Sprache zu gewährleisten; nur wenn dies fehlschlägt, lösen wir es wie einen Ausdruck in anderen Kontexten auf, und zwar auf das erste gefundene Element (das entweder eine Konstante oder ein Typ sein muss). Diese Mehrdeutigkeit besteht nur auf der rechten Seite einesis
-Ausdrucks.
Muster
Muster werden im is_pattern-Operator, in switch_statement und in switch_expression verwendet, um die Form von Daten auszudrücken, mit denen eingehende Daten (die wir als Eingabewert bezeichnen) verglichen werden sollen. Muster können rekursiv sein, sodass Teile der Daten mit Untermustern abgeglichen werden können.
pattern
: declaration_pattern
| constant_pattern
| var_pattern
| positional_pattern
| property_pattern
| discard_pattern
;
declaration_pattern
: type simple_designation
;
constant_pattern
: constant_expression
;
var_pattern
: 'var' designation
;
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;
property_pattern
: type? property_subpattern simple_designation?
;
simple_designation
: single_variable_designation
| discard_designation
;
discard_pattern
: '_'
;
Deklarationsmuster
declaration_pattern
: type simple_designation
;
declaration_pattern testet, ob ein Ausdruck von einem bestimmten Typ ist, und wandelt ihn bei erfolgreichem Test in diesen Typ um. Dies kann eine lokale Variable des angegebenen Typs einführen, die durch den angegebenen Bezeichner benannt wird, wenn die Bezeichnung single_variable_designation ist. Diese lokale Variable ist definitiv zugewiesen, wenn das Ergebnis des Mustervergleichsvorgangs true
ist.
Die Laufzeitsemantik dieses Ausdrucks besteht darin, dass er den Laufzeittyp des linken Operanden relational_expression mit dem Typ im Muster vergleicht. Wenn er diesem Laufzeittyp (oder einem Untertyp) entspricht und nicht null
ist, ist das Ergebnis des is operator
true
.
Bestimmte Kombinationen des statischen Typs der linken Seite und des angegebenen Typs gelten als inkompatibel und führen zu einem Kompilierzeitfehler. Ein Wert vom statischen Typ E
gilt als musterkompatibel mit einem Typ T
, wenn eine Identitätskonvertierung, eine implizite Verweiskonvertierung, eine Schachtelungskonvertierung, eine explizite Verweiskonvertierung oder eine Entschachtelungskonvertierung von E
in T
vorhanden ist oder wenn einer dieser Typen ein offener Typ ist. Es handelt sich um einen Kompilierzeitfehler, wenn eine Eingabe vom Typ E
nicht musterkompatibel mit dem Typ in einem Typmuster ist, mit dem sie abgeglichen wird.
Das Typmuster eignet sich zum Ausführen von Laufzeittyptests von Referenztypen und ersetzt das Idiom
var v = expr as Type;
if (v != null) { // code using v
durch das etwas prägnantere
if (expr is Type v) { // code using v
Es handelt sich um einen Fehler, wenn type ein nullbarer Werttyp ist.
Das Typmuster kann verwendet werden, um Werte von nullbaren Typen zu testen: Ein Wert vom Typ Nullable<T>
(oder ein geschachteltes T
) entspricht dem Typmuster T2 id
, wenn der Wert nicht null ist und der Typ von T2
T
oder ein Basistyp oder eine Schnittstelle von T
ist. Zum Beispiel im Codefragment
int? x = 3;
if (x is int v) { // code using v
Die Bedingung der if
-Anweisung ist zur Laufzeit true
, und die Variable v
enthält den Wert 3
vom Typ int
innerhalb des Blocks. Nach dem Block befindet sich die Variable v
im Bereich, ist aber nicht definitiv zugewiesen.
Gleichbleibendes Muster
constant_pattern
: constant_expression
;
Ein Konstantenmuster testet den Wert eines Ausdrucks anhand eines konstanten Werts. Die Konstante kann ein beliebiger Konstantenausdruck sein, z. B. ein Literal, der Name einer deklarierten const
-Variablen oder eine Enumerationskonstante. Wenn der Eingabewert kein offener Typ ist, wird der konstante Ausdruck implizit in den Typ des übereinstimmenden Ausdrucks umgewandelt. Wenn der Typ des Eingabewerts nicht musterkompatibel mit dem Typ des konstanten Ausdrucks ist, endet der Musterabgleich mit einem Fehler.
Das Muster c wird als übereinstimmend mit dem konvertierten Eingabewert e betrachtet, wenn object.Equals(c, e)
true
zurückgeben würde.
Wir erwarten, dass e is null
die häufigste Art ist, um in neu geschriebenem Code auf null
zu testen, da es keinen benutzerdefinierten operator==
aufrufen kann.
var-Muster
var_pattern
: 'var' designation
;
designation
: simple_designation
| tuple_designation
;
simple_designation
: single_variable_designation
| discard_designation
;
single_variable_designation
: identifier
;
discard_designation
: _
;
tuple_designation
: '(' designations? ')'
;
designations
: designation
| designations ',' designation
;
Wenn das designation-Element simple_designation ist, entspricht Ausdruck e dem Muster. Mit anderen Worten: Ein Abgleich mit einem var-Muster ist immer mit simple_designation erfolgreich. Wenn das simple_designation-Element single_variable_designation ist, wird der Wert von e an eine neu eingeführte lokale Variable gebunden. Der Typ der lokalen Variablen ist der statische Typ von e.
Wenn das designation-Element tuple_designation ist, entspricht das Muster einem positional_pattern-Element der Form (var
designation, … )
, wobei sich designation-Elemente in tuple_designation befinden. Das Muster var (x, (y, z))
entspricht beispielsweise (var x, (var y, var z))
.
Es ist ein Fehler, wenn der Name var
an einen Typ gebunden wird.
Ausschussmuster
discard_pattern
: '_'
;
Ein Ausdruck e entspricht immer dem Muster _
. Anders ausgedrückt entspricht jeder Ausdruck dem Ausschussmuster.
Ein Ausschussmuster darf nicht als Muster von is_pattern_expression verwendet werden.
Positionsmuster
Ein Positionsmuster überprüft, ob der Eingabewert nicht null
ist, ruft eine geeignete Deconstruct
Methode auf und führt einen weiteren Musterabgleich für die resultierenden Werte durch. Es unterstützt auch eine tupelähnliche Mustersyntax (ohne Angabe des Typs), wenn der Typ des Eingabewerts mit dem Typ übereinstimmt, der Deconstruct
enthält, oder wenn der Typ des Eingabewerts ein Tupeltyp ist, oder wenn der Typ des Eingabewerts object
oder ITuple
ist und der Laufzeittyp des Ausdrucks ITuple
implementiert.
positional_pattern
: type? '(' subpatterns? ')' property_subpattern? simple_designation?
;
subpatterns
: subpattern
| subpattern ',' subpatterns
;
subpattern
: pattern
| identifier ':' pattern
;
Wenn der Typ weggelassen wird, nehmen wir an, dass es der statische Typ des Eingabewerts ist.
Bei einer Übereinstimmung eines Eingabewerts mit type(
subpattern_list)
des Musters wird eine Methode ausgewählt, indem in type nach zugänglichen Deklarationen von Deconstruct
gesucht wird und eine davon nach denselben Regeln wie bei der Dekonstruktionsdeklaration ausgewählt wird.
Es ist ein Fehler, wenn positional_pattern den Typ auslässt, ein einzelnes subpattern-Element ohne Bezeichner, kein property_subpattern-Element und kein simple_designation-Element hat. Dies unterscheidet zwischen constant_pattern in Klammern und positional_pattern.
Um die Werte zu extrahieren, die zu den Mustern in der Liste passen,
- Wenn type weggelassen wurde und der Typ des Eingabewerts ein Tupeltyp ist, muss die Anzahl der Teilmuster mit der Kardinalität des Tupels übereinstimmen. Jedes Tupelelement wird mit dem entsprechenden Teilmuster abgeglichen, und der Abgleich ist erfolgreich, wenn alle erfolgreich sind. Wenn ein Teilmuster einen Bezeichner hat, muss dieser ein Tupelelement an der entsprechenden Position im Tupeltyp benennen.
- Andernfalls, wenn ein geeignetes
Deconstruct
-Element als Member von type vorhanden ist, liegt ein Kompilierzeitfehler vor, wenn der Typ des Eingabewerts nicht musterkompatibel mit type ist. Zur Laufzeit wird der Eingabewert für type getestet. Wenn dies fehlschlägt, schlägt der Positionsmustervergleich fehl. Wenn dies erfolgreich ist, wird der Eingabewert in diesen Typ konvertiert, undDeconstruct
wird mit neuen, vom Compiler generierten Variablen aufgerufen, um dieout
-Parameter zu erhalten. Jeder empfangene Wert wird mit dem entsprechenden Teilmuster abgeglichen, und der Abgleich ist erfolgreich, wenn alle diese erfolgreich sind. Wenn ein Teilmuster einen Bezeichner hat, muss dieser einen Parameter an der entsprechenden Position vonDeconstruct
benennen. - Andernfalls, wenn type weggelassen wurde und der Eingabewert vom Typ
object
oderITuple
oder einem Typ ist, der durch eine implizite Verweiskonvertierung inITuple
konvertiert werden kann, und kein Bezeichner unter den Teilmustern erscheint, führen wir einen Abgleich mitITuple
durch. - Andernfalls ist das Muster ein Kompilierungszeitfehler.
Die Reihenfolge, in der Teilpattern während der Laufzeit abgeglichen werden, ist nicht spezifiziert, und bei einem fehlgeschlagenen Abgleich wird möglicherweise nicht versucht, alle Teilmuster abzugleichen.
Beispiel
In diesem Beispiel werden viele der in dieser Spezifikation beschriebenen Features verwendet.
var newState = (GetState(), action, hasKey) switch {
(DoorState.Closed, Action.Open, _) => DoorState.Opened,
(DoorState.Opened, Action.Close, _) => DoorState.Closed,
(DoorState.Closed, Action.Lock, true) => DoorState.Locked,
(DoorState.Locked, Action.Unlock, true) => DoorState.Closed,
(var state, _, _) => state };
Mustereigenschaft
Ein Eigenschaftenmuster überprüft, ob der Eingabewert nicht null
ist und rekursiv werte abgleicht, die durch die Verwendung von barrierefreien Eigenschaften oder Feldern extrahiert wurden.
property_pattern
: type? property_subpattern simple_designation?
;
property_subpattern
: '{' '}'
| '{' subpatterns ','? '}'
;
Es liegt ein Fehler vor, wenn ein Teilmuster eines property_pattern-Element keinen Bezeichner enthält (es muss die zweite Form mit einem Bezeichner aufweisen). Ein nachfolgendes Komma nach dem letzten Unterpattern ist optional.
Beachten Sie, dass ein Muster zur Überprüfung auf Null aus einem trivialen Eigenschaftsmuster herausfällt. Um zu überprüfen, ob die Zeichenfolge s
ungleich Null ist, können Sie eine der folgenden Formulare schreiben:
if (s is object o) ... // o is of type object
if (s is string x) ... // x is of type string
if (s is {} x) ... // x is of type string
if (s is {}) ...
Bei einer Übereinstimmung von Ausdrucks e mit type{
property_pattern_list}
des Musters liegt ein Kompilierzeitfehler vor, wenn Ausdruck e nicht mit dem durch type angegebenen Typ T musterkompatibel ist. Wenn der Typ nicht vorhanden ist, betrachten wir ihn als den statischen Typ von und. Wenn der Bezeichner vorhanden ist, deklariert er eine Mustervariable vom Typ type. Jeder der Bezeichner, die auf der linken Seite von property_pattern_list erscheinen, muss eine zugängliche lesbare Eigenschaft oder ein Feld von T bezeichnen. Wenn das simple_designation-Element von property_pattern vorhanden, definiert es eine Mustervariable vom Typ T.
Zur Laufzeit wird der Ausdruck für T getestet. Wenn dies fehlschlägt, schlägt der Eigenschaftsmusterabgleich fehl, und das Ergebnis ist false
. Bei einem erfolgreichen Test wird jedes property_subpattern-Feld bzw. die Eigenschaft gelesen und der Wert mit dem entsprechenden Muster abgeglichen. Das Ergebnis des gesamten Abgleichs ist nur dann false
, wenn das Ergebnis eines dieser Abgleiche false
ist. Die Reihenfolge, in der Teilmuster abgeglichen werden, ist nicht festgelegt, und bei einem fehlgeschlagenen Abgleich müssen zur Laufzeit nicht unbedingt alle Teilmuster übereinstimmen. Wenn der Abgleich erfolgreich ist und simple_designation von property_pattern ein single_variable_designation-Element ist, wird eine Variable vom Typ T definiert, welcher der übereinstimmende Wert zugewiesen wird.
Hinweis: Das Eigenschaftsmuster kann zum Musterabgleich mit anonymen Typen verwendet werden.
Beispiel
if (o is string { Length: 5 } s)
switch-Ausdruck
switch_expression wird hinzugefügt, um switch
-ähnliche Semantik für einen Ausdruckskontext zu unterstützen.
Die Syntax der Sprache C# wird durch die folgenden syntaktischen Produktionen erweitert:
multiplicative_expression
: switch_expression
| multiplicative_expression '*' switch_expression
| multiplicative_expression '/' switch_expression
| multiplicative_expression '%' switch_expression
;
switch_expression
: range_expression 'switch' '{' '}'
| range_expression 'switch' '{' switch_expression_arms ','? '}'
;
switch_expression_arms
: switch_expression_arm
| switch_expression_arms ',' switch_expression_arm
;
switch_expression_arm
: pattern case_guard? '=>' expression
;
case_guard
: 'when' null_coalescing_expression
;
switch_expression als expression_statement ist nicht zulässig.
Wir überlegen, dies in einer zukünftigen Überarbeitung zu lockern.
Der Typ von switch_expression ist der beste gemeinsame Typ (§12.6.3.15) der Ausdrücke, die rechts von den =>
-Token von switch_expression_arm-Elementen erscheinen, wenn ein solcher Typ existiert und der Ausdruck in jedem Zweig des switch-Ausdrucks implizit in diesen Typ konvertiert werden kann. Zusätzlich fügen wir eine neue Konvertierung von switch-Ausdrücken hinzu, bei der es sich um eine vordefinierte implizite Konvertierung von einem switch-Ausdruck in jeden T
-Typ handelt, für den eine implizite Konvertierung von jedem Zweig des Ausdrucks in T
vorhanden ist.
Es ist ein Fehler, wenn das Muster eines switch_expression_arm-Elements das Ergebnis nicht beeinflussen kann, da ein vorheriges Muster und ein Wächter immer übereinstimmen.
Ein switch-Ausdruck gilt als vollständig, wenn ein Zweig des switch-Ausdrucks jeden Wert seiner Eingabe verarbeitet. Der Compiler muss eine Warnung ausgeben, wenn ein switch-Ausdruck nicht vollständig ist.
Zur Laufzeit ist das Ergebnis von switch_expression der Wert des Ausdrucks des ersten switch_expression_arm-Elements, für den der Ausdruck auf der linken Seite von switch_expression mit dem Muster von switch_expression_arm übereinstimmt und für den case_guard von switch_expression_arm, falls vorhanden, true
ergibt. Wenn keine solches switch_expression_arm-Element vorhanden ist, löst switch_expression eine Instanz der Ausnahme System.Runtime.CompilerServices.SwitchExpressionException
aus.
Optionale Klammern beim Aktivieren eines Tupelliterals
Um mithilfe von switch_statement ein Tupelliteral zu aktivieren, müssen Sie scheinbar redundante Klammern schreiben.
switch ((a, b))
{
um Folgendes zuzulassen
switch (a, b)
{
Die Klammern der switch-Anweisung sind optional, wenn der zu aktivierende Ausdruck ein Tupelliteral ist.
Reihenfolge der Auswertung im Musterabgleich
Dank der Flexibilität des Compilers beim Neuanordnen der vorgänge, die während des Musterabgleichs ausgeführt werden, kann die Flexibilität ermöglicht werden, die zur Verbesserung der Effizienz des Musterabgleichs verwendet werden kann. Die (nicht erzwungene) Anforderung wäre, dass Eigenschaften, auf die in einem Muster zugegriffen wird, und die Dekonstruktionsmethoden „rein“ (ohne Nebeneffekte, idempotent usw.) sein müssen. Das bedeutet nicht, dass wir Reinheit als Sprachkonzept hinzufügen würden, nur dass wir die Compilerflexibilität beim Neuanordnen von Vorgängen zulassen würden.
Lösung 2018-04-04 LDM: bestätigt: Der Compiler darf Aufrufe von Deconstruct
, Eigenschaftszugriffe und Aufrufe von Methoden in ITuple
neu anordnen und kann davon ausgehen, dass die zurückgegebenen Werte bei mehreren Aufrufen gleich sind. Der Compiler sollte keine Funktionen aufrufen, die sich nicht auf das Ergebnis auswirken können, und wir werden sehr vorsichtig sein, bevor wir zukünftig Änderungen an der vom Compiler generierten Auswertungsreihenfolge vornehmen.
Einige mögliche Optimierungen
Bei der Zusammenstellung von Musterabgleichen können gemeinsame Teile von Mustern genutzt werden. Wenn beispielsweise der Typentest der obersten Ebene von zwei aufeinanderfolgenden Mustern in switch_statement vom gleichen Typ ist, kann der generierte Code den Typentest für das zweite Muster überspringen.
Wenn es sich bei einigen Mustern um ganze Zahlen oder Zeichenfolgen handelt, kann der Compiler dieselbe Art von Code generieren, den er für eine Switch-Anweisung in früheren Versionen der Sprache generiert.
Weitere Informationen zu diesen Arten von Optimierungen finden Sie unter [Scott und Ramsey (2000)].
C# feature specifications