Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

C# 7.0–Des évolutions mais pas de révolution

C# est un langage qui a connu de grandes évolutions majeures comme LINQ, il ne peut guère aller beaucoup plus loin mais malgré tout la nouvelle mouture apporte son lot d’innovations, qu’on aimera, adorera, ou détestera !

C# 7ème version

imageMicrosoft ayant connu des difficultés avec son Java maison a demandé à Anders de sauver la mise. Mélangeant ce qu’il avait fait avec Delphi (comme la notion de propriété) chez Borland, un peu du Java MS mis au placard et un soupçon de C++ il a pondu C#. On connait généralement tous l’histoire (mais ce n’est pas certain, ça remonte un peu !). C# aurait ainsi pu n’être qu’une tentative désespérée de sauver la face sans grand avenir. Mais il s’avère que ce langage a connu un franc succès… Et que le talent de Anders Hejlsberg au nom aussi imprononçable qu’il s’écrit a permis des évolutions incroyables dont la plus marquante pour moi restera pour toujours Linq dont je ne saurai plus me passer pour écrire du code.

Lentement mais surement, C# a franchi les outrages du temps (même s’il reste un langage assez récent comparé à C++ ou même Java) sans prendre une ride, sans devenir ringard ou pire, dépassé.

La 7ème version de ce langage arrive. Il y a fort à parier que jamais plus nous ne connaitrons de bonds aussi spectaculaires que Linq, mais tout de même, chaque version a amené ses petits perfectionnements qui au final en font un grand langage toujours aussi agréable à utiliser. La version 7 n’échappe pas à la règle même si comme à chaque fois désormais certaines “améliorations” feront hérisser le poil de certains.

Mais voyons de quoi il s’agit.

Les variables OUT

Un petit truc qui me plait bien car franchement cela m’agaçait… Au lieu d’écrire :

string result;  
if (dictionary.TryGetValue(key, out result)) 

On peut écrire :

if (dictionary.TryGetValue(key, out string result)) 

C’est quand même moins lourd. Cette variable a déclarer avant m’énervait. Soulagement donc mais pas une révolution c’est évident.

Tests étendus “pattern matching”

Voici quelque chose qui va permettre pour les uns d’écrire un code plus concis et plus lisible et pour d’autres qui signifie toujours plus d’obfuscation quii rend le langage de moins en moins lisible notamment pour les débutants. Les deux positions sont vraies, sans que cela soit paradoxal.

C# 7 amène dans son sac de nouveautés la notion de “patterns”. Le pattern matching se concrétise par des éléments syntaxiques qui peuvent tester si une valeur a une certaine «forme», et extraire des informations à partir de la valeur quand cela est nécessaire.

Pour l’instant les motifs ou patterns que C# 7.0 propose sont de type Constante (tester si C est égal à la valeur testée), des motifs de Type sous la forme T x (on teste si une valeur est de type T et dans ce cas elle est placée dans la valeur x), et les motifs de variables sous la forme Var x (le test passe toujours et la valeur est transférée dans x).

Ce n’est bien sur qu’un début et cela évoluera avec d’autres types de motifs.

En pratique il faut un peu de code pour voir de quoi il s’agit je l’admets.

Expression “is” avec pattern matching

Prenons le cas de “is”, lorsqu’on y ajoute le pattern matching cela devient encore plus puissant :

public void PrintStars(object o)
{
    if (o is null) return;     // constant pattern "null"
    if (!(o is int i)) return; // type pattern "int i"
    WriteLine(new string('*', i));
}

Comme on le voit la première ligne est assez classique et correspond au “constant pattern “null””, au motif de type Constante dont la valeur de test est “null”.

La seconde ligne devient plus intéressante et moins classique. Le “is” est utilisé avec le “type pattern”, quand on teste un type tout en transférant dans une valeur si le test passe. Ici on note le “i” placé derrière “int”. Sans ce “i” le test ressemble à du C# 6 ou précédent. Avec le “i” on demande en pratique de créer une variable “i” de type “int” si le “is” passe… Du coup la dernière ligne de la méthode n’est pas exécutée si le “is” ne passe pas (test assez classique de la ligne précédente) mais si elle est exécutée on pourra utiliser la variable entière “i” qui aura été créée !

