Category Archives: Uncategorized

Tout faire ou presque avec le pipeline de HttpClient

Il y a quelques années, Microsoft a introduit la classe HttpClient comme alternative moderne à HttpWebRequest pour faire des requêtes web depuis des applications .NET. Non seulement cette nouvelle API est beaucoup plus facile à utiliser, plus propre, et asynchrone, mais elle est aussi facilement extensible.

Vous avez peut-être remarqué que HttpClient a un constructeur qui accepte un HttpMessageHandler. De quoi s’agit-il ? Un HttpMessageHandler est un objet qui accepte une requête (HttpRequestMessage) et renvoie une réponse (HttpResponseMessage) ; la façon dont il le fait dépend complètement de l’implémentation. Par défaut, HttpClient utilise HttpClientHandler, un handler qui envoie une requête vers un serveur sur le réseau et renvoie la réponse du serveur. L’autre implémentation fournie de HttpMessageHandler est une classe abstraite nommée DelegatingHandler, et c’est de celle là que je voudrais parler.

Le pipeline

DelegatingHandler est un handler qui est conçu pour être chaîné à un autre handler, ce qui donne un pipeline à travers lequel les requêtes et réponses vont passer, comme illustré par ce schéma :

Schéma du pipeline de HttpClient

(Image tirée du site officiel ASP.NET)

Chaque handler a la possibilité d’examiner et/ou de modifier la requête avant de la passer au handler suivant, et d’examiner et/ou de modifier la réponse reçue du handler suivant. Habituellement, le dernier handler dans le pipeline est le HttpClientHandler, qui communique directement avec le réseau.

La chaîne de handlers peut être configurée comme ceci :

var pipeline = new MyHandler1()
{
    InnerHandler = new MyHandler2()
    {
        InnerHandler = new HttpClientHandler()
    }
};
var client = new HttpClient(pipeline);

Mais si vous préférez les interfaces “fluent”, il est facile de créer une méthode d’extension qui permet de le faire comme ceci :

var pipeline = new HttpClientHandler()
    .DecorateWith(new MyHandler2())
    .DecorateWith(new MyHandler1());
var client = new HttpClient(pipeline);

Tout ça semble peut-être un peu abstrait pour l’instant, mais cette architecture à base de pipeline rend possible plein de scénarios intéressants. En effet, ces handlers de messages HTTP peuvent être utilisés pour ajouter des comportements personnalisés au traitement des requêtes et réponses. Je vais en donner quelques exemples.

Remarque : Je présente cette fonctionnalité d’un point de vue client (vu que je développe essentiellement des applis clientes), mais le même système de handlers est également utilisé côté serveur dans ASP.NET Web API.

Tests unitaires

Le premier cas d’utilisation qui vient à l’esprit, et le premier que j’ai mis en oeuvre, c’est les tests unitaires. Si vous testez une classe qui fait des paiements en ligne via HTTP, vous ne voulez pas que vos tests envoient réellement des requêtes au vrai serveur… Vous voulez juste vous assurer que les requêtes envoyées sont correctes, et que le code réagit correctement à des réponses spécifiques. Une solution simple à ce problème est de créer un handler “stub”, et de l’injecter dans votre classe à la place de HttpClientHandler. Voici une implémentation possible :

class StubHandler : HttpMessageHandler
{
    // Responses to return
    private readonly Queue<HttpResponseMessage> _responses =
        new Queue<System.Net.Http.HttpResponseMessage>();

    // Requests that were sent via the handler
    private readonly List<HttpRequestMessage> _requests =
        new List<System.Net.Http.HttpRequestMessage>();

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (_responses.Count == 0)
            throw new InvalidOperationException("No response configured");

        _requests.Add(request);
        var response = _responses.Dequeue();
        return Task.FromResult(response);
    }

    public void QueueResponse(HttpResponseMessage response) =>
        _responses.Enqueue(response);

    public IEnumerable<HttpRequestMessage> GetRequests() =>
        _requests;
}

Cette classe permet d’enregister les requêtes qui sont envoyées via le handler et de spécifier les réponses qui doivent être renvoyées. Par exemple, on pourrait écrire un test comme celui-ci :

// Arrange
var handler = new StubHandler();
handler.EnqueueResponse(new HttpResponseMessage(HttpStatusCode.Unauthorized));
var processor = new PaymentProcessor(handler);

// Act
var paymentResult = await processor.ProcessPayment(new Payment());

// Assert
Assert.AreEqual(PaymentStatus.Failed, paymentResult.Status);

Bien sûr, plutôt que de créer un stub manuellement, il est possible d’utiliser un framework de mock pour générer un faux handler. Le fait que la méthode SendAsync soit protégée rend cette approche un peu moins facile qu’elle devrait l’être, mais on peut facilement contourner le problème en créant une classe dérivée qui expose une méthode publique virtuelle, et en faisant un mock de cette classe :

public abstract class MockableMessageHandler : HttpMessageHandler
{
    protected override sealed Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return DoSendAsync(request);
    }

    public abstract Task<HttpResponseMessage> DoSendAsync(HttpRequestMessage request);
}

Exemple d’utilisation avec FakeItEasy :

// Arrange
var handler = A.Fake<MockableMessageHandler>();
A.CallTo(() => handler.DoSendAsync(A<HttpRequestMessage>._))
    .Returns(new HttpResponseMessage(HttpStatusCode.Unauthorized));
var processor = new PaymentProcessor(handler);
...

Logging

Écrire dans le log les requêtes envoyées et les réponses reçues peut aider à diagnostiquer certains problèmes. C’est très facile à mettre en oeuvre avec un un DelegatingHandler personnalisé :

public class LoggingHandler : DelegatingHandler
{
    private readonly ILogger _logger;

    public LoggingHandler(ILogger logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _logger.Trace($"Request: {request}");
        try
        {
            // base.SendAsync calls the inner handler
            var response = await base.SendAsync(request, cancellationToken);
            _logger.Trace($"Response: {response}");
            return response;
        }
        catch (Exception ex)
        {
            _logger.Error($"Failed to get response: {ex}");
            throw;
        }
    }
}

Réessayer les requêtes échouées

Un autre cas d’utilisation intéressant des handlers de messages HTTP est de réessayer automatiquement les requêtes qui ont échouées. Par exemple, le serveur auquel on s’adresse peut être temporairement indisponible (503), il peut limiter nos requêtes (429), ou on peut tout simplement avoir perdu l’accès à internet. Dans toutes ces situations, réessayer la même requête plus tard a de bonnes chances de fonctionner (le serveur peut avoir redémarré, on peut avoir retrouvé du wifi…). Gérer la retentative au niveau du code applicatif est laborieux, car ça peut se produire pratiquement n’importe où. Avoir cette logique au plus bas niveau possible et implémentée d’une façon complètement transparente pour l’appelant rend les choses beaucoup plus simples.

