Machen Sie mehr mit Mustern in C # 8.0

Visual Studio 2019 Preview 2 ist da! Und damit stehen Ihnen einige weitere C # 8.0-Funktionen zur Verfügung. Es geht hauptsächlich um Mustervergleich, obwohl ich am Ende noch einige andere Neuigkeiten und Änderungen ansprechen werde.


Original im Blog

Mehr Muster an mehr Orten


Als C # 7.0 Pattern Matching einführte, sagten wir, dass wir in Zukunft mehr Muster an mehr Stellen hinzufügen würden. Diese Zeit ist gekommen! Wir fügen das hinzu, was wir als rekursive Muster bezeichnen , sowie eine kompaktere Ausdrucksform von switch Anweisungen, die Switch- Ausdrücke genannt werden (Sie haben es erraten!).


Hier ist ein einfaches C # 7.0-Beispiel für Muster, um uns zu beginnen:


 class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } static string Display(object o) { switch (o) { case Point p when pX == 0 && pY == 0: return "origin"; case Point p: return $"({pX}, {pY})"; default: return "unknown"; } } 

Ausdrücke wechseln


Lassen Sie uns zunächst feststellen, dass viele switch Anweisungen in den Fallkörpern nicht wirklich interessante Arbeit leisten. Oft erzeugen sie alle nur einen Wert, entweder indem sie ihn einer Variablen zuweisen oder indem sie ihn zurückgeben (wie oben). In all diesen Situationen ist die switch-Anweisung offen gesagt ziemlich klobig. Es fühlt sich an wie das 5 Jahrzehnte alte Sprachmerkmal, mit vielen Zeremonien.


Wir beschlossen, dass es Zeit war, eine Ausdrucksform des switch hinzuzufügen. Hier ist es, angewendet auf das obige Beispiel:


 static string Display(object o) { return o switch { Point p when pX == 0 && pY == 0 => "origin", Point p => $"({pX}, {pY})", _ => "unknown" }; } 

Hier haben sich einige Dinge gegenüber switch-Anweisungen geändert. Lassen Sie uns sie auflisten:


  • Das Schlüsselwort switch lautet "infix" zwischen dem getesteten Wert und der {...} Liste der Fälle. Das macht es kompositorischer mit anderen Ausdrücken und auch visuell leichter von einer switch-Anweisung zu unterscheiden.
  • Das case Schlüsselwort und das : wurden der Kürze halber durch einen Lambda-Pfeil => .
  • default wurde der Kürze halber durch das _ Verwerfungsmuster _ ersetzt.
  • Die Körper sind Ausdrücke! Das Ergebnis des ausgewählten Körpers wird zum Ergebnis des Schalterausdrucks.

Da ein Ausdruck entweder einen Wert haben oder eine Ausnahme auslösen muss, löst ein Schalterausdruck, der ohne Übereinstimmung das Ende erreicht, eine Ausnahme aus. Der Compiler warnt Sie hervorragend, wenn dies der Fall sein kann, zwingt Sie jedoch nicht dazu, alle Switch-Ausdrücke mit einem Catch-All zu beenden: Vielleicht wissen Sie es besser!


Da unsere Display Methode jetzt aus einer einzelnen return-Anweisung besteht, können wir sie natürlich so vereinfachen, dass sie einen Ausdruck enthält:


  static string Display(object o) => o switch { Point p when pX == 0 && pY == 0 => "origin", Point p => $"({pX}, {pY})", _ => "unknown" }; 

Um ehrlich zu sein, bin ich mir nicht sicher, welche Formatierungsanleitung wir hier geben werden, aber es sollte klar sein, dass dies viel knapper und klarer ist, insbesondere weil die Kürze es Ihnen normalerweise ermöglicht, den Schalter wie oben "tabellarisch" zu formatieren , mit Mustern und Körpern auf derselben Linie, und die => s sind untereinander aufgereiht.


Übrigens planen wir, ein nachfolgendes Komma nach dem letzten Fall in Übereinstimmung mit allen anderen "durch Kommas getrennten Listen in geschweiften Klammern" in C # zuzulassen, aber Vorschau 2 erlaubt dies noch nicht.


Eigenschaftsmuster


Apropos Kürze, die Muster werden plötzlich zu den schwersten Elementen des obigen Schalterausdrucks! Lassen Sie uns etwas dagegen tun.


Beachten Sie, dass der switch-Ausdruck das Typmuster Point p (zweimal) sowie eine when Klausel verwendet, um zusätzliche Bedingungen für den ersten case hinzuzufügen.


