Rekursiver Musterabgleich
Hinweis
Dieser Artikel ist eine Feature-Spezifikation. 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 Feature-Spezifikation 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.
Champion Issue: https://github.com/dotnet/csharplang/issues/45
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 ist ein Kompilierfehler, wenn der relationale_Ausdruck links vom is
Token keinen Wert bezeichnet oder keinen Typ hat.
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, so dass Elemente der Daten mit Teilmustern 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 gilt als passend für den konvertierten Eingabewert e, wenn object.Equals(c, e)
true
ergeben 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. Zum Beispiel ist das Muster var (x, (y, z))
äquivalent zu (var x, (var y, var z))
.
Es ist ein Fehler, wenn der Name var
an einen Typ gebunden ist.
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.
Positionelles Muster
Ein Positionsmuster prüft, ob der Eingabewert nicht null
ist, ruft eine entsprechende Deconstruct
-Methode auf und führt einen weiteren Mustervergleich mit den resultierenden Werten 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 Kompilierfehler.
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 Funktionen 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 Eigenschaftsmuster prüft, ob der Eingabewert nicht null
ist und gleicht rekursiv Werte ab, die durch die Verwendung von zugänglichen 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 nachgestelltes Komma nach dem letzten Teilmuster ist optional.
Beachten Sie, dass ein Muster zur Überprüfung auf Null aus einem trivialen Eigenschaftsmuster herausfällt. Um zu prüfen, ob die Zeichenfolge s
nicht-null ist, können Sie eine der folgenden Formen 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
Wenn Sie dem Compiler bei der Neuordnung der Operationen, die während des Pattern-Matching ausgeführt werden, Flexibilität einräumen, können Sie die Effizienz des Pattern-Matching verbessern. 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, sondern nur, dass wir dem Compiler die Möglichkeit bieten, die Operationen flexibel umzuordnen.
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 das Ergebnis nicht beeinflussen können, und wir werden sehr vorsichtig sein, bevor wir in Zukunft Änderungen an der vom Compiler generierten Reihenfolge der Auswertung 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 einige der Muster Ganzzahlen oder Zeichenfolgen sind, kann der Compiler die gleiche Art von Code generieren, die er in früheren Versionen der Sprache für eine switch-Anweisung generiert hat.
Mehr über diese Art von Optimierungen finden Sie in [Scott und Ramsey (2000)].
C# feature specifications