Voici une implémentation possible d’un handler qui réessaie les requêtes échouées :

public class RetryHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        while (true)
        {
            try
            {
                // base.SendAsync calls the inner handler
                var response = await base.SendAsync(request, cancellationToken);

                if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
                {
                    // 503 Service Unavailable
                    // Wait a bit and try again later
                    await Task.Delay(5000, cancellationToken);
                    continue;
                }

                if (response.StatusCode == (HttpStatusCode)429)
                {
                    // 429 Too many requests
                    // Wait a bit and try again later
                    await Task.Delay(1000, cancellationToken);
                    continue;
                }

                // Not something we can retry, return the response as is
                return response;
            }
            catch (Exception ex) when(IsNetworkError(ex))
            {
                // Network error
                // Wait a bit and try again later
                await Task.Delay(2000, cancellationToken);
                continue;
            }
        }
    }

    private static bool IsNetworkError(Exception ex)
    {
        // Check if it's a network error
        if (ex is SocketException)
            return true;
        if (ex.InnerException != null)
            return IsNetworkError(ex.InnerException);
        return false;
    }
}

Remarquez que cette implémentation est un peu simpliste ; pour l’utiliser dans du code de production, il faudra sans doute ajouter un “exponential backoff” (attendre de plus en plus longtemps entre chaque tentative), prendre en compte l’en-tête Retry-After pour déterminer combien de temps il faut attendre, ou encore déterminer de façon un peu plus subtile si une exception correspond à une erreur réseau. De plus, en l’état actuel, ce handler réessaiera indéfiniment jusqu’à ce que la requête réussisse ; assurez-vous donc de passer un jeton d’annulation (CancellationToken) pour pouvoir l’arrêter si besoin.

Autres cas d’utilisations

Je ne peux pas donner d’exemples pour chaque scénario possible, mais voilà quelques autres cas d’utilisation possible des handlers de messages HTTP :

  • Gestion personnalisée des cookies (quelque chose que j’ai réellement fait, pour contourner un bug dans CookieContainer)
  • Authentification personnalisée (également quelque chose que j’ai fait, pour implémenter l’authentification OAuth2)
  • Utiliser l’en-tête X-HTTP-Method-Override pour passer les proxy qui refusent certaines méthode HTTP (voir l’article de Scott Hanselman pour plus de détails)
  • Chiffrement ou encodage personnalisé
  • Mise en cache

Comme vous le voyez, il y a tout un monde de possibilités. Si vous avez d’autres idées, indiquez les dans les commentaires !

Déconstruction de tuples en C# 7

Dans mon précedent billet, j’ai parlé d’une nouvelle fonctionnalité de C# 7 : les tuples. Dans Visual Studio 15 Preview 3, cette feature n’était pas tout à fait terminée ; il lui manquait 2 aspects importants :

  • la génération de métadonnées pour les noms des éléments des tuples, pour que les noms soient préservés entre les assemblies
  • la déconstruction des tuples en variables distinctes

Eh bien, il semble que l’équipe du langage C# n’a pas chômé au cours du mois écoulé, car ces deux éléments sont maintenant implémentés dans VS 15 Preview 4, qui a été publié hier ! Ils ont aussi rédigé des guides sur l’utilisation des tuples et de la déconstruction.

Il est maintenant possible d’écrire des choses comme ça :

var values = ...
var (count, sum) = Tally(values);
Console.WriteLine($"There are {count} values and their sum is {sum}");

(la méthode Tally est celle du précédent billet)

Remarquez que la variable t du billet précédent a disparu ; on affecte maintenant directement les variables count et sum à partir du résultat de la méthode, ce qui est à mon sens beaucoup plus élégant. Il ne semble pas y avoir de moyen d’ignorer une partie du tuple (c’est-à-dire ne pas l’affecter à une variable), mais peut-être cette possibilité viendra-t-elle plus tard.

Un aspect intéressant de la déconstruction est qu’elle n’est pas limitée aux tuples ; n’importe quel type peut être déconstruit, à condition d’avoir une méthode Deconstruct avec les paramètres out adéquats :

class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public void Deconstruct(out int x, out int y)
    {
        x = X;
        y = Y;
    }
}

...

var (x, y) = point;
Console.WriteLine($"Coordinates: ({x}, {y})");

La méthode Deconstruct peut également être une méthode d’extension, ce qui peut être utile si vous voulez déconstruire un type dont vous ne contrôlez pas le code. Les vieilles classes Sytem.Tuple, par exemple, peuvent être déconstruites à l’aide de méthodes d’extension comme celle-ci :

public static void Deconstruct<T1, T2>(this Tuple<T1, T2> tuple, out T1 item1, out T2 item2)
{
    item1 = tuple.Item1;
    item2 = tuple.Item2;
}

...

var tuple = Tuple.Create("foo", 42);
var (name, value) = tuple;
Console.WriteLine($"Name: {name}, Value = {value}");

Pour finir, les méthodes qui renvoient des tuples sont maintenant décorées d’un attribut [TupleElementNames] qui indique les noms des éléments du tuple :

// Code décompilé
[return: TupleElementNames(new[] { "count", "sum" })]
public static ValueTuple<int, double> Tally(IEnumerable<double> values)
{
   ...
}

(l’attribut est généré par le compilateur, vous n’avez pas besoin de l’écrire vous-même)

Cela permet de partager les noms des éléments du tuple entre les assemblies, et permet aux outils comme Intellisense de fournir des informations utiles sur la méthode.

L’implémentation des tuples en C# 7 semble donc à peu près terminée ; gardez cependant à l’esprit qu’il s’agit encore d’une preview, et que les choses peuvent encore changer d’ici la release finale.

Tuples en C# 7

Un tuple est une liste finie et ordonnée de valeurs, éventuellement de types différents, et est utilisé pour regrouper des valeurs liées entre elles sans avoir à créer une type spécifique pour les contenir.

.NET 4.0 a introduit un ensemble de classes Tuple , qui s’utilisent de la façon suivante

private static Tuple<int, double> Tally(IEnumerable<double> values)
{
	int count = 0;
	double sum = 0.0;
	foreach (var value in values)
	{
	    count++;
	    sum += value;
	}
	return Tuple.Create(count, sum);
}

...

var values = ...
var t = Tally(values);
Console.WriteLine($"There are {t.Item1} values and their sum is {t.Item2}");

Les classes Tuple ont deux principaux inconvénients:

  • Ce sont des classes, c’est-à-dire des types référence. Cela implique qu’elles doivent être allouées sur le tas, et collectées par le garbage collector quand elles ne sont plus utilisées. Pour les applications où les performances sont critiques, cela peut être problématique. De plus, le fait qu’elles puissent être nulles n’est souvent pas souhaitable.
  • Les éléments du tuple n’ont pas de noms, ou plutôt, ils ont toujours les mêmes noms (Item1, Item2, etc), qui ne sont pas du tout significatifs. Le type Tuple<T1, T2> ne communique absolument aucune information sur ce que le tuple représente réellement, ce qui en fait un mauvais candidat pour les APIs publiques.

