Freigeben über


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:

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, nintund 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.NaNist. 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, orund 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:

  1. Wenn P ein Typmuster ist, ist der eingeschränkte Typ der Typ des Typmusters.
  2. Wenn P ein Deklarationsmuster ist, ist der eingeschränkte Typ der Typ des Deklarationsmusters.
  3. Wenn P ein rekursives Muster ist, das einen expliziten Typ angibt, ist der schmale Typ dieser Typ.
  4. Wenn P anhand der Regeln für ITuple abgeglichen wird, ist der eingeschränkte Typ der Typ System.Runtime.CompilerServices.ITuple.
  5. 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.
  6. 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.
  7. Wenn P ein or-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 von or-Mustern (ignoriert Klammermuster).
  8. Wenn P ein and-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.
  9. Ansonsten ist der eingeschränkte Typ von P der Eingabetyp von P.

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 oder ordü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 2sein 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.