Musterabgleichänderungen für C# 9.0
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.
Wir ziehen eine kleine Handvoll Verbesserungen an Musterabgleich für C# 9.0 in Betracht, die natürliche Synergie aufweisen und gut funktionieren, um eine Reihe häufiger Programmierprobleme zu beheben:
- https://github.com/dotnet/csharplang/issues/2925 Typmuster
- https://github.com/dotnet/csharplang/issues/1350 Klammernmuster, um die Vorrangstellung der neuen Kombinatoren zu erzwingen oder hervorzuheben.
- https://github.com/dotnet/csharplang/issues/1350 Konjunktive
and
Muster, die mit zwei unterschiedlichen Mustern übereinstimmen müssen; - https://github.com/dotnet/csharplang/issues/1350 Disjunktive
or
Muster, die mit einem von zwei unterschiedlichen Mustern übereinstimmen müssen; - https://github.com/dotnet/csharplang/issues/1350 Negierte
not
Mustern, die ein bestimmtes Muster erfordern, nicht übereinstimmen; und - https://github.com/dotnet/csharplang/issues/812 Relationale Muster, bei denen der Eingabewert kleiner als, kleiner oder gleich usw. einer bestimmten Konstante sein muss.
Muster in Klammern
Geklammerte Muster ermöglichen es dem Programmierer, Klammern um ein Muster zu setzen. Dies ist bei den vorhandenen Mustern in C# 8.0 nicht so nützlich, aber die neuen Musterkombinatoren stellen eine Rangfolge vor, die der Programmierer möglicherweise außer Kraft setzen möchte.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Typmuster
Wir erlauben einen Typ als Muster:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Dies ändert den vorhandenen is-type-expression in ein is-pattern-expression, wobei das Muster ein Typmuster ist, auch wenn wir die vom Compiler erstellte Syntaxstruktur nicht ändern würden.
Ein subtiles Implementierungsproblem besteht darin, dass diese Grammatik mehrdeutig ist. Eine Zeichenfolge wie a.b
kann entweder als qualifizierter Name (in einem Typkontext) oder als gepunkteter Ausdruck (in einem Ausdruckskontext) analysiert werden. Der Compiler ist bereits in der Lage, einen qualifizierten Namen genauso zu behandeln wie einen Punktausdruck, um etwas wie e is Color.Red
zu verarbeiten. Die semantische Analyse des Compilers würde weiterentwickelt werden, um ein (syntaktisches) Konstantenmuster (z. B. einen gepunkteten Ausdruck) als Typ zu binden, damit es als gebundenes Typmuster erkannt wird, um dieses Konstrukt zu unterstützen.
Nach dieser Änderung wären Sie in der Lage zu schreiben.
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
Relationale Muster
Relationale Muster ermöglichen es dem Programmierer, auszudrücken, dass ein Eingabewert im Vergleich zu einem konstanten Wert eine relationale Einschränkung erfüllen muss:
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
Relationale Muster unterstützen die relationalen Operatoren <
, <=
, >
und >=
für alle integrierten Typen, die solche binären relationalen Operatoren mit zwei Operanden desselben Typs in einem Ausdruck unterstützen. Insbesondere unterstützen wir alle diese relationalen Muster für sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
und nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
Der Ausdruck ist erforderlich, um einen konstanten Wert auszuwerten. Es ist ein Fehler, wenn dieser Konstantenwert double.NaN
oder float.NaN
ist. Es handelt sich um einen Fehler, wenn der Ausdruck eine NULL-Konstante ist.
Wenn es sich bei der Eingabe um einen Typ handelt, für den ein geeigneter eingebauter binärer relationaler Operator definiert ist, der mit der Eingabe als linker Operand und der angegebenen Konstante als rechter Operand anwendbar ist, wird die Auswertung dieses Operators als Bedeutung des relationalen Musters verwendet. Andernfalls wandeln wir mithilfe einer expliziten Nullwert- oder Unboxing-Konvertierung die Eingabe in den Typ des Ausdrucks um. Es handelt sich um einen Kompilierungszeitfehler, wenn keine solche Konvertierung vorhanden ist. Das Muster wird als nicht übereinstimmend betrachtet, wenn die Konvertierung fehlschlägt. Wenn die Konvertierung erfolgreich ist, ist das Ergebnis des Musterabgleichsvorgangs das Ergebnis der Auswertung des Ausdrucks e OP v
wobei e
die konvertierte Eingabe ist, OP
der relationale Operator ist und v
der konstante Ausdruck ist.
Musterkombinatoren
Muster-Kombinatoren die Übereinstimmung beider unterschiedlicher Muster mithilfe von and
ermöglichen (dies kann auf eine beliebige Anzahl von Mustern durch die wiederholte Verwendung von and
), entweder von zwei verschiedenen Mustern mit or
(ditto) oder die Negation eines Musters mit not
erweitert werden.
Eine häufige Anwendung von Kombinatoren ist das Idiom.
if (e is not null) ...
Besser lesbar als die aktuelle Schreibweise e is object
, drückt dieses Muster deutlich aus, dass auf einen nicht-null Wert überprüft wird.
Die and
und or
Kombinatoren sind nützlich, um Wertebereiche zu testen.
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
In diesem Beispiel wird veranschaulicht, dass and
eine höhere Analysepriorität (d. h. eine engere Bindung) als or
haben. Der Programmierer kann das Klammermuster verwenden, um die Rangfolge explizit zu gestalten:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Wie bei allen Mustern können diese Kombinationszeichen in jedem Kontext verwendet werden, in dem ein Muster erwartet wird, einschließlich geschachtelter Muster, der , der Schalterausdruck und das Muster der Fallbeschriftung einer Switch-Anweisung.
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
Wechseln zu 6.2.5 Grammatikambiguitäten
Aufgrund der Einführung des Typmusters ist es möglich, dass ein generischer Typ vor dem Token =>
angezeigt wird. Daher fügen wir =>
der Gruppe von Token hinzu, die in §6.2.5 grammatikalischen Mehrdeutigkeiten aufgeführt sind, um die Mehrdeutigkeit von <
aufzulösen, die mit der Typargumentliste beginnt. Siehe auch https://github.com/dotnet/roslyn/issues/47614.
Offene Probleme mit vorgeschlagenen Änderungen
Syntax für relationale Operatoren
Sind and
, or
und not
eine Art kontextbezogenes Schlüsselwort? Wenn ja, gibt es eine einschneidende Änderung (z. B. im Vergleich zur Verwendung als Kennzeichner in einem Deklarationsmuster).
Semantik (z. B. Typ) für relationale Operatoren
Wir erwarten, dass alle primitiven Typen unterstützt werden, die in einem Ausdruck mithilfe eines relationalen Operators verglichen werden können. Die Bedeutung in einfachen Fällen ist klar
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Aber wenn die Eingabe kein primitiver Typ ist, in welchen Typ versuchen wir, sie umzuwandeln?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Wir haben vorgeschlagen, dass, wenn der Eingabetyp bereits ein vergleichbarer Grundtyp ist, dies der Typ des Vergleichs ist. Wenn die Eingabe jedoch kein vergleichbarer Grundtyp ist, behandeln wir die Relationale als impliziten Typtest für den Typ der Konstante auf der rechten Seite des relationalen Elements. Wenn der Programmierer mehr als einen Eingabetyp unterstützt, muss dies explizit erfolgen:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Ergebnis: Die relationale Funktion enthält einen impliziten Typtest für den Typ der Konstante auf der rechten Seite des relationalen Elements.
Fließende Typinformationen von der linken zu der rechten Seite von and
Es wurde vorgeschlagen, dass beim Schreiben eines and
-Kombinators die Informationen, die links über den übergeordneten Typ erfasst wurden, nach rechts weitergegeben werden können. Beispiel:
bool isSmallByte(object o) => o is byte and < 100;
Hier wird der Eingabetyp, der auf das zweite Muster, durch den Typ eingeschränkt, Anforderungen von links vom and
. Wir würden die Typentgrenzungsemantik für alle Muster wie folgt definieren. Der eingeschränkte Typ eines Musters P
wird wie folgt definiert:
- Wenn
P
ein Typmuster ist, ist der eingeschränkte Typ der Typ des Typmusters. - Wenn
P
ein Deklarationsmuster ist, ist der eingeschränkte Typ der Typ des Deklarationsmusters. - Wenn
P
ein rekursives Muster ist, das einen expliziten Typ angibt, ist der schmale Typ dieser Typ. - Wenn
P
anhand der Regeln fürITuple
abgeglichen wird, ist der eingeschränkte Typ der TypSystem.Runtime.CompilerServices.ITuple
. - Wenn
P
ein Konstantenmuster ist, bei dem es sich bei der Konstante nicht um die Nullkonstante handelt und der Ausdruck keine Konvertierung von Konstantenausdrücken in den Eingabetyp hat, ist der eingeschränkte Typ der Typ der Konstante. - Wenn
P
ein relationales Muster ist, bei dem der Konstantenausdruck keine Konvertierung von Konstantenausdrücken in den Eingabetyp hat, ist der eingeschränkte Typ der Typ der Konstante. - Wenn
P
einor
-Muster ist, ist der eingeschränkte Typ der gemeinsame Typ des eingeschränkten Typs des Untermusters, wenn ein solcher gemeinsamer Typ vorhanden ist. Zu diesem Zweck berücksichtigt der allgemeine Typalgorithmus nur Identitäts-, Box- und implizite Verweiskonvertierungen und berücksichtigt alle Unterpattern einer Sequenz vonor
-Mustern (ignoriert Klammermuster). - Wenn
P
einand
-Muster ist, dann ist der eingeschränkte Typ der eingeschränkte Typ des rechten Musters. Darüber hinaus ist der eingeschränkte Typ des linken Musters der Eingabetyp des rechten Musters. - Ansonsten ist der eingeschränkte Typ von
P
der Eingabetyp vonP
.
Ergebnis: Die oben genannte Eingrenzungssemantik wurde implementiert.
Variablendefinitionen und definitive Zuordnung
Durch das Hinzufügen von or
und not
Mustern entstehen einige interessante neue Probleme bei Mustervariablen und eindeutiger Zuordnung. Da Variablen normalerweise höchstens einmal deklariert werden können, scheint jede Mustervariable, die auf einer Seite eines or
-Musters deklariert wird, nicht definitiv zugewiesen zu werden, wenn das Muster übereinstimmt. Ebenso würde eine Variable, die innerhalb eines not
-Musters deklariert wurde, nicht erwartet werden, dass sie definitiv zugewiesen wird, wenn das Muster übereinstimmt. Die einfachste Möglichkeit, dies zu beheben, besteht darin, das Deklarieren von Mustervariablen in diesen Kontexten zu verbieten. Dies kann jedoch zu restriktiv sein. Es gibt andere Ansätze zu berücksichtigen.
Ein Szenario, das es wert ist, in Betracht gezogen zu werden, ist folgendes
if (e is not int i) return;
M(i); // is i definitely assigned here?
Dies funktioniert heute nicht, da die Mustervariablen für einen is-pattern-expression definitiv nur dann zugewiesen werden, wenn die is-pattern-expression „true” ist (definitiv zugewiesen wenn wahr).
Dies wäre einfacher aus der Sicht des Programmierers, als auch die Unterstützung für eine Negation der Bedingung in der if
-Anweisung hinzuzufügen. Auch wenn wir solche Unterstützung hinzufügen, fragen sich Programmierer, warum der obige Codeausschnitt nicht funktioniert. Andererseits ist das gleiche Szenario in einem switch
weniger sinnvoll, da es keinen entsprechenden Punkt im Programm gibt, an dem definitiv zugewiesen wird, wenn ein falsches sinnvoll wäre. Würden wir dies in einem Musterausdruck zulassen, aber nicht in anderen Kontexten, in denen Muster zulässig sind? Das scheint unregelmäßig zu sein.
Im Zusammenhang damit ist das Problem der endgültigen Zuordnung in einem disjunktives Muster.
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Wir würden nur erwarten, dass i
definitiv zugewiesen wird, wenn die Eingabe nicht null ist. Da wir jedoch nicht wissen, ob die Eingabe innerhalb des Blocks null ist oder nicht, wird i
nicht definitiv zugewiesen. Was, wenn wir jedoch zulassen, dass i
in verschiedenen Mustern deklariert wird, die sich gegenseitig ausschließen?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
Hier wird die Variable i
definitiv innerhalb des Blocks zugewiesen und nimmt ihren Wert vom anderen Element des Tupels, wenn ein Nullelement gefunden wird.
Es wurde auch vorgeschlagen, Variablen in jedem Fall eines Fallblocks (mehrfach) zu definieren.
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Um eine dieser Arbeiten vorzunehmen, müssten wir sorgfältig definieren, wo solche Mehrfachdefinitionen zulässig sind und unter welchen Bedingungen eine solche Variable definitiv zugewiesen wird.
Sollten wir uns entscheiden, diese Arbeit auf einen späteren Zeitpunkt zu verschieben (was ich empfehle), könnten wir in C# 9 sagen.
- Unter einem
not
oderor
dürfen Mustervariablen nicht deklariert werden.
Dann hätten wir Zeit, etwas Erfahrung zu sammeln, die Einblicke in den möglichen Wert des späteren Lockerungsprozesses geben könnte.
Ergebnis: Mustervariablen können nicht unter einem not
- oder or
-Pattern deklariert werden.
Diagnostik, Subsumption und Vollständigkeit
Diese neuen Musterformulare führen zu vielen neuen Möglichkeiten für diagnosefähige Programmiererfehler. Wir müssen entscheiden, welche Arten von Fehlern wir diagnostizieren und wie dies zu tun ist. Hier sind einige Beispiele:
case >= 0 and <= 100D:
Dieser Fall kann niemals übereinstimmen (da die Eingabe nicht sowohl ein int
als auch ein double
sein kann). Wir haben bereits einen Fehler, wenn ein Fall erkannt wird, der niemals übereinstimmen kann, aber sein Wortlaut („Der Switch-Fall wurde bereits von einem vorherigen Fall behandelt” und „Das Muster wurde bereits von einem vorherigen Arm des Switch-Ausdrucks behandelt”) kann in neuen Szenarien irreführend sein. Möglicherweise müssen wir den Wortlaut so ändern, dass das Muster nie mit der Eingabe übereinstimmt.
case 1 and 2:
Ebenso wäre dies ein Fehler, da ein Wert nicht sowohl 1
als auch 2
sein kann.
case 1 or 2 or 3 or 1:
Dieser Fall kann übereinstimmen, aber die or 1
am Ende fügt dem Muster keine Bedeutung hinzu. Ich schlage vor, einen Fehler zu erzeugen, wenn einige Konjunktionen oder Disjunktionen eines Verbundmusters weder eine Mustervariable definieren noch den Satz übereinstimmender Werte beeinflussen.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
Hier fügt 0 or 1 or
dem zweiten Fall nichts hinzu, da diese Werte vom ersten Fall behandelt worden wären. Dies verdient auch einen Fehler.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Ein Switch-Ausdruck wie dieser sollte als erschöpfende betrachtet werden (er behandelt alle möglichen Eingabewerte).
In C# 8.0 gilt ein Switch-Ausdruck mit einer Eingabe vom Typ byte
nur dann als erschöpfend, wenn es einen endgültigen Arm enthält, dessen Muster mit allem übereinstimmt (ein verworfenes Muster oder var-Muster). Selbst ein Switch-Ausdruck mit einem Arm für jeden eindeutigen byte
Wert wird in C# 8 nicht als erschöpfend betrachtet. Um die Vollständigkeit relationaler Muster ordnungsgemäß sicherzustellen, müssen wir auch diesen Fall berücksichtigen. Dies wird technisch eine bahnbrechende Änderung sein, aber es wird wahrscheinlich kein Benutzer bemerken.
C# feature specifications