En C# 7, une nouvelle fonctionnalité sera introduite pour améliorer le support des tuples : il sera possible de déclarer des types tuple “inline”, un peu comme des types anonymes, à part qu’ils ne sont pas limités à la méthode courante. En utilisant cette feature, le code précédent devient beaucoup plus clair:

static (int count, double sum) Tally(IEnumerable<double> values)
{
	int count = 0;
	double sum = 0.0;
	foreach (var value in values)
	{
	    count++;
	    sum += value;
	}
	return (count, sum);
}

...

var values = ...
var t = Tally(values);
Console.WriteLine($"There are {t.count} values and their sum is {t.sum}");

Notez comment le type de retour de Tally est déclaré, et comment le résultat est utilisé. C’est beaucoup mieux ! Les éléments du tuple ont maintenant des noms significatifs, et la syntaxe est plus agréable. Cette fonctionnalité repose sur un nouveau type ValueTuple<T1, T2>, qui est une structure et ne nécessite donc pas d’allocation sur le tas.

Vous pouvez essayer cette feature dès maintenant dans Visual Studio 15 Preview 3. Cependant, le type ValueTuple<T1, T2> ne fait pas (encore) partie du .NET Framework; pour faire fonctionner cet exemple, il faudra installer le package NuGet System.ValueTuple.

Enfin, une dernière remarque concernant les noms des membres du tuple : comme beaucoup d’autres fonctionnalités du langage, c’est juste du sucre syntaxique. Dans le code compilé, les membres du tuple sont référencés en tant que Item1 et Item2, et non count et sum. La méthode Tally renvoie en fait un ValueTuple<int, double>, et non un type spécialement généré.

Notez que le compilateur qui est livré avec VS 15 Preview 3 ne génère aucune métadonnée concernant les noms des membres du tuple. Cette partie de la feature n’est pas encore implémentée, mais devrait être incluse dans la version finale. Cela signifie qu’en attendant, on ne peut pas utiliser de tuples entre différents assemblies (enfin, on peut, mais en perdant les noms des membres, il faudra donc utiliser Item1 et Item2 pour y faire référence).

Piège: utiliser var et async ensemble

Il y a quelques jours au bureau, je suis tombé sur un bug assez sournois dans notre application principale. Le code semblait assez innocent, et à première vue je ne voyais vraiment pas ce qui n’allait pas… Le code était similaire à ceci:

public async Task<bool> BookExistsAsync(int id)
{
    var store = await GetBookStoreAsync();
    var book = store.GetBookByIdAsync(id);
    return book != null;
}

// Pour donner le contexte, voici les types et méthodes utilisés dans BookExistsAsync:

private Task<IBookStore> GetBookStoreAsync()
{
    // ...
}


public interface IBookStore
{
    Task<Book> GetBookByIdAsync(int id);
    // ...
}

public class Book
{
    public int Id { get; set; }
    // ...
}

La méthode BookExistsAsync renvoie toujours true. Voyez-vous pourquoi ?

Regardez cette ligne :

var book = store.GetBookByIdAsync(id);

Vite, sans réfléchir, quel est le type de book ? Si vous avez répondu Book, regardez de plus près : c’est Task<Book>. Il manque le await ! Et une méthode async renvoie toujours une tâche non nulle, donc book n’est jamais nul.

Quand vous avez une méthode async sans await, le compilateur vous avertit, mais en l’occurrence il y a un await sur la ligne précédente. La seule chose qu’on fait avec book est de vérifier qu’il n’est pas null ; puisque Task<T> est un type référence, il n’y rien de suspect dans le fait de le comparer à null. Donc le compilateur ne voit rien d’anormal ; l’analyseur statique de code (ReSharper dans mon cas) ne voit rien d’anormal ; et le pauvre cerveau humain qui fait la revue de code ne voit rien d’anormal non plus… Évidemment, le problème aurait facilement pu être détecté avec une couverture de tests adéquate, mais malheureusement cette méthode n’était pas couverte.

Alors, comment éviter ce type d’erreur ? En arrêtant d’utiliser var et en spécifiant toujours les types explicitement ? Mais j’aime var, je l’utilise presque partout ! D’ailleurs, je crois que c’est la première fois que je vois un bug lié à l’utilisation de var. Je ne suis vraiment pas prêt à l’abandonner…

Idéalement, j’aurais aimé que ReSharper détecte le problème; peut-être qu’il devrait considérer que toutes les méthodes qui renvoient une Task sont implicitement [NotNull], sauf mention contraire. En attendant, je n’ai pas de solution miracle pour ce problème ; faites juste attention quand vous appelez une méthode asynchrone, et écrivez des tests unitaires !

Publier un package sur NuGet.org depuis AppVeyor

Depuis quelques mois, j’utilise AppVeyor CI pour certains de mes projets open-source (avec Cake pour les scripts de build). J’en suis très content, mais il y avait un point qui m’ennuyait : je ne trouvais pas comment publier des packages sur NuGet.org directement depuis AppVeyor. Il fallait que je télécharge le package en local, puis que je l’uploade manuellement depuis ma machine (soit avec nuget push en ligne de commande, soit via le formulaire web sur NuGet.org), ce qui augmente considérablement les risques de mauvaise manipulation.

La solution s’est avérée assez simple en fin de compte, une fois que j’ai su où chercher. Je vais en décrire ici les étapes.

Configuration initiale

1. Configurer NuGet.org comme environnement de déploiement

Avant de pouvoir pousser des packages vers la galerie NuGet, il faut configurer NuGet.org comme environnement de déploiement (c’est une opération à faire une seule fois, à moins de vouloir publier des packages sous différentes identités).

Une fois connecté sur AppVeyor, allez sur la page Environments, cliquez sur New Environment, et sélectionnez NuGet comme fournisseur de déploiement :

appveyor_new_environment

Choisissez un nom pour l’environnement (par exemple NuGet.org), et saisissez votre clé d’API (non, ce n’est pas ma vraie clé sur la copie d’écran…). Vous pouvez trouver votre clé d’API NuGet sur votre page de compte quand vous êtes connecté sur NuGet.org. Laissez l’URL vide si vous voulez poussez sur la galerie NuGet officielle. Quand vous avez terminé, cliquez sur Add Environment.

2. Configurer les artéfacts dans votre projet

Dans les paramètres de votre projet, assurez-vous que les packages NuGet générés par votre processus de build sont configurés comme des artéfacts. Il suffit pour cela d’ajouter le chemin vers les fichiers .nupkg (le nom de déploiement est optionnel) :

appveyor_configure_artifacts