C’est simple mais très efficace.

Le Switch et le pattern matching

Autre possibilité d’utilisation du pattern matching, avec le switch et le case.

Il devient d’abord possible de faire un switch sur n’importe quel type et plus seulement sur un type primaire, ce qui est génial, et les patterns peuvent être utilisées dans les “case”, ce qui est fantastique, les “case” peuvent avoir des conditions additionnelles, ce qui est la cerise sur le gâteau !

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Attention embrouille !

Avec l’introduction du pattern matching l’ordre des cas dans le swtich devient désormais important !

Le premier test qui matche fera exécuter le cas. Par exemple on note que le cas du carré (longueur égale à hauteur) est testé avant celui du rectangle… Inverser le test donnera un résultat différent.

Autre détail à noter, le cas “default” est toujours évalué en dernier. Même si dans le code ci-dessus le test à “null” semble suivre le test par défaut, ce dernier sera toujours exécuté à la fin (donc après le test à null ici, ce qui est ainsi différent de l’ordre d’écriture et peut troubler les développeurs peu attentifs !).

Plus étonnant le cas “null” ne sera en fait jamais exécuté c’est un code mort à supprimé car le switch version pattern matching fonctionne comme le “is”, c’est à dire que le “null” n’est jamais testé comme ok. Si on veut tester la valeur nulle il faut laisser cela au cas “default” ou le faire autrement…

Comme je le disais il y aura les gars qui seront pour et les gars qui seront contre ! (et ça marche pareil avec les nanas, je veux rassurer les féministes !).

Les tuples

Les tuples ça existe déjà et il y a déjà un clivage entre les deux camps. Personnellement je trouve ça très pratique dans l’idée mais à chaque fois que j’ai écrit un code qui les utilisait j’ai tout refait en déclarant un type car je trouvais imbuvable le coup des “item1”, “item2” etc qui n’ont aucun sens et font donc perdre celui du code (avec à la clé plus d’erreurs possibles).

Mais c’est là que C# 7 vient ajouter le petit truc en plus qui rend tout cela plus utilisable. Avec les types de tuple et les littéraux de tuple.

Avec cet ajout on trouve donc désormais des méthodes capables de retourner plusieurs valeurs typées et nommées. Plus d’obligation de créer une classe juste pour rendre lisible une méthode qui retourne plusieurs valeurs, c’est un truc que j’adore et qui s’ajoute aux autres améliorations des tuples :

(string, string, string) LookupName(long id) // tuple return type
{
    ... // retrieve first, middle and last from data storage
    return (first, middle, last); // tuple literal
}

Ici le “return” passe simplement trois valeurs entre parenthèses. C’est possible car la méthode prend comme type de retour (string, string, string) ce qui est automatiquement traduit en tuple sans avoir à l’écrire.

Mais ce n’est toujours qu’un tuple “normal” qui est créé. Ainsi on utilisera la méthode comme si elle était écrite avec Tuple<string,string,string>, c’est à dire avec les “Item1”, “Item2”, ceux qui me chagrinent justement…

var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");

C’est mieux au niveau de la déclaration de la méthode mais c’est toujours aussi fouillis au niveau de l’utilisation. Mais heureusement C# 7 nous permet d’aller un cran plus loin en écrivant la déclaration de la méthode ainsi :

(string first, string middle, string last) LookupName(long id)…

Maintenant chaque élément du tuple en plus d’être typé, en plus de ne pas demander l’indication Tuple<…> devient un champ nommé !

L’utilisation de la méthode devient alors véritablement clair :

var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");

Le type Tuple<…>, sa déclaration habituelle, son côté figé, tout cela disparait. C’est en réalité un type Dynamic qui est créé, mais de façon à ce que cela soit plus performant que les dynamiques habituels. Ces contournements font que pour un novice qui apprendra C# 7 il sera plus facile de lui dire que les méthodes peuvent retourner plusieurs valeurs sans même parler de la classe Tuple.

