Plus de fonctionnalités avec des modèles dans C # 8.0

Plus récemment, Visual Studio 2019 Preview 2 est sorti et avec lui, quelques fonctionnalités supplémentaires de C # 8.0 sont prêtes à être essayées. Il s'agit principalement d'une comparaison avec l'échantillon, bien qu'à la fin je vais aborder quelques autres nouvelles et modifications.


Cet article est en anglais.




Merci d'avoir traduit notre MSP, Lev Bulanov .

Plus de modèles dans plus d'endroits


Lorsque la correspondance de modèles est apparue dans C # 7.0, nous avons noté qu'une augmentation du nombre de modèles dans plus d'endroits était attendue à l'avenir. Cette fois est venue! Nous ajoutons ce que nous appelons des modèles récursifs, ainsi qu'une forme plus compacte d'expressions de commutateur appelées (vous l'avez deviné) des expressions de commutateur.


Pour commencer, voici un exemple simple de modèles C # 7.0:


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"; } } 

Changer d'expressions


Tout d'abord, notez que de nombreuses expressions de commutateur , en fait, ne font pas beaucoup de travail intéressant dans les corps de cas . Souvent, tous créent simplement une valeur, soit en l'affectant à une variable, soit en la renvoyant (comme indiqué ci-dessus). Dans toutes ces situations, l'interrupteur semble être hors de propos. Ceci est similaire à une fonction de langue vieille de cinquante ans.


Nous avons décidé qu'il était temps d'ajouter un formulaire de déclaration de changement . Cela s'applique à l'exemple ci-dessous:


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

Il y a quelques choses qui ont changé par rapport aux instructions switch. Énumérons-les:


  • Le mot-clé switch est "infixe" entre la valeur testée et la liste des {...} cas. Cela le rend plus compositionnel avec d'autres expressions, et aussi plus facile à distinguer visuellement de l'instruction switch.
  • Le mot clé et le symbole case : ont été remplacés par la flèche lambda => pour faire court.
  • La valeur par défaut pour la brièveté a été remplacée par le modèle de réinitialisation _ .
  • Les corps sont des expressions. Le résultat du corps sélectionné devient le résultat de l'instruction switch.

Parce que l'expression doit compter ou lever une exception, une expression de sélection qui se termine sans correspondance lèvera une exception. Le compilateur vous avertira lorsque cela peut se produire, mais il ne vous forcera pas à terminer toutes les instructions select avec la fonction catch-all.


Puisque notre méthode Display consiste désormais en une seule instruction return, nous pouvons la simplifier pour l'expression:


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

Quelles que soient les recommandations de mise en forme, elles doivent être extrêmement claires et concises. Brevity vous permet de formater le commutateur de manière "tabulaire", comme décrit ci-dessus, avec des motifs et un corps sur la même ligne, et => alignés les uns sous les autres.


Soit dit en passant, nous prévoyons d'autoriser l'utilisation d'une virgule après le dernier cas conformément à toutes les autres "listes séparées par des virgules entre crochets" en C #, mais cela n'est pas encore autorisé dans l'aperçu 2.


Propriétés du motif


En parlant de brièveté, les motifs deviennent soudain les éléments les plus difficiles des expressions de choix. Faisons quelque chose.


Notez que l'expression de sélection utilise un modèle de type Point p (deux fois), ainsi que quand ajouter des conditions supplémentaires dans le premier cas .


Dans C # 8.0, nous ajoutons des éléments facultatifs supplémentaires au type de modèles, ce qui permet au modèle lui-même d'aller plus loin dans la valeur qui correspond au modèle. Vous pouvez en faire un modèle de propriété en ajoutant {...} contenant des modèles imbriqués, en appliquant des valeurs aux propriétés ou aux champs disponibles. Cela nous permet de réécrire l'expression du commutateur comme suit:


 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" }; 

Les deux cas vérifient toujours que o est Point . Dans le premier cas, le motif de la constante 0 est récursivement appliqué aux propriétés X et Y de la variable p , vérifiant si elles ont cette valeur. Ainsi, nous pouvons éliminer la condition quand dans ce cas et dans d'autres cas similaires.


Dans le second cas, le motif var est appliqué à chacun de X et Y. Rappelez-vous que le modèle var en C # 7.0 réussit toujours et déclare simplement une nouvelle variable pour contenir la valeur. Donc, x et y contiennent des valeurs int pour pX et pY .


Nous n'utilisons jamais p et pouvons le sauter ici:


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

Une chose reste la même pour tous les types de modèles, y compris les modèles de propriété, c'est une exigence que la valeur soit non nulle. Cela ouvre la possibilité d'utiliser le modèle de propriétés "vide" {} comme un modèle compact "non nul". Par exemple. nous pourrions remplacer la solution de repli par les deux cas suivants:


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