C’est tout ! Vous êtes maintenant prêt à publier.

Publier des packages

Lancez une nouvelle build, et quand il est terminé, cliquez sur le bouton Deploy pour cette build :

appveyor_deploy_button

Sélectionnez NuGet.org comme environnement de déploiement :

appveyor_deploy_to_nuget

Cliquez sur Deploy, et laissez la magie opérer. Si tout se passe bien, vous devriez voir quelque chose comme ça :

appveyor_deploy_result

Félicitations, votre package est maintenant sur la galerie NuGet. Si si, vous pouvez allez vérifier !

Essai des fonctionnalités de C# 7 dans Visual Studio “15” Preview

Il y a environ deux semaines, Microsoft a publié la première version préliminaire de la prochaine mouture de Visual Studio. Vous pourrez découvrir toutes les nouveautés qu’elle contient dans les notes de version. Il y a quelques nouveautés vraiment sympa (j’aime particulièrement le nouvel “installeur léger”), mais le plus intéressant pour moi est que le compilateur C# livré avec inclut quelques unes des fonctionnalités prévues pour C# 7. Regardons ça de plus près !

Activer les nouvelles fonctionnalités

Les nouvelles fonctionnalités ne sont pas activées par défaut. On peut les activer individuellement avec l’option /feature: en ligne de commande, mais le plus simple est de toutes les activer en ajoutant __DEMO__ et __DEMO_EXPERIMENTAL__ dans les symboles de compilation conditionnelle (Propriétés du projet, onglet Build).

Fonctions locales

La plupart des langages fonctionnels permettent de déclarer des fonctions dans le corps d’autres fonctions. Il est maintenant possible de faire la même chose en C# 7 ! La syntaxe pour déclarer une méthode dans une autre est sans grande surprise :

long Factorial(int n)
{
    long Fact(int i, long acc)
    {
        return i == 0 ? acc : Fact(i - 1, acc * i);
    }
    return Fact(n, 1);
}