Cela va même plus loin avec la “déconstruction”, c’est à dire la segmentation des éléments d’un Tuple dans plusieurs variables :

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");

L’appelant récupère ici les trois valeurs du tuple dans trois variables, peu importe les noms des champs du tuple.

On notera qu’on peut utiliser “var” au lieu de nommer le type de chaque variable. Il est bien sûr possible de déconstruire un tuple dans des variables existantes :

(first, middle, last) = LookupName(id2);

On suppose ici que first, middle et last sont déjà déclarés plus haut dans le code. Aucune nouvelle variable ne sera créée par la déconstruction.

Déconstruction partout !

Ce que je viens de dire pour les tuples marche en réalité pour TOUT type. On peut désormais déconstruire n’importe quel type, à une condition toutefois : que ce type contienne une méthode spéciale qui s’appelle forcément Deconstruct :

public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }

Pascal ou C# ? : Les fonctions locales

Et oui tout vient à temps pour qui sait attendre… J’ai quitté il y a longtemps le Pascal pour C# mais parfois encore malgré les années des choses me manquent cruellement, comme les sous-fonctions.

Avec l’arrivée des expressions Lambda j’avais trouvé un moyen de déclarer des delagate dans les méthodes pour créer des sous-fonctions à la Pascal. Ca marche plutôt bien.

Je sais, là aussi il y a le camp de ceux qui adorent et de ceux qui détestent. Un Pascalien reste un Pascalien je n’y peux rien, moi j’adore.

Mais ce n’était qu’une astuce et seulement depuis l’arrivé des Lambda… Grâce à C# 7 on retrouve ce que Anders avait déjà fait dans Turbo Pascal et Delphi… Tant d’années pour y revenir… mais c’est là, ça s’écrit comme une sous-fonction Pascal sans bricoler avec les lambda :

public int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
    return Fib(x).current;

    (int current, int previous) Fib(int i)
    {
        if (i == 0) return (1, 0);
        var (p, pp) = Fib(i - 1);
        return (p + pp, p);
    }
}

