Cet article est le deuxième d’une série sur les nouveautés C# 7. Retrouvez les autres :

Le pattern matching est une technique provenant des langages fonctionnels. D’après sa définition, elle a pour but de valider la présence de patterns dans une séquence. Une séquence, dans le monde fonctionnel, est représentée par des données en entrée. Dans le monde objet, la séquence est une instance d’une classe. Pour implémenter les fonctionnalités de pattern matching à C#, deux instructions ont évolué : is et switch.

Il y a 3 types de patterns avec lesquels nous pouvons valider une séquence :

  • des constantes
  • des types
  • des types inférés

Évolutions de l’instruction is

Partons d’un exemple que l’on peut assez facilement rencontrer :

public void DoSomething(IUser user)
{
    Reader reader = user as Reader;
    if (reader != null)
    {
        //Reader case
    }

    Writer writer = user as Writer;
    if (writer != null)
    {
        //Writer case
    }

    Administrator admin = user as Administrator;
    if (admin != null)
    {
        //Admin case
    }
}

Ce code est trop verbeux pour ce qu’il fait réellement. La déclaration des variables reader, writer et admin et les tests de nullité nuisent beaucoup à la lisibilité de ce code. Avec C# 7, nous pouvons écrire ceci :

public void DoSomething(IUser user)
{
    if (user is Reader reader)
    {
        //Reader case
    }

    if (user is Writer writer)
    {
        //Writer case
    }

    if (user is Administrator admin)
    {
        //Admin case
    }
}

Le problème a été résolu. L’instruction is permet de valider le type de user, et nous pouvons déclarer inline 3 variables reader, writer et admin. Voici quelques autres petites choses que nous pouvons maintenant écrire :

public void DoSomethingElse()
{
    int i = 6;

    if (i is 5)
    {
        //Shouldn't ever reach
    }
}

Ici, le mot-clé sert à tester une variable contre une constante, et cela se comportera exactement comme l’expression if (i == 5). Mais nous pouvons aussi mélanger les genres, sachant que la propriété IsActive n’existe que pour les administrateurs :

public void DoSomething(IUser user)
{
    if (user is Administrator admin && admin.IsActive)
    {
        //Do stuff of active admins
    }
}

Enfin, on peut écrire ce type de chose avec l’inférence de type. Le matching fonctionnera toujours, mais la newUser est un clône de user, donc pas de souci de référence.

public void DoSomething(IUser user)
{
    if (user is var newUser)
    {
        //Do stuff with the new user
    }
}

Evolutions de l’instruction switch

Jusqu’à C# 6, l’instruction switch était réservée aux types primitifs. On peut dorénavant switcher sur des patterns.

public static void DoSomething(IUser user)
{
    switch (user)
    {
        case Reader r:
            System.Console.WriteLine(r.GetType());
            break;
        case Writer w:
            System.Console.WriteLine(w.GetType());
            break;
        case Administrator a:
            System.Console.WriteLine(a.GetType());
            break;
    }
}

Là où ça devient réellement intéressant, c’est en couplant le case avec l’instruction when.

public static void DoSomething(IUser user)
{
    switch (user)
    {
        case Reader r when r.Name.StartsWith("A"):
            System.Console.WriteLine(r.GetType());
            break;
        case Writer w when w.Birthdate.AddYears(18) <= DateTime.Today:
            System.Console.WriteLine(w.GetType());
            break;
        case Administrator a:
            System.Console.WriteLine(a.GetType());
            break;
        case null:
            throw new ArgumentException("user");
        default:
            System.Console.WriteLine(user.GetType());
            break;
    }
}

Plusieurs choses à noter :

  • l’instruction when permet de filtrer le matching de façon lisible et compréhensible
  • on peut écrire “case null” de façon à gérer proprement les cas de nullité d’un argument ou d’une variable.
  • L’ordre des clauses a une importance

La mécanique de pattern matching de C# fait en sorte que l’on parcourt la liste des case de haut en bas. Dès qu’un pattern est reconnu, on sort. Faites bien attention à écrire vos instructions de la plus restrictive à la plus générale. Seule exception : le default sera forcément exécuté en dernier. Donc pour assurer la lisibilité de vos listes de patterns, placez bien le default en fin de liste.

Pour finir, voici un exemple d’utilisation de l’inférence de type mettant également en valeur l’importance de l’ordre des cases :

public static void FilterAge(int age)
{
    switch (age)
    {
        case var toddler when toddler <= 3:
            //is a toddler
            break;
        case var child when (child < 13):
            //is a teenager
            break;
        case var teenAger when (teenAger <= 19):
            //is a teenager
            break;
        default:
            //is an adult
            break;
    }
}

Si jamais le case teenager était placé en premier, aucun toddler ou child ne serait matché. Notons également l’emploi de parenthèses permettant de clarifier certaines situations.

Nous avons parcouru les premiers pas du pattern matching dans le langage C#. Ce sont donc des premiers pas. Mads Torgersen annonçait en août 2016 que d’autres patterns seraient progressivement ajoutés et que le champ d’utilisation serait élargi avec le temps.