La première version du langage C# a vu le jour il y’a 15 ans, avec l’arrivée du .Net framework 1.0, et depuis, ce langage n’a pas cessé d’évoluer jusqu’à arriver à la version 7.

C# 7 marque un bond considérable avec l’arrivée d’un grand nombre de nouveautés qu’on peut déjà s’amuser à expérimenter avec VS 2017 RC.
Cette version n’est pas encore en version finale, et ne sera complètement disponible que dans Visual Studio 2017.

Décortiquons ensemble ces nouveautés :

Vous pouvez télécharger le code source utilisé tout au long de cet artile sur Github.


1. Litteral binaries

Afin d’améliorer la lisibilité du code, C#7 permet d’utiliser un séparateur de littéreaux numériques. C’est le caractère “_” (underscore) qu’on pourra inclure dorénavant dans ces littéreaux, comme suit :

int first = 123_456;

Cette amélioration nous sera beaucoup plus utile avec l’écriture des entiers en binaire (préfixé par “0b”), qui sera désormais possible:

int second = 0b_100;
byte binary = 0b001_001;
int numbers[] = {0b1, 0b10, 0b100, 0b1_000, 0b10__000};

2. Tuples

J’ai installé le package nuget System.ValueTuple pour tester les Tuples.

Dans certains cas, les développeurs ont besoin de retourner plusieurs valeurs depuis une fonction. Pour ce faire, ils utilisent par exemple :

  • System.Tuple : le code peut-être fastidieux, et on ne peut pas renommer les variables de retour (Item1, Item 2…);
  • Les paramètres “out” : ça ne marche pas avec les méthodes async;
  • Une liste, un tableau ou tout autre objet spécifique : beaucoup de code pour un objet qui sera peu utilisé.

Pour faire mieux, C# 7 ajoute les Tuples : System.ValueTuple<>

Pour mettre les Tuples en oeuvre, on va écrire une fonction qui calcule la somme et le nombre des éléments du tableau “numbers” qu’on a vu dans la première partie de l’article :

(int, int) Calculer(int[] numbers)
{
    var r = (0,0);
    foreach (var n in numbers)
    {
        r.Item1 += n;
        r.Item2++;
    }
    return r;
}

var t = Calculer(numbers);
WriteLine($"Sum: {t.Item1}, Count: {t.Item2}");

La fonction retourne bien un tuple (la somme et le count), mais on peut l’améliorer et tirer profit de tout ce que cette nouvelle fonctionnalité nous offre.
On a la possibilité de donner des noms significatifs à nos variables de retour, et se débarraser des “Item1, Item2…”, ce qui donne :

(int sum, int count) Calculer(int[] numbers)
{
    var r = (s:0,c:0);
    foreach (var n in numbers)
    {
        r = (r.s +=n, r.c + 1);
    }
    return r;
}

var (sum, count) = Calculer(numbers);
WriteLine($"Sum: {sum}, Count: {count}");

3. Les fonctions locales

Parfois, on a besoin d’une fonction qui ne va servir que dans une seule méthode qui l’utilise (poue une meilleure lisibilité du code). Avec C# 7, on peut désormais déclarer des fonctions dans le corps d’autres fonctions : C’est les fonctions locales.
Prenons par exemple le dernier bout de code qu’on a vu dans la section précedente. On va créer une fonction locale “Ajouter” qui va se charger d’incrémenter la variable count et d’auditionner les valeurs du tableau à la variable somme, comme suit :

(int sum, int count) Calculer(int[] numbers)
{
    var r = (s:0,c:0);
    foreach (var n in numbers)
    {
        Ajouter(n, 1);
    }
    return r;

    void Ajouter(int x, int y) 
    {
        r.s += x; 
        r.c += y;
    }
}

Les paramètres et les variables locales de la fonction “Calculer” sont disponibles au sein de la fonction locale.


4. Déconstruction

Les déconstructeurs présentent un excellent moyen pour décomposer n’importe quel type (y compris les Tuples) qui a une méthode Deconstruct. Imaginons un type Personne qui a les deux propriétés Nom et Prenom

class Personne
{
    public string Nom {get; set;}
    public string Prenom {get; set;}

    public Personne(string nom, string prenom)
    {
        Nom = nom;
        Prenom = prenom;
    }

    public void Deconstruct(out string nom, out string prenom)
    {
        nom = Nom;
        prenom = Prenom;
    }
}

On peut appeler la méthode Deconstruct avec plusieurs façons pour assigner son résultat à des nouvelles variables :

Personne p = GetPersonne(); // GetPersonne() nous retournera un objet Personne
(var nom, var prenom) = p;
var (nom, prenom) = p;

On peut également assigner le résultat à des variables déjà existantes :

(_nom, _prenom) = p;

5. Les variables out

Cette fonctionnalité permet à une variable “out” d’être déclaré directement dans l’appel :

Avant C# 7 :

int i;
if (int.TryParse("1", out i))
{
    i++;
}

A partir de C# 7 :

if (int.TryParse("1", out int i))
{
    i++;
}

6. Ref Returns and Locals

Je n’ai pas encore testé cette fonctionnalité malheureusement.

