Tag Archives: C# 7

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).

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.