In C # 8.0 fügen wir dem Typmuster weitere optionale Elemente hinzu, sodass das Muster selbst weiter in den Wert eingreifen kann, der mit dem Muster übereinstimmt. Sie können es zu einem Eigenschaftsmuster machen, indem Sie {...} mit verschachtelten Mustern hinzufügen, um sie auf die zugänglichen Eigenschaften oder Felder des Werts anzuwenden. Lassen Sie uns den Schalterausdruck wie folgt umschreiben:


 static string Display(object o) => o switch { Point { X: 0, Y: 0 } p => "origin", Point { X: var x, Y: var y } p => $"({x}, {y})", _ => "unknown" }; 

In beiden Fällen wird weiterhin geprüft, ob o ein Point . Der erste Fall wendet dann das konstante Muster 0 rekursiv auf die X und Y Eigenschaften von p und prüft, ob sie diesen Wert haben. Somit können wir die when Klausel in diesem und vielen häufigen Fällen beseitigen.


Der zweite Fall wendet das var Muster auf jedes von X und Y Denken Sie daran, dass das var Muster in C # 7.0 immer erfolgreich ist und einfach eine neue Variable deklariert, die den Wert enthält. Somit erhalten x und y die int-Werte von pX und pY .


Wir verwenden p nie und können es hier sogar weglassen:


  Point { X: 0, Y: 0 } => "origin", Point { X: var x, Y: var y } => $"({x}, {y})", _ => "unknown" 

Eine Sache, die für alle Typmuster einschließlich Eigenschaftsmuster gilt, ist, dass der Wert ungleich Null sein muss. Dies eröffnet die Möglichkeit, dass das "leere" Eigenschaftsmuster {} als kompaktes "Nicht-Null" -Muster verwendet wird. Zum Beispiel könnten wir den Fallback-Fall durch die folgenden zwei Fälle ersetzen:


  {} => o.ToString(), null => "null" 

Das {} behandelt verbleibende Nicht- null Objekte, und null erhält die Nullen, sodass der Schalter vollständig ist und der Compiler sich nicht über durchfallende Werte beschwert.


Positionsmuster


Das Eigenschaftsmuster hat den zweiten Point nicht genau kürzer gemacht und scheint die Mühe dort nicht wert zu sein, aber es kann noch mehr getan werden.


Beachten Sie, dass die Point Klasse eine Deconstruct Methode hat, einen sogenannten Deconstructor . In C # 7.0 erlaubten Dekonstruktoren, dass ein Wert bei der Zuweisung dekonstruiert wurde, sodass Sie zB schreiben konnten:


 (int x, int y) = GetPoint(); // split up the Point according to its deconstructor 

C # 7.0 hat die Dekonstruktion nicht in Muster integriert. Dies ändert sich mit Positionsmustern, die eine zusätzliche Möglichkeit darstellen, Typmuster in C # 8.0 zu erweitern. Wenn der übereinstimmende Typ ein Tupeltyp ist oder einen Dekonstruktor hat, können wir Positionsmuster als kompakte Methode zum Anwenden rekursiver Muster verwenden, ohne Eigenschaften benennen zu müssen:


 static string Display(object o) => o switch { Point(0, 0) => "origin", Point(var x, var y) => $"({x}, {y})", _ => "unknown" }; 

Sobald das Objekt als Point abgeglichen wurde, wird der Dekonstruktor angewendet und die verschachtelten Muster werden auf die resultierenden Werte angewendet.


Dekonstruktoren sind nicht immer angemessen. Sie sollten nur zu Typen hinzugefügt werden, bei denen wirklich klar ist, welcher der Werte welcher ist. Für eine Point Klasse ist es beispielsweise sicher und intuitiv anzunehmen, dass der erste Wert X und der zweite Y , sodass der obige Schalterausdruck intuitiv und leicht zu lesen ist.


Tupelmuster


Ein sehr nützlicher Sonderfall von Positionsmustern ist, wenn sie auf Tupel angewendet werden. Wenn eine switch-Anweisung direkt auf einen Tupelausdruck angewendet wird, können wir sogar zulassen, dass der zusätzliche Satz von Klammern weggelassen wird, wie in switch (x, y, z) anstelle von switch ((x, y, z)) .


Tupelmuster eignen sich hervorragend zum gleichzeitigen Testen mehrerer Eingabeteile. Hier ist eine einfache Implementierung einer Zustandsmaschine:


 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition) switch { (Opened, Close) => Closed, (Closed, Open) => Opened, (Closed, Lock) when hasKey => Locked, (Locked, Unlock) when hasKey => Closed, _ => throw new InvalidOperationException($"Invalid transition") }; 