{} Traite les autres objets non nuls et null obtient des zéros, donc le commutateur est exhaustif et le compilateur ne se plaindra pas des valeurs manquantes.


Modèles de position


Le modèle de propriété ne raccourcit pas le deuxième cas de point. Vous n'avez pas à vous en préoccuper, vous pouvez faire encore plus.


Notez que la classe Point a une méthode Deconstruct , le soi-disant déconstructeur. Dans C # 7.0, les déconstructeurs vous permettent de "déconstruire" une valeur lorsqu'ils sont attribués, vous pouvez donc écrire, par exemple:


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

C # 7.0 n'a pas intégré la déconstruction aux motifs. Cela change avec les modèles de position, qui sont un moyen supplémentaire d'étendre les types de modèles dans C # 8.0. Si le type correspondant est un type de tuple ou a un déconstructeur, nous pouvons utiliser des modèles de position comme un moyen compact d'appliquer des modèles récursifs sans avoir à nommer les propriétés:


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

Après avoir fait correspondre l'objet à Point , le déconstructeur est appliqué et les motifs imbriqués sont appliqués aux valeurs résultantes.


Les déconstructeurs ne sont pas toujours appropriés. Ils doivent être ajoutés uniquement aux types où il est vraiment clair laquelle des valeurs est laquelle. Par exemple, pour la classe Point , vous pouvez supposer que la première valeur est X et la seconde est Y , de sorte que l'expression de commutateur ci-dessus est claire et facile à lire.


Modèles de tuple


Un cas particulier très utile des modèles de position est leur application aux tuples. Si l'instruction switch est appliquée directement à l'expression de tuple, nous permettons même d'omettre l'ensemble supplémentaire de parenthèses, comme dans switch (x, y, z) au lieu de switch ((x, y, z)) .


Les modèles de tuple sont parfaits pour tester plusieurs entrées en même temps. Voici une implémentation simple de la machine d'état:


 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") }; 

Bien sûr, nous pourrions inclure hasKey dans le tuple au lieu d'utiliser des clauses when - c'est une question de goût:


 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") }; 

En général, vous voyez que les modèles récursifs et les expressions de commutateur peuvent conduire à une logique de programme plus claire et plus déclarative.


Autres fonctionnalités de C # 8.0 dans l'aperçu 2


Malgré le fait que dans VS 2019 Preview 2, les fonctions principales pour travailler avec des motifs sont les plus importantes, il y en a plusieurs plus petites que j'espère que vous trouverez également utiles et intéressantes. Je n'entrerai pas dans les détails, donnez juste une brève description de chacun.


Utilisation d'annonces


En utilisant C # , les opérateurs augmentent toujours le niveau d'imbrication, ce qui peut être très ennuyeux et de mauvaise lisibilité. Dans les cas simples, lorsque vous avez juste besoin d'effacer la ressource à la fin de la portée, des déclarations sont utilisées. Les déclarations using sont simplement des déclarations de variables locales avec le mot-clé using devant elles, et leur contenu est placé à la fin du bloc d'instructions en cours. Par conséquent, au lieu de:

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

Vous pouvez simplement écrire


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

Structures de référence jetables


Les structures de référence ont été introduites dans C # 7.2, et il ne semble pas y avoir de place pour les répéter ici. Mais tout de même, il convient de noter quelque chose: ils ont certaines limites, comme l'impossibilité d'implémenter des interfaces. Les structures de référence peuvent désormais être utilisées sans implémenter l'interface IDisposable , en utilisant simplement la méthode Dispose .


Fonctions locales statiques


Si vous voulez vous assurer que votre fonction locale n'entraîne pas de coûts d'exécution associés à la «capture» (référencement) des variables de la portée, vous pouvez la déclarer statique. Ensuite, le compilateur empêchera le lien vers tout ce qui est déclaré dans les fonctions englobantes - à l'exception des autres fonctions locales statiques!


Modifications de l'aperçu 1


Les principales fonctions de l'aperçu 1 étaient des types de référence annulables et des flux asynchrones. Les deux fonctionnalités ont un peu changé dans l'aperçu 2, donc si vous avez commencé à les utiliser, il est utile de connaître les éléments suivants.


Types de référence nullables


Nous avons ajouté plus d'options pour gérer les avertissements annulables à la fois à la source (via les directives d' avertissement #nullable et #pragma ) et au niveau du projet. Nous avons également changé l'abonnement au fichier de projet en <NullableContextOptions> activer </ NullableContextOptions> .


Threads asynchrones


Nous avons changé la forme de l' interface IAsyncEnumerable <T> que le compilateur attend. Cela conduit au fait que le compilateur ne se synchronise pas avec l'interface fournie dans .NET Core 3.0 Preview 1, ce qui peut provoquer certains problèmes. Cependant, .NET Core 3.0 Preview 2 sera bientôt publié et cela renverra la synchronisation.

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


All Articles