Ici, la méthode Fact est locale à la méthode Factorial (au cas où vous vous posez la question, c’est une implémentation avec récursion terminale de la factorielle — ce qui n’a pas beaucoup de sens, vu que C# ne supporte pas la récursion terminale, mais bon, c’est juste un exemple…).

Bien sûr, il était déjà possible de simuler une fonction locale à l’aide d’une expression lambda, mais cela avait plusieurs inconvénients :

  • c’est moins lisible, car il faut déclarer explicitement le type du delegate ;
  • c’est plus lent, à cause du coût de création d’une instance de delegate, et de l’appel via le delegate ;
  • l’écriture d’expressions lambda récursives est peu intuitive.

Voici les principaux avantages des fonctions locales :

  • quand une méthode est utilisée seulement comme auxiliaire d’une autre méthode, la rendre locale permet de rendre cette relation plus explicite;
  • comme les lambdas, une fonction locale peut capturer les variables et paramètres de la méthode qui la contient;
  • les fonctions locales supportent la récursion comme n’importe quelle autre méthode

Pour en savoir plus sur les fonctions locales, allez voir sur le dépôt Github de Roslyn.

Valeurs de retour et variables locales par référence

Il est possible, depuis la première version de C#, de passer des paramètres par référence, ce qui est conceptuellement similaire au fait de passer un pointeur dans un langage comme C. Jusqu’ici, cette possibilité était limitée aux paramètres, mais C# 7 l’étend aux valeurs de retour et aux variables locales. Voici un exemple :

static void TestRefReturn()
{
    var foo = new Foo();
    Console.WriteLine(foo); // 0, 0
     
    foo.GetByRef("x") = 42;
 
    ref int y = ref foo.GetByRef("y");
    y = 99;
 
    Console.WriteLine(foo); // 42, 99
}
 
class Foo
{
    private int x;
    private int y;
 
    public ref int GetByRef(string name)
    {
        if (name == "x")
            return ref x;
        if (name == "y")
            return ref y;
        throw new ArgumentException(nameof(name));
    }
 
    public override string ToString() => $"{x},{y}";
}

Examinons ce code d’un peu plus près.

  • A la ligne 6, on dirait que j’affecte une valeur à une méthode. Qu’est-ce que ça peut bien vouloir dire ? Eh bien, la méthode GetByRef renvoie un champ de la classe Foo par référence (notez le type de retour ref int). Si je passe "x" en argument, elle renvoie le champ x par référence (et non une copie de la valeur du champ). Donc si j’affecte une valeur à cela, la valeur du champ x est également modifiée.
  • A la ligne 8, au lieu de simplement affecter une valeur directement au champ renvoyé par GetByRef, je passe par une variable locale y. Cette variable partage maintenant le même emplacement mémoire que le champ foo.y. Si j’affecte une valeur à y, cela modifie donc aussi foo.y.

Notez qu’il est également possible de renvoyer par référence un emplacement dans un tableau :

private MyBigStruct[] array = new MyBigStruct[10];
private int current;
 
public ref MyBigStruct GetCurrentItem()
{
    return ref array[current];
}

Il est probable que la plupart des développeurs C# n’auront jamais besoin de cette fonctionnalité; elle est assez bas-niveau, et ce n’est pas le genre de chose dont on a habituellement besoin quand on écrit des applications métier. Cependant, elle est très utile pour du code dont les performances sont critiques : copier une structure de grande taille est assez couteux, donc avoir la possibilité de la renvoyer plutôt par référence peut représenter un gain de performance non négligeable.

Pour en savoir plus sur cette fonctionnalité, rendez-vous sur Github.

Pattern matching

Le pattern matching (parfois traduit en “filtrage par motif”, mais je trouve cette traduction un peu bancale) est une fonctionnalité très courante dans les langages fonctionnels. C# 7 introduit certains aspects du pattern matching, sous forme d’extensions de l’opérateur is. Par exemple, quand on teste le type d’une variable, on peut maintenant introduire une nouvelle variable après le type, de façon à ce que cette variable reçoive la valeur de l’opérande de gauche, mais avec le type indiqué par l’opérande de droite (ce sera plus clair dans un instant avec un exemple).

Par exemple, si vous devez tester qu’une valeur est de type DateTime, puis faire quelque chose avec ce DateTime, il faut actuellement tester le type, puis convertir la valeur vers ce type :

object o = GetValue();
if (o is DateTime)
{
    var d = (DateTime)o;
    // Do something with d
}

En C# 7, on peut simplifier un peu :

object o = GetValue();
if (o is DateTime d)
{
    // Do something with d
}

d est maintenant déclarée directement dans l’expression o is DateTime.

Cette fonctionnalité peut aussi être utilisée dans un block switch :

object v = GetValue();
switch (v)
{
    case string s:
        Console.WriteLine($"{v} is a string of length {s.Length}");
        break;
    case int i:
        Console.WriteLine($"{v} is an {(i % 2 == 0 ? "even" : "odd")} int");
        break;
    default:
        Console.WriteLine($"{v} is something else");
        break;
}

Dans ce code, chaque case introduit une variable du type approprié, qu’on peut ensuite utiliser dans le corps du case.

Pour l’instant, je n’ai couvert que le pattern matching avec un simple test du type, mais il existe aussi des formes plus sophistiquées. Par exemple :

switch (DateTime.Today)
{
    case DateTime(*, 10, 31):
        Console.WriteLine("Happy Halloween!");
        break;
    case DateTime(var year, 7, 4) when year > 1776:
        Console.WriteLine("Happy Independance Day!");
        break;
    case DateTime { DayOfWeek is DayOfWeek.Saturday }:
    case DateTime { DayOfWeek is DayOfWeek.Sunday }:
        Console.WriteLine("Have a nice week-end!");
        break;
    default:
        break;
}

Plutôt cool, non ?

Le pattern matching existe aussi sous une autre forme (encore expérimentale), avec un nouveau mot-clé match :

object o = GetValue();
string description = o match
    (
        case string s : $"{o} is a string of length {s.Length}"
        case int i : $"{o} is an {(i % 2 == 0 ? "even" : "odd")} int"
        case * : $"{o} is something else"
    );

Cela ressemble beaucoup à un switch, sauf que c’est une expression et non une instruction.

Il y a encore beaucoup à dire sur le pattern matching, mais ça devient un peu long pour un article de blog, je vous invite donc à consulter la spec sur Github pour plus de détails.

Littéraux binaires et séparateurs de chiffres

Ces fonctionnalités n’étaient pas explicitement mentionnées dans les notes de version, mais j’ai remarqué qu’elles étaient inclues aussi. Elles étaient initialement prévues pour C# 6, mais avaient finalement été retirées avant la release ; elles sont de retour en C# 7.

Il est maintenant possible d’écrire des littéraux numériques sous forme binaire, en plus du décimal et de l’hexadécimal :

int x = 0b11001010;

Pratique pour définir des masques de bits !

De plus, pour rendre les grands nombres plus lisibles, on peut désormais grouper les chiffres en introduisant des séparateurs. Cela fonctionne avec les littéraux décimaux, hexadécimaux, et binaires :

int oneBillion = 1_000_000_000;
int foo = 0x7FFF_1234;
int bar = 0b1001_0110_1010_0101;

Conclusion

Visual Studio “15” Preview vous permet donc de commencer à expérimenter avec les nouvelles fonctionnalités de C# 7 ; n’hésitez pas à faire part de vos retours sur Github ! Et gardez à l’esprit que ce n’est qu’une version préliminaire, beaucoup de choses peuvent encore changer avant la version finale.

Utiliser plusieurs sources d’annulation avec CreateLinkedTokenSource

La programmation asynchrone en C# était auparavant quelque chose de difficile ; grâce à la Task Parallel Library de .NET 4 et au async/await de C# 5, elle est devenu relativement facile, et en conséquence, est de plus en plus couramment utilisée. Dans le même temps, une approche standardisée pour gérer l’annulation a été introduite : les jetons d’annulation. L’idée générale est que vous créez un CancellationTokenSource qui contrôle l’annulation, et vous passez le jeton qu’il fournit à la méthode que vous voulez pouvoir annuler. Cette méthode le passera ensuite aux autres méthodes qu’elle appelle si elles peuvent être annulées, et/où vérifiera régulièrement si l’annulation a été demandée. Lors de l’annulation, la méthode lancera une OperationCanceledException. Exemple vite fait mal fait :

private readonly IBusinessService _businessService;
private CancellationTokenSource _cancellationSource;
private Task _asyncOperation;
 
private void StartAsyncOperation()
{
    if (_asyncOperation != null)
        return;
    var _cancellationSource = new CancellationTokenSource();
    _asyncOperation = _businessService.DoSomethingAsync(_cancellationSource.Token);
}
 
// async void is bad; like I said, this is a quick and dirty example
private async void StopAsyncOperation()
{
    try
    {
        _cancellationSource.Cancel();
        // wait for the operation to finish
        await _asyncOperation;
    }
    catch (OperationCanceledException)
    {
        // Operation was successfully canceled
    }
    catch (Exception)
    {
        // Oops, something went wrong
    }
    finally
    {
        _asyncOperation = null;
        _cancellationSource.Dispose();
        _cancellationSource = null;
    }
 
...
 
class BusinessService : IBusinessService
{
    public async Task DoSomethingAsync(CancellationToken cancellationToken)
    {
        var data = await GetDataFromServerAsync(cancellationToken);
        foreach (string line in data)
        {
            cancellationToken.ThrowIfCancellationRequested();
            await ProcessLineAsync(line, cancellationToken);
        }
    }
 
    ...
}

Dans ce cas, StopAsyncOperation serait appelée, par exemple, si l’utilisateur décide d’annuler l’opération.

Tout ça marche très bien et est assez facile à mettre en œuvre. Mais que se passe-t-il s’il y a une autre raison d’annuler l’opération, connue seulement du BusinessService et hors du contrôle de la méthode appelante ? C’est là que la méthode CancellationSource.CreateLinkedTokenSource entre en jeu ; en gros, cette méthode crée une nouvelle source d’annulation qui sera annulée quand l’un des jetons spécifiés sera annulé.

Commençons par un cas simple : vous avez un autre jeton d’annulation que vous voulez aussi prendre en compte. Le code pourrait ressembler à ceci :

public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
    var otherToken = GetOtherCancellationToken();
    using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, otherToken))
    {
        var data = await GetDataFromServerAsync(linkedCts.Token);
        foreach (string line in data)
        {
            linkedCts.Token.ThrowIfCancellationRequested();
            await ProcessLineAsync(line, linkedCts.Token);
        }
    }
}

On crée une source d’annulation liée, basée sur les deux jetons d’annulation, et on utilise le jeton de cette nouvelle source à la place de cancellationToken. Si cancellationToken ou otherToken est annulé, linkedCts.Token sera annulé aussi. Si nécessaire, le code appelant peut détecter comment l’opération a été annulée en vérifiant la propriété CancellationToken de l’exception OperationCanceledException.

Maintenant, un cas un peu plus difficile : la seconde source d’annulation est en fait un évènement. On veut annuler l’opération quand cet évènement se produit, en plus de l’annulation par l’utilisateur représentée par le paramètre cancellationToken. On va donc s’abonner à l’évènement et déclencher l’annulation quand il se produit. Voilà un moyen de le faire :

public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
    using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
    {
        EventHandler handler = (sender, e) => linkedCts.Cancel();
        try
        {
            SomeEvent += handler;
            var data = await GetDataFromServerAsync(linkedCts.Token);
            foreach (string line in data)
            {
                linkedCts.Token.ThrowIfCancellationRequested();
                await ProcessLineAsync(line, linkedCts.Token);
            }
        }
        finally
        {
            SomeEvent -= handler;
        }
    }
}