L’exemple (puisé de la documentation MS) est un peu tiré par les cheveux car la vraie fonction est celle qui est déclarée à l’intérieur, elle ne devrait pas être là mais à part. Mais ça se discute… La méthode qui déclare la sous-fonction (en C#7 on dit méthode locale) n’est qu’une enveloppe qui teste si la valeur est négative, c’est totalement artificiel et pourrait être contenu dans la fonction elle-même. Mais les exemples sont ce qu’ils sont, souvent réducteurs… Et tout le monde s’enflamme sur l’exemple trop simple qui ne sert pas la cause de ce qui est montré !

En tout cas vous avez compris le principe, et s’il est bien utilisé je suis totalement pour. Pas seulement par nostalgie de pascalien mais tout simplement parce qu’il arrive souvent qu’une méthode soit plus claire avec des sous-fonctions. Certains diront que si le code d’une méthode devient trop “compliqué” (ce qui est un jugement ‘moral’ plus que technique, aucune norme n’existant) il faut éclater la méthode en plusieurs méthodes.

Parfois c’est en effet ce qu’il faut faire.

Mais parfois pas du tout, cela pollue la classe avec des tas de petites méthodes qui en réalité ne servent à rien en dehors des méthodes où elles auraient du être déclarées.

En Pascal il existe par exemple la notion de variable globale, déclarée hors de toute classe (qui n’existent pas en Pascal standard), ce que Delphi qui est objet avait repris mais avec une astuce, ces variables étaient de toute façon englobées en réalité dans une classe non visible. Mais peu importe, tout le monde considère, moi le premier, que des variables globales comme le fait le Pascal non objet est une catastrophe aujourd’hui. C’est plus clair si les variables sont contenues dans les classes où elles sont utilisées. Et les classes dans des namespaces…

Si les sous-fonctions vous gênent dîtes-vous qu’il ne s’agit jamais que d’attribuer un namespace local à une méthode privée localement (ce namespace étant la méthode mère), et ce pour les mêmes bonnes raisons qu’on n’utilise plus de variables “globales” et qu’on place les variables dans des classes et les classes dans des namespaces…

Les littéraux le binaire et le underscore

Certainement moins clivant, les littéraux sont autorisés à utiliser le signe souligné (le “tiret du 8”) pour séparer les chiffres ce qui permet de les rendre plus lisibles :

var d = 123_456;
var x = 0xAB_CD_EF;

var b = 0b1010_1011_1100_1101_1110_1111;

Au passage on découvre les littéraux binaires… déclarés avec le préfixe “0b”. Le souligné prend ici tout son intérêt pour séparer les octets.

Le retour d’une Référence

Alors là c’est chaud, très chaud. Niveau polémique je suis certain que ça va bouillir avec des équipes qui interdiront carrément ce genre de trucs comme c’est le cas pour la surcharge des opérateurs. On replonge dans les heures sombres du développement avec les histoires de pointeurs retournés à la place de valeurs ce qui fichait un beau bazar et était l’un des points rendant les programmes en C si prompts à planter !

Alors certes il ne s’agit pas de manipuler ici de vrais pointeurs mais c’est tout comme techniquement, une référence c’est un pointeur. Et ça possède les mêmes effets pervers. Effets qui est justement ce qui est recherché ici, ce qui est un peu vicieux je trouve.

Il y a néanmoins des cas où cette nouveauté sera certainement très appréciée, comme toutes les features un peu discutables.

Concrètement ? Voici un bout de code :

public ref int Find(int number, int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (numbers[i] == number) 
        {
            return ref numbers[i]; // return the storage location, not the value
        }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} not found");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9

La méthode “Find” ne retourne plus une valeur mais une référence. De fait son appel est farci de “ref” pour le faire comprendre. Mais lorsqu’on modifie la variable “place” on modifie aussi la valeur dans l’array originale ! Attention danger donc…

Les corps d’expression

C’est un ajour de C# 6 que j’aime bien car cela simplifie le code pour le développeur expérimenté sans le rendre plus difficile à comprendre pour le débutant (et un code lisible par tous dans une équipe qui peut recevoir des novices est essentiel). On remplace une méthode par une expression. Il existait toutefois des limitations, C# 7 les fait sauter et on peut utiliser les expressions pour ainsi dire partout même dans les constructeurs ou destructeurs :

class Person
{
    private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
    private int id = GetId();

    public Person(string name) => names.TryAdd(id, name); // constructors
    ~Person() => names.TryRemove(id, out *);              // destructors
    public string Name
    {
        get => names[id];                                 // getters
        set => names[id] = value;                         // setters
    }
}

Les expressions peuvent être utilisées aussi pour lever des exceptions, là encore cela simplifie beaucoup le code :

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name ?? throw new ArgumentNullException(name);
    public string GetFirstName()
    {
        var parts = Name.Split(" ");
        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
    }
    public string GetLastName() => throw new NotImplementedException();
}

D’autres choses

Il existe d’autres améliorations notamment dans la généralisation des types de retour async, mais la feature n’est pas encore implémentée dans la preview. Il y aura certainement d’autres modifications mineures dans la version finale.

Conclusion

C# évolue, de plus en plus à la marge car il a tellement évolué qu’on voit mal ce qu’on pourrait encore lui ajouter, en tout cas de très significatif et sans le changer en profondeur. Pour les changements radicaux de paradigme il y a déjà F# par exemple. Inutile de trop bricoler C#.

Il n’empêche, C# n’est pas statique, il s’améliore sans cesse. Des features introduites dans une version sont améliorées dans une suivante.

Et c’est bien de travailler avec un langage qui se modernise tout le temps. Certes apprendre C# aujourd’hui est bien plus difficile qu’à l’époque de la version 1.0. C’est le prix à payer.

Mais grâce à cela C# reste un langage performant, d’une puissance incroyable, et toujours très agréable à utiliser au quotidien, et ça c’est très important !

 

Stay Tuned !

Faites des heureux, PARTAGEZ l'article !