Depuis la première version de C#, le langage prend en charge le passage des paramètres par référence, à l’aide du mot clé “ref”.
C# 7 permettra de déclarer une fonction qui, non seulement accepte les paramètres “ref”, mais qui a aussi une valeur de retour “ref” (la fonction retournera une référence au lieu d’une copie).
En plus, les variables locales peuvent être déclarées comme des variables “ref”.

On retrouve le bout de code suivant sur la page github de la fonctionnalité :

public static ref int Max(ref int first, ref int second, ref int third)
{
    ref int max = first > second ? ref first : ref second;
    return max > third ? ref max : ref third;
}

int a = 1, b = 2, c = 3;
Max(ref a, ref b, ref c) = 4;

Debug.Assert(a == 1); //true
Debug.Assert(b == 2); //true
Debug.Assert(c == 4); //true

Si on applique cette nouveauté à notre classe Personne (qu’on a vu à la section 4):

public ref int GetAge()
{
    return ref age;
}

Personne p = GetDummyPerson();
p.GetAge() = 30; // assigne la valeur 30 à la variable age (par référence)
ref int _age = ref GetAge(); //On récupère une référence mémoire

Le retour des valeurs par référence peut améliorer les performances des applications qui requièrent de grandes quantités de mémoire; Cette fonctionnalité permet aux développeurs de continuer à utiliser un code “safe” tout en évitant les copies inutiles.


7. Expression-Bodied Everything

C# 7 introduit une nouvelle syntaxe qui permet d’avoir des membres d’une classe dont le corps est une expression

public Personne(string nom) => Nom = nom; //Constructeur de la classe Personne
public string ToString() => $"Hello blog"; //Surcharge de la méthode ToString()

Cette écriture peut-être utilisée avec les constructeurs, destructeurs, proprietés, méthodes…


8. Throw Expr

D’après la page du projet roslyn, l’expression “throw :

  • N’a pas de type
  • Peut-être convertible à n’importe quel type par une conversion implicite

Une expression “throw n’est autorisée que dans les contextes syntaxiques suivants :

  • Comme deuxième ou troixième opérande d’un opérateur conditionnel ternaire “? :
int x = y == 2 ? 1 : throw new Exception(); 
  • Comme deuxième opérande d’un opérateur decoalescence nulle “??
var Nom = _nom ?? throw new Exception();
  • Comme le corps d’une “lambda expression-bodied” ou d’une méthode
private string Foo() => throw new Exception();

9. Pattern matching

Cette extension permet de nombreux avantages des langages fonctionnels (tels que F# et Scala), et présente une nouvelle forme d’expression qui permet une décomposition multiniveau et extrêmement concisie.

  • L’expression “is”

    L’opérateur is a été étendu pour permettre à toutes les constantes à droite de l’opérateur au lieu des types seulement.

public bool IsInt32(object obj)
{
    return (obj is Int32); //type pattern
}

public bool IsEqualToOne(object obj)
{
    return (obj is 5); //constant pattern
}

public bool IsNull(object obj)
{
    return (obj is null);
}

Cela peut également être combiné avec la fonctionnalité permettant l’affectation des variables via l’opérateur “is” :

public static void Dummy(object obj)
{
    if (obj is int i)
    {
        Console.WriteLine(i++);
    }
    else
    {
        Console.WriteLine("L'objet n'est pas un entier");
    }
}

Ce qui nous permettra d’éviter de faire un test de type et un cast et de gagner en productivité.

  • L’instruction “switch”

    La nouvelle extension a également étendu et modifié l’utilisation des instructions “case”. On peut créer des variables qui recevront le résultat de la conversion; Si par exemple on utilise un switch sur un type System.Object, on peut écrire :

case int x:

Si l’objet est un int, on affecte le résultat du cast à la variable x fraîchement créée, sinon, on passe au bloc “case” suivant. On peut être plus spécifiques, en définissant des intervalles, comme suit :

case int x when x > 0 : // si l'objet est positif, ce bloc sera exécuté
case int y : //si l'objet est inférieur ou égale à zéro, ce bloc sera exécuté

N.B : L’instruction “default” sera toujours évaluée en dernier, mais l’emplacement des autres blocs “case” est devenu important dans C# 7, car il influence le “flux du programme” (program flow) : il faut passer du plus spécifique au plus général.

public static void Dummy(object o)
{
    switch (o)
    {
        default:
        Console.WriteLine("Type inconnu");
        break;

        //Le bloc suivant est toujours évalué à true 
        //et le reste du code inatteignable
        //Dans ce cas, le compilateur va générer une erreur

        //case object x:
        //Console.WriteLine("Type objet");
        //break;
        
        case int n when (n >= 100):
        Console.WriteLine($"entier supérieur à 100 : {n}");
        break;

        case int n when (n < 100 && n >= 50):
        Console.WriteLine($"entier entre 50 et 100 : {n}");
        break;

        case int n when (n < 50):
        Console.WriteLine($"entier inférieur à 50 : {n}");
        break;

        case 1:
        WriteLine("entier égal à 1");
        break;

        case int n:
        Console.WriteLine("entier");
        break;
    }
}

Conclusion

Vous pouvez télécharger le code source utilisé tout au long de cet artile sur Github.

J’espère que cet article vous a plu et que j’ai pu présenter d’une manière simple et claire les nouveautés du langage.
Je vous recommande de visiter la page du compilateur roslyn et suivre l’état d’avancement du développement des nouvelles fonctionnalités sur Github.

Alt text