Ici on passe seulement cancellationToken à CreateLinkedTokenSource, et on annule directement linkedCts quand l’évènement se produit. Le code devient un peu plus alambiqué, mais il atteint l’objectif.

Je ne peux pas vraiment vous donner un cas d’utilisation spécifique de cette technique dans le monde réel, parce que les cas où je l’ai utilisée sont trop spécifiques pour être d’intérêt général, mais je peux décrire le scénario général. J’ai une opération de longue durée qui est constituée de plusieurs sous-opérations de longue durée. L’opération entière peut être annulée globalement, et chacune des sous-opérations peut également être annulée individuellement, sans affecter les autres. Voilà à quoi ça peut ressembler (en quasi pseudo-code) :

async Task GlobalOperationAsync(CancellationToken cancellationToken)
{
    foreach (var subOperation is SubOperations)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var subToken = subOperation.GetSpecificCancellationToken();
        using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, subToken))
        {
            try
            {
                await subOperation.RunAsync(linkedCts.Token);
            }
            catch (OperationCanceledException ex)
            {
                // Rethrow only if global cancellation was requested
                if (cancellationToken.IsCancellationRequested)
                    throw;
                     
                // otherwise continue running the other sub-operations
            }
        }
    }
}

Remarquez que, bien que CancellationToken ait été introduit avec la TPL et que tous mes exemples soient asynchrones, vous pouvez également utiliser cette technique avec du code synchrone.

Voilà, j’espère que cela vous sera utile. Passez un bon réveillon du nouvel an, et une excellente année 2016 !

Filtres d’exception en C# 6 : leur plus grand avantage n’est pas celui qu’on croit

Les filtres d’exception sont l’une des fonctionnalités majeures de C# 6. Ils tirent parti d’une fonctionnalité du CLR qui a toujours existé, mais qui n’était pas exploitée en C# jusqu’ici. Ils permettent de spécifier une condition sur un bloc catch :

static void Main()
{
    try
    {
        Foo.DoSomethingThatMightFail(null);
    }
    catch (MyException ex) when (ex.Code == 42)
    {
        Console.WriteLine("Error 42 occurred");
    }
}

Comme on pourrait s’y attendre, le bloc catch ne sera exécuté que si ex.Code == 42. Si cette condition n’est pas vérifiée, l’exception sera propagée en remontant la pile jusqu’à ce qu’elle soit interceptée ailleurs ou qu’elle termine le processus.

A première vue, cela n’apporte rien de très nouveau. Après tout, on pouvait déjà faire ceci :

static void Main()
{
    try
    {
        Foo.DoSomethingThatMightFail(null);
    }
    catch (MyException ex)
    {
        if (ex.Code == 42)
            Console.WriteLine("Error 42 occurred");
        else
            throw;
    }
}

Puisque ce bout de code est équivalent au précédent, les filtres d’exception sont juste du sucre syntaxique… enfin, ils sont équivalents, non ?

NON !

Déroulement de pile

Il y a en fait une différence subtile mais importante : les filtres d’exception ne déroulent pas la pile. OK, mais qu’est-ce que ça veut dire ?

Quand on entre dans un bloc catch, la pile est déroulée : cela signifie que toutes les frames pour les appels de méthode plus “profonds” que la méthode courante sont dépilées. Cela implique que toutes les informations concernant l’état d’exécution de ces méthodes sont perdues, ce qui rend plus difficile de trouver la cause première de l’exception.

Supposons que la méthode DoSomethingThatMightFail lance une MyException avec le code 123, et que le débogueur soit configuré pour ne s’arrêter que sur les exceptions non gérées.

  • Dans le code sans filtre d’exception, on entre toujours dans le bloc catch (puisque le type de l’exception correspond), et la pile est immédiatement déroulée. Puisque l’exception ne satisfait pas la condition, elle est relancée. Le débogueur s’arrêtera donc sur le throw; dans le block catch ; aucune information sur l’état d’exécution de la méthode DoSomethingThatMightFail ne sera disponible. Autrement dit, on ne pourra pas savoir ce qui était en train de se passer dans la méthode qui a lancé l’exception.
  • En revanche, dans le code qui utilise un filtre d’exception, l’exception ne satisfait pas la condition, donc on n’entrera pas du tout dans le bloc catch, et la pile ne sera pas déroulée. Le débogueur s’arrêtera dans la méthode DoSomethingThatMightFail, ce qui permettra de voir facilement ce qui était en train de se passer quand l’exception a été lancée.

Bien sûr, quand on débogue directement une application dans Visual Studio, on peut configurer le débogueur pour s’arrêter dès qu’une exception est lancée, qu’elle soit gérée ou non. Mais on n’a pas toujours cette possibilité ; par exemple, si on débogue un problème en production, on travaille plutôt sur un crash dump, donc le fait que la pile n’ait pas été déroulée devient très utile, puisque ça permet de voir ce qui était en train de se passer dans la méthode qui a lancé l’exception.

Pile vs. trace de pile

Vous avez peut-être remarqué que j’ai parlé plus haut de la pile (call stack), et non de la trace de pile (stack trace). Bien qu’on utilise souvent le terme “pile” pour faire référence à la trace de pile, ce sont deux choses bien différentes. La pile est une zone mémoire allouée à chaque thread qui contient des informations sur les méthodes en cours d’exécution : adresse de retour, arguments, et variables locales. La trace de pile est juste une chaine qui contient les noms des méthodes actuellement sur la pile (et l’emplacement dans ces méthodes, si les symboles de débogage sont disponibles). La propriété Exception.StackTrace contient la trace de la pile telle qu’elle était quand l’exception a été lancée, et n’est pas affectée quand la pile est déroulée ; si on relance l’exception avec throw;, elle n’est pas modifiée non plus. Elle n’est écrasée que si on relance l’exception avec throw ex;. La pile elle-même, en revanche, est déroulée quand on entre dans un bloc catch, comme décrit plus haut.

Effets de bord

il est intéressant de noter qu’un filtre d’exception peut contenir n’importe quelle expression qui renvoie un bool (enfin presque… on ne peut pas utiliser await par exemple). Cela peut être une condition logique, une propriété, un appel de méthode, etc. Techniquement, rien n’empêche de causer des effets de bord dans un filtre d’exception. Dans la plupart des cas, je déconseillerais vivement de faire ça, car ça peut causer des comportements très déroutants ; il peut devenir très difficile de comprendre dans quel ordre les choses sont exécutées. Cependant, il y a un scénario courant qui pourrait bénéficier d’effets de bord dans un filtre d’exception: le logging. On pourrait facilement créer une méthode qui log l’exception et renvoie false pour qu’on n’entre pas dans le bloc catch. Cela permettrait de logger les exception à la volée sans les gérer, et donc sans dérouler la pile:

