Déconstruction de tuples en C# 7

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

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

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

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

Très mauvaisMauvaisMoyenBonExcellent (1 votes) 
Loading...

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

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

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

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

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.

Posted in Uncategorized. Tags: , , . No Comments »

Injecter automatiquement des fakes dans une test fixture avec FakeItEasy

Très mauvaisMauvaisMoyenBonExcellent (1 votes) 
Loading...

Aujourd’hui j’aimerais partager avec vous une fonctionnalité sympa de FakeItEasy que j’ai découverte récemment.

Quand on écrit des tests unitaires pour une classe qui a des dépendances, on a généralement besoin de créer des objets factices (fakes) pour les dépendances et de les injecter manuellement dans la classe testée (SUT, System Under Test), ou d’utiliser un conteneur IoC pour enregistrer les dépendances factices et construire le SUT. C’est un peu laborieux, et il y a quelques mois j’avais créé une extension Unity pour créer automatiquement les fakes et rendre tout ça plus facile. Mais maintenant je réalise que FakeItEasy offre une solution encore meilleure : déclarer simplement les dépendances et le SUT comme des propriétés de la fixture, et appeler Fake.InitializeFixture sur la fixture pour les initialiser. Voilà à quoi ça ressemble :

public class BlogManagerTests
{
    [Fake] public IBlogPostRepository BlogPostRepository { get; set; }
    [Fake] public ISocialNetworkNotifier SocialNetworkNotifier { get; set; }
    [Fake] public ITimeService TimeService { get; set; }
 
    [UnderTest] public BlogManager BlogManager { get; set; }
 
    public BlogManagerTests()
    {
        Fake.InitializeFixture(this);
    }
 
    [Fact]
    public void NewPost_should_add_blog_post_to_repository()
    {
        var post = A.Dummy<BlogPost>();
 
        BlogManager.NewPost(post);
 
        A.CallTo(() => BlogPostRepository.Add(post)).MustHaveHappened();
    }
 
    [Fact]
    public void PublishPost_should_update_post_in_repository_and_publish_link_to_social_networks()
    {
        var publishDate = DateTimeOffset.Now;
        A.CallTo(() => TimeService.Now).Returns(publishDate);
 
        var post = A.Dummy<BlogPost>();
 
        BlogManager.PublishPost(post);
 
        Assert.Equal(BlogPostStatus.Published, post.Status);
        Assert.Equal(publishDate, post.PublishDate);
 
        A.CallTo(() => BlogPostRepository.Update(post)).MustHaveHappened();
        A.CallTo(() => SocialNetworkNotifier.PublishLink(post)).MustHaveHappened();
    }
}

Le SUT est déclaré comme une propriété avec l’attribut [UnderTest]. Chaque dépendance qu’on a besoin de manipuler est déclarée comme une propriété avec l’attribut [Fake]. La méthode Fake.InitializeFixture initialise le SUT, en créant les dépendances factices à la volée, et affecte ces dépendances aux propriétés correspondantes.

J’aime beaucoup le fait que les tests semblent beaucoup plus propres avec cette technique ; le code de plomberie est réduit au strict minimum, il ne reste plus qu’à configurer les dépendances et écrire les tests proprement dits.

Quelques remarques sur le code ci-dessus :

  • On peut utiliser des champs privés plutôt que des propriétés publiques pour les fakes et le SUT, mais comme ces champs ne sont jamais explicitement assignés dans le code, cela provoque un avertissement du compilateur (CS0649), donc je préfère utiliser des propriétés.
  • Les tests dans mon exemple utilisent xUnit, j’ai donc mis l’appel à Fake.InitializeFixture dans le constructeur, mais si vous utilisez un autre framework comme NUnit ou MSTest, vous pouvez le mettre dans la méthode Setup.

Notez aussi qu’il y a des limites aux scénarios supportés par cette approche :

  • seule l’injection par constructeur est supportée, pas l’injection par propriétés (autrement dit les dépendances doivent être des paramètres du constructeur du SUT)
  • les dépendances nommées ne sont pas supportées ; seul le type est pris en compte, on ne peut donc pas avoir plusieurs dépendances distinctes du même type
  • les collections de dépendances ne sont pas supportées (c’est à dire si votre SUT reçoit une collection de dépendances, par exemple IFooService[])

Utiliser plusieurs sources d’annulation avec CreateLinkedTokenSource

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

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 !

[WPF] Empêcher l’utilisateur de coller une image dans un RichTextBox

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

Le contrôle RichTextBox de WPF est assez puissant, et très pratique quand on a besoin d’accepter une saisie en texte riche. Cependant, l’une de ses fonctionnalités peut devenir problématique : l’utilisateur peut coller une image. Selon ce qu’on veut faire du texte saisi par l’utilisateur, ce n’est pas forcément souhaitable.

Quand j’ai cherché sur le web un moyen d’empêcher cela, les seules solutions que j’ai trouvées suggéraient d’intercepter la frappe de touches Ctrl-V, et de bloquer l’événement si le presse-papiers contient une image. Cette approche présente plusieurs problèmes:

  • elle n’empêche pas l’utilisateur de coller via le menu contextuel
  • elle ne fonctionne pas si le raccourci de la commande a été modifié
  • elle n’empêche pas l’utilisateur d’insérer une image par glisser-déposer

Puisque cette solution ne me convenait pas, j’ai utilisé le site .NET Framework Reference Source pour chercher un moyen d’intercepter l’opération de collage elle-même. J’ai suivi le code à partir de la propriété ApplicationCommands.Paste, et j’ai finalement trouvé l’événement attaché DataObject.Pasting. Ce n’est pas un endroit où j’aurais pensé à chercher, mais quand on y réfléchit, c’est finalement assez logique. Cet événement peut être utilisé pour intercepter une opération de collage ou de glisser-déposer, et permet au gestionnaire de l’événement de faire différentes choses:

  • annuler purement et simplement l’opération
  • changer le format de données qui sera collé depuis le presse-papiers
  • remplacer le DataObject utilisé pour le collage