Natürlich könnten wir uns dafür entscheiden, hasKey in das eingeschaltete Tupel aufzunehmen, anstatt when Klauseln zu verwenden - es ist wirklich Geschmackssache:


 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition, hasKey) switch { (Opened, Close, _) => Closed, (Closed, Open, _) => Opened, (Closed, Lock, true) => Locked, (Locked, Unlock, true) => Closed, _ => throw new InvalidOperationException($"Invalid transition") }; 

Alles in allem hoffe ich, dass Sie sehen können, dass rekursive Muster und Schalterausdrücke zu einer klareren und deklarativeren Programmlogik führen können.


Weitere C # 8.0-Funktionen in Vorschau 2


Während die Musterfunktionen die wichtigsten sind, die in VS 2019 Preview 2 online gehen, gibt es einige kleinere, von denen ich hoffe, dass Sie sie auch nützlich und unterhaltsam finden. Ich werde hier nicht auf Details eingehen, sondern Ihnen nur eine kurze Beschreibung von jedem geben.


Deklarationen verwenden


In C # führt die using Anweisungen immer zu einer Verschachtelung, die sehr ärgerlich sein und die Lesbarkeit beeinträchtigen kann. Für die einfachen Fälle, in denen nur eine Ressource am Ende eines Bereichs bereinigt werden soll, müssen Sie jetzt stattdessen Deklarationen verwenden. Verwenden von Deklarationen sind einfach lokale Variablendeklarationen mit einem using Schlüsselwort im Vordergrund, und ihr Inhalt wird am Ende des aktuellen Anweisungsblocks angeordnet. Also statt:


 static void Main(string[] args) { using (var options = Parse(args)) { if (options["verbose"]) { WriteLine("Logging..."); } ... } // options disposed here } 

Sie können einfach schreiben


 static void Main(string[] args) { using var options = Parse(args); if (options["verbose"]) { WriteLine("Logging..."); } } // options disposed here 

Einweg-Ref-Strukturen


Ref-Strukturen wurden in C # 7.2 eingeführt, und dies ist nicht der Ort, um ihre Nützlichkeit zu wiederholen, aber im Gegenzug weisen sie einige schwerwiegende Einschränkungen auf, z. B. die Unfähigkeit, Schnittstellen zu implementieren. Ref-Strukturen können jetzt ohne Implementierung der IDisposable Schnittstelle verfügbar IDisposable werden, indem einfach eine Dispose Methode enthalten ist.


Statische lokale Funktionen


Wenn Sie sicherstellen möchten, dass Ihre lokale Funktion nicht die Laufzeitkosten verursacht, die mit dem "Erfassen" (Referenzieren) von Variablen aus dem umschließenden Bereich verbunden sind, können Sie sie als static deklarieren. Dann verhindert der Compiler die Referenzierung von Elementen, die in umschließenden Funktionen deklariert sind - mit Ausnahme anderer statischer lokaler Funktionen!


Änderungen seit Vorschau 1


Die Hauptmerkmale von Vorschau 1 waren nullfähige Referenztypen und asynchrone Streams. Beide haben sich in Vorschau 2 etwas weiterentwickelt. Wenn Sie sie also verwenden, sollten Sie Folgendes beachten.


Nullable Referenztypen


Wir haben weitere Optionen hinzugefügt, um nullfähige Warnungen sowohl in der Quelle (über die #nullable und #pragma warning ) als auch auf Projektebene zu steuern. Wir haben auch das Opt-In für die Projektdatei in <NullableContextOptions>enable</NullableContextOptions> .


Asynchrone Streams


Wir haben die Form der IAsyncEnumerable<T> -Schnittstelle geändert, die der Compiler erwartet! Dies führt dazu, dass der Compiler nicht mehr mit der in .NET Core 3.0 Preview 1 bereitgestellten Schnittstelle synchronisiert ist, was zu Problemen führen kann. .NET Core 3.0 Preview 2 ist jedoch in Kürze verfügbar, wodurch die Schnittstellen wieder synchronisiert werden.


Haben Sie es!


Wie immer freuen wir uns über Ihr Feedback! Bitte spielen Sie insbesondere mit den neuen Musterfunktionen herum. Stoßen Sie auf Backsteinmauern? Ist etwas nervig? Welche coolen und nützlichen Szenarien finden Sie für sie? Klicken Sie auf den Feedback-Button und lassen Sie es uns wissen!


Viel Spaß beim Hacken


Mads Torgersen, Designleiter für C #

Source: https://habr.com/ru/post/de438256/


All Articles