try
{
    DoSomethingThatMightFail(s);
}
catch (Exception ex) when (Log(ex, "An error occurred"))
{
    // this catch block will never be reached
}
 
...
 
static bool Log(Exception ex, string message, params object[] args)
{
    Debug.Print(message, args);
    return false;
}

Conclusion

Comme avez pu le voir, les filtres d’exception ne sont pas juste du sucre syntaxique. Contrairement à la plupart des fonctionnalités de C# 6, ce n’est pas vraiment une fonctionnalité de “codage” (dans le sens où ça ne rend pas le code significativement plus clair), mais plutôt une fonctionnalité de “débogage”. Bien compris et utilisés, ils peuvent rendre beaucoup plus facile la résolution de problèmes dans le code.

Personnaliser l’interpolation de chaine avec C# 6

L’une des principales nouveautés de C# 6 est l’interpolation de chaines de caractères, qui permet d’écrire ce genre de chose :

string text = $"{p.Name} was born on {p.DateOfBirth:D}";

Un aspect peu connu de cette fonctionnalité est qu’une chaine interpolée peut être traitée soit comme un String, soit comme un IFormattable, selon le contexte. Quand elle est convertie en IFormattable, cela crée un objet FormattableString qui implémente l’interface et expose :

  • la chaine de format, avec les valeurs remplacées par des marqueurs numériques (compatible avec String.Format)
  • les valeurs pour les marqueurs

La méthode ToString() de cet objet appelle simplement String.Format(format, values). Mais il y a aussi une surcharge qui accepte un IFormatProvider, et c’est là que ça devient intéressant, parce que cela permet de personnaliser la façon dont les valeurs sont formatées. Il n’est peut-être pas évident de voir en quoi c’est utile, donc laissez moi vous montrer quelques exemples…

Spécifier la culture

Pendant la conception de la fonctionnalité d’interpolation de chaines, il y a eu un débat assez vif pour décider s’il fallait utiliser la culture courante ou la culture neutre (“invariant”) pour formater les valeurs; il y avait de bons arguments des deux côtés, mais au final il a été décidé d’utiliser la culture courante, par souci de cohérence avec String.Format et des APIs similaires qui utilisent la mise en forme composite. Utiliser la culture courante est pertinent quand on utilise l’interpolation de chaines pour construire des chaines qui seront affichée dans l’interface utilisateur ; mais il y a aussi des scénarios où on veut construire des chaines qui seront utilisées dans des APIs ou protocoles (URLs, requêtes SQL…), et dans ces cas là il faut généralement utiliser la culture neutre.

C# 6 fournit un moyen facile de faire cela, en tirant parti de la conversion en IFormattable. Il suffit de créer une méthode comme celle-ci :

static string Invariant(FormattableString formattable)
{
    return formattable.ToString(CultureInfo.InvariantCulture);
}

Et vous pouvez ensuite l’utiliser comme suit:

string text = Invariant($"{p.Name} was born on {p.DateOfBirth:D}");

Les valeurs dans la chaine interpolée seront formatées avec la culture neutre, et non plus avec la culture courante.

Construire des URLs

Voici un exemple plus avancé. L’interpolation de chaines est un moyen pratique de construire des URLs, mais si on inclut des chaines arbitraires dans l’URL, il faut prendre soin de les encoder pour ne pas avoir de caractères invalides dans l’URL. Un interpolateur de chaine personnalisé peut le faire pour nous; il faut juste créer un IFormatProvider personnalisé qui s’occupera d’encoder les valeurs. L’implémentation n’était pas évidente au premier abord, mais après quelques tâtonnements je suis arrivé à ceci :

class UrlFormatProvider : IFormatProvider
{
    private readonly UrlFormatter _formatter = new UrlFormatter();

    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter))
            return _formatter;
        return null;
    }

    class UrlFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg == null)
                return string.Empty;
            if (format == "r")
                return arg.ToString();
            return Uri.EscapeDataString(arg.ToString());
        }
    }
}

Ce formateur peut être utiliser comme ceci :

static string Url(FormattableString formattable)
{
    return formattable.ToString(new UrlFormatProvider());
}

...

string url = Url($"http://foobar/item/{id}/{name}");

Cela va correctement encoder les valeurs de id et name de façon à ce que l’’URL générée ne contienne que des caractères valides.

Aparté: Avez-vous remarqué le if (format == "r") ? C’est un spécificateur de format personnalisé qui indique que la valeur ne doit pas être encodé (“r” pour “raw”). Pour l’utiliser, il suffit de l’inclure dans la chaine de format comme ceci : {id:r}. Cela empêchera l’encodage de id.

Construire des requêtes SQL

On peut faire quelque chose de similaire pour les requêtes SQL. Bien sûr, intégrer des valeurs directement dans une requête est une mauvaise pratique bien connue, pour des raison de sécurité et de performance (il faut utiliser des requêtes paramétrées); mais pour un développement “à l’arrache”, ça peut parfois être utile. Et puis c’est une bonne illustration de cette fonctionnalité. Pour intégrer des valeurs dans une requête SQL, il faut :

  • encadrer les chaines entre des apostrophes, et échapper les apostrophes à l’intérieur des chaines en les doublant
  • formater les dates en fonction de ce que le SGBD attend (généralement MM/dd/yyyy)
  • formater les nombres selon la culture neutre
  • remplacer les valeurs nulles par le littéral NULL.

(il y a probablement d’autres choses à prendre en compte, mais ce sont les plus évidentes).

On peut utiliser la même approche que pour les URLs, et créer un SqlFormatProvider :

class SqlFormatProvider : IFormatProvider
{
    private readonly SqlFormatter _formatter = new SqlFormatter();

    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter))
            return _formatter;
        return null;
    }

    class SqlFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg == null)
                return "NULL";
            if (arg is string)
                return "'" + ((string)arg).Replace("'", "''") + "'";
            if (arg is DateTime)
                return "'" + ((DateTime)arg).ToString("MM/dd/yyyy") + "'";
            if (arg is IFormattable)
                return ((IFormattable)arg).ToString(format, CultureInfo.InvariantCulture);
            return arg.ToString();
        }
    }
}

On peut ensuite utiliser ce formateur comme ceci :

static string Sql(FormattableString formattable)
{
    return formattable.ToString(new SqlFormatProvider());
}

...

string sql = Sql($"insert into items(id, name, creationDate) values({id}, {name}, {DateTime.Now})");

De cette façon les valeurs seront correctement formatées pour générer une requête SQL valide.

Utiliser l’interpolation de chaines quand on cible des versions plus anciennes de .NET