Dans mon cas, je voulais juste empêcher le collage ou le glisser-déposer d’une image, donc j’ai simplement annulé l’opération quand le FormatToApply était "Bitmap", comme illustré ci-dessous.

XAML:

<RichTextBox DataObject.Pasting="RichTextBox1_Pasting" ... />

Code-behind:

private void RichTextBox1_Pasting(object sender, DataObjectPastingEventArgs e)
{
    if (e.FormatToApply == "Bitmap")
    {
        e.CancelCommand();
    }
}

Bien sûr, il est également possible de gérer ça plus intelligemment. Par exemple, si le DataObject contient plusieurs formats, on pourrait créer un nouveau DataObject avec uniquement les formats acceptables. Comme ça l’utilisateur peut encore coller quelque chose, tant que ce n’est pas une image.

Posted in WPF. Tags: , , , , . No Comments »

Weak events en C#, suite

Très mauvaisMauvaisMoyenBonExcellent (1 votes) 
Loading...

Il y a quelques années, j’ai blogué à propos d’une implémentation générique du pattern “weak event” en C#. Le but était de pallier les problèmes de fuites mémoire liés aux évènements quand on oublie de s’en désabonner. L’implémentation était basée sur l’utilisation de références faibles sur les abonnés, de façon à éviter d’empêcher qu’ils soient libérés par le garbage collector.

Ma solution initiale était plus une preuve de concept qu’autre chose, et avait un sérieux problème de performance, dû à l’utilisation de DynamicInvoke à chaque fois que l’évènement était déclenché. Au fil des années, j’ai revisité le problème des “weak events” plusieurs fois, en apportant quelques améliorations à chaque fois, et j’ai maintenant une implémentation qui devrait être suffisamment performante pour la plupart des cas d’utilisation. L’API publique est similaire à celle de ma première solution. En gros, au lieu d’écrire un évènement comme ceci :

public event EventHandler<MyEventArgs> MyEvent;

On l’écrit comme ceci :

private readonly WeakEventSource<MyEventArgs> _myEventSource = new WeakEventSource<MyEventArgs>();
public event EventHandler<MyEventArgs> MyEvent
{
    add { _myEventSource.Subscribe(value); }
    remove { _myEventSource.Unsubscribe(value); }
}

Du point de vue de celui qui s’abonne à l’évènement, c’est exactement pareil qu’un évènement normal, mais l’abonné restera éligible à la garbage collection s’il n’est plus référencé nulle part ailleurs.

L’objet qui publie l’évènement peut le déclencher comme ceci :

_myEventSource.Raise(this, e);

Il y a une petite limitation : la signature de l’évènement doit être EventHandler<TEventArgs> (avec ce que vous voulez comme TEventArgs, bien sûr). Ca ne peut pas être quelque chose comme FooEventHandler, ou un type de délégué custom. Je ne pense pas que ce soit un problème majeur, dans la mesure où une vaste majorité des évènements dans le monde .NET respecte le pattern recommandé void (sender, args), et les delegates spécifiques comme FooEventHandler ont en fait la même signature que EventHandler<FooEventArgs>. J’avais d’abord essayé de supporter n’importe quel type de delegate, mais ça s’est avéré un peu trop compliqué… pour l’instant en tout cas Winking smile.

 

Comment ça marche?

La nouvelle solution est encore basée sur des références faibles, mais change la façon dont la méthode cible est appelée. Au lieu d’utiliser DynamicInvoke, on crée un “open-instance delegate” pour la méthode lors de l’abonnement. Cela signifie que pour une méthode ayant une signature comme void EventHandler(object sender, EventArgs e), on  crée un delegate avec la signature void OpenEventHandler(object target, object sender, EventArgs e). Le paramètre supplémentaire target représente l’instance sur laquelle la méthode est appelée. Pour invoquer le gestionnaire de l’évènement, il suffit de récupérer la cible à partir de la référence faible, et si elle est toujours vivante, de la passer au “open-instance delegate”.

Pour de meilleures performances, ce delegate est en fait créé seulement la première fois qu’on rencontre une méthode donnée, et est mis en cache pour être réutilisé ultérieurement. Ainsi, si plusieurs instances d’une classe s’abonnent à l’évènement avec la même méthode, le delegate ne sera créé que la première fois, et sera réutilisé pour les abonnés suivants.

Notez que techniquement, le delegate créé n’est pas un “vrai” open-instance delegate comme ceux créés par la méthode Delegate.CreateDelegate. Il est en fait créé à l’aide des expressions Linq. La raison est que dans un vrai open-instance delegate, le type du premier paramètre doit être le type qui déclare la méthode, et non object. Puisque cette information n’est pas disponible statiquement, il faut introduire un cast dynamiquement.

 

Le code source est disponible sur GitHub: WeakEvent. Un package NuGet est disponible ici : ThomasLevesque.WeakEvent.

Le dépôt GitHub contient aussi des snippets pour Visual Studio et pour ReSharper, pour faciliter l’écriture du code de plomberie pour un weak event.

Casse-tête C# n°2

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading...

Encore un petit casse-tête basé sur un problème que j’ai rencontré au boulot…

Regardez ce morceau de code :

Console.WriteLine($"x > y is {x > y}");
Console.WriteLine($"!(x <= y) is {!(x <= y)}");

Comment faudrait-il déclarer x et y pour que le programme produise la sortie (apparemment illogique) suivante ?

x > y is False
!(x <= y) is True
Posted in Casse-têtes. Tags: , . 5 Comments »
css.php