Comme c’est souvent le cas avec les fonctionnalités du langage qui exploitent des types du .NET Framework, il est possible d’utiliser cette fonctionnalité avec des versions plus anciennes de .NET qui n’ont pas la classe FormattableString ; il suffit de créer la classe soi-même dans le namespace approprié. En fait, il y a en l’occurrence deux classes à implémenter : FormattableString et FormattableStringFactory. Jon Skeet était apparemment très pressé d’essayer, et il a déjà donné un exemple avec le code pour ces classes :

using System;

namespace System.Runtime.CompilerServices
{
    public class FormattableStringFactory
    {
        public static FormattableString Create(string messageFormat, params object[] args)
        {
            return new FormattableString(messageFormat, args);
        }

        public static FormattableString Create(string messageFormat, DateTime bad, params object[] args)
        {
            var realArgs = new object[args.Length + 1];
            realArgs[0] = "Please don't use DateTime";
            Array.Copy(args, 0, realArgs, 1, args.Length);
            return new FormattableString(messageFormat, realArgs);
        }
    }
}

namespace System
{
    public class FormattableString
    {
        private readonly string messageFormat;
        private readonly object[] args;

        public FormattableString(string messageFormat, object[] args)
        {
            this.messageFormat = messageFormat;
            this.args = args;
        }
        public override string ToString()
        {
            return string.Format(messageFormat, args);
        }
    }
}

C’est la même approche qui permettait d’utiliser Linq en ciblant .NET 2.0 (LinqBridge) ou les attributs d’infos de l’appelant quand on cible .NET 4.0 ou plus ancien. Bien sûr, ça nécessite quand même le compilateur C# 6 pour fonctionner…

Conclusion

La conversion de chaines interpolées en IFormattable avait déjà été mentionnée il y a quelque temps, mais n’était pas encore implémentée dans Visual Studio 2015 CTP 5. La CTP 6 qui vient d’être publiée embarque une nouvelle version du compilateur qui inclut cette fonctionnalité, vous pouvez donc commencer à jouer avec ! Cette fonctionnalité rend l’interpolation de chaine très flexible, et je suis sûr que les gens vont trouver toutes sortes de cas d’utilisation auxquels je n’avais pas pensé.

Vous pouvez trouver le code des exemples ci-dessus sur GitHub.

Optimiser ToArray et ToList en fournissant le nombre d’éléments

Les méthodes d’extension ToArray et ToList sont des moyens pratiques de matérialiser une séquence énumérable (par exemple une requête Linq). Cependant, quelque chose me chiffonne : ces deux méthodes sont très inefficaces si elles ne connaissent pas le nombre d’éléments dans la séquence (ce qui est presque toujours le cas quand on les utilise sur une requête Linq). Concentrons nous sur ToArray pour l’instant (ToList a quelques différences, mais le principe est essentiellement le même).

Pour faire simple, ToArray prend une séquence, et retourne un tableau qui contient tous les éléments de la séquence. Si la séquence implémente ICollection<T>, ToArray utilise la propriété Count pour allouer un tableau de la bonne taille, et copie les éléments dedans ; voici un exemple :

List<User> users = GetUsers();
User[] array = users.ToArray();

Dans ce scénario, ToArray est assez efficace. Maintenant, changeons ce code pour extraire les noms des utilisateurs :

List<User> users = GetUsers();
string[] array = users.Select(u => u.Name).ToArray();

Maintenant, l’argument de ToArray est un IEnumerable<User> renvoyé par Select. Il n’implémente pas ICollection<User>, donc ToArray ne connait pas le nombre d’éléments, et ne peut donc pas allouer un tableau de la bonne taille. Voilà donc ce qu’il fait :

  1. commencer par allouer un petit tableau (4 éléments dans l’implémentation actuelle)
  2. copier les éléments depuis la source vers le tableau jusqu’à ce que celui-ci soit plein
  3. s’il n’y a plus d’éléments dans la séquence, aller en 7
  4. sinon, allouer un nouveau tableau deux fois plus grand que le précédent
  5. copier les éléments de l’ancien tableau vers le nouveau
  6. répéter depuis l’étape 2
  7. si le tableau est plus long que le nombre d’éléments, le tronquer : allouer un nouveau tableau d’exactement la bonne taille, et y copier les éléments depuis le tableau précédent
  8. renvoyer le tableau

S’il n’y a que quelques éléments, tout ceci est assez indolore ; mais pour une très longue séquence, c’est très inefficace, à cause des nombreuses allocations et copies.

Ce qui est agaçant est que, bien souvent, on (le développeur) connait le nombre d’éléments de la source! Dans l’exemple ci-dessus, on n’utilise que Select, qui ne change pas le nombre d’éléments, donc on sait que c’est le même que dans la liste d’origine; mais ToArray ne le sait pas, car l’information a été perdue en cours de route. Si seulement on pouvait fournir cette information nous-mêmes…

Eh bien, c’est en fait très facile à faire : il suffit de créer une nouvelle méthode d’extension qui accepte le nombre d’éléments comme paramètre. Voilà à quoi elle pourrait ressembler :

public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source, int count)
{
    if (source == null) throw new ArgumentNullException("source");
    if (count < 0) throw new ArgumentOutOfRangeException("count");
    var array = new TSource[count];
    int i = 0;
    foreach (var item in source)
    {
        array[i++] = item;
    }
    return array;
}

On peut maintenant optimiser notre exemple précédent de la façon suivante :

List<User> users = GetUsers();
string[] array = users.Select(u => u.Name).ToArray(users.Count);

Notez que si vous spécifiez un nombre inférieur au nombre réel d’éléments dans la séquence, vous obtiendrez une IndexOutOfRangeException ; il est de votre responsabilité de fournir une information correcte à la méthode.

Et au final, qu’est-ce qu’on y gagne? D’après mes tests, cette version améliorée de ToArray est environ deux fois plus rapide que la version standard, pour une longue séquence (testé avec 1.000.000 éléments). Pas mal du tout !

Notez qu’on peut améliorer ToList de la même manière, en utilisant le constructeur de List<T> qui permet de spécifier la capacité initiale :

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source, int count)
{
    if (source == null) throw new ArgumentNullException("source");
    if (count < 0) throw new ArgumentOutOfRangeException("count");
    var list = new List<TSource>(count);
    foreach (var item in source)
    {
        list.Add(item);
    }
    return list;
}

Ici, le gain de performance n’est pas aussi important que pour ToArray (environ 25% au lieu de 50%), probablement parce que la liste n’a pas besoin d’être tronquée, mais il n’est pas négligeable.

Bien sûr, une optimisation similaire pourrait être faite pour ToDictionary, puisque la classe Dictionary<TKey, TValue> a aussi un constructeur qui permet de spécifier la capacité initiale.

Les méthodes ToArray et ToList améliorées sont disponibles dans ma librairie Linq.Extras, qui fournit également de nombreuses méthodes d’extension utiles pour travailler avec des séquences et collections.