Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Linq au quotidien, petites astuces et utilisations détournées...

Linq je vous en parle souvent, c'est tellement génial. Et ce que j'aime bien c'est trouver des utilisations presque à contre-sens de Linq.
J'entends par là des façons de "rentabiliser" Linq à toute autre tâche que d'interroger des données (SQL, XML ou autres).

Je ne suis pas le seul dans ce cas ! J'ai au moins trouvé un autre zigoto qui s'amuse au même jeu, Igor Ostrovsky dont vous pouvez visiter le blog (cliquez sur son nom). Comme il s'exprime en anglais et que je sais que certains visiteurs ici ne sont pas forcément doués pour cette langue, voici une adaptation libre(*) de son billet présentant quelques astuces très amusantes.
(*) je n'ai repris que les exemples de code, le texte et le projet exemple sont de mon cru, si vous parlez anglais je vous laisse découvrir la prose de Igor sur son blog.

Initialiser un tableau, une liste

Il est souvent nécessaire d'initialiser une array avec des éléments qui, pour que cela soit utile, répondent à certains critères. Normalement cela se règle soit sous la forme d'une liste de constantes dans la déclaration de l'array, ce qui est pour le moins assez peu souple, soit sous la forme d'un code d'initialisation, ce qui du coup devient assez lourd pour parfois pas grand chose... C'est oublier que Linq peut apporter ici une aide non négligeable !

Voici trois exemples :

int[] A = Enumerable.Repeat(-1, 10).ToArray();
int[] B = Enumerable.Range(0, 10).ToArray();
int[] C = Enumerable.Range(0, 10).Select(i => 100 + 10 * i).ToArray();

Les trois arrays contiendront 10 éléments. "A" contiendra 10 éléments tous égaux à -1. "B" contiendra la suite 0 à 9, et "C" contiendra la suite de 100 à 190 par pas de 10.

Amusant, n'est-il pas ?

Plusieurs itérations en une seule

Imaginons que nous possédions deux arrays, A1 et B2, chacune contenant une sélection différente d'éléments. Supposons que souhaitions appliquer le traitement "Calcule(x)" aux éléments de ces deux arrays. La solution s'imposant serait :

foreach (var x in A1) { Calcule(x); }
foreach (var x in A2) { Calcule(x); }

Ce n'est pas très long, mais répéter du code n'est jamais très élégant. Voici comme Linq permet de faire la même chose en une seule boucle :

foreach (var x in A1.Concat(A2)) { Calcule(x); }

L'opérateur Concat effectue une .. concaténation, donnant l'illusion à notre boucle qu'elle travaille sur une seule et même array alors qu'il n'en est rien... On notera que Linq travaillant au niveau des énumérateurs, il n'y même pas de variable temporaire créée par le mécanisme pour stocker la concaténation. C'est élégant et en plus c'est efficace du point de vue de la mémoire.

Fun, isn't it ?

Créer des séquences aléatoires

Il est parfois nécessaire de créer des séquences aléatoires, pour tester un bout de code par exemple. La classe Random est bien pratique mais encore une fois il faudra déclarer le tableau puis déclarer une méthode pour l'initialiser en utilisant Random. Avec Linq tout cela peut se réduire à deux déclarations :

Random rand = new Random();
var ListeAléatoire = Enumerable.Repeat(0, 10).Select(x => rand.Next(10,500));

Le tableau "ListeAléatoire" contiendra 10 nombres aléatoires entre 10 et 500, et pas une seule déclaration de méthode à l'horizon. Vous noterez en revanche l'utilisation qui est faite de l'expression Lambda (nouveauté de C# 3.0) au niveau du Select.

Créer des chaînes de caractères

Si les exemples portent jusqu'ici sur des tableaux de nombres, c'est par pure souci de simplification. Il est bien entendu possible d'appliquer les mêmes astuces à d'autres types de données comme les chaînes :

 string chaine = new string( Enumerable.Range(0, 20) .Select(i => (char)(‘A’ + i % 3)) .ToArray());

La chaîne "chaine" contiendra "ABCABCABCABCABCABCAB". Remplacez le "20" par un "rand.Next(10,100)" par exemple, et en plus la longueur de la chaîne sera elle-même aléatoire. C'est pas pratique ça ?

Dans les commentaires du blog de Igor, un lecteur proposait une autre façon d'initialiser une chaîne avec Linq, la voici pour l'intérêt de la démonstration de la versatilité de Linq :

string chaine2 = string.Join(string.Empty, Enumerable.Repeat("ABC", 7).ToArray());

Ici on peut passer une pattern et un nombre de répétition.

Convertir une valeur en séquence de 1 objet

Il est parfois nécessaire de convertir une valeur en une séquence de longueur 1. Passer un paramètre unique là où un tableau de valeur est attendu réclame de créer le fameux tableau et de l'initialiser. Rien de bien méchant mais pourquoi ne pas mettre Linq à contribution ?

IEnumerable<int> sequence = Enumerable.Repeat(LaValeur, 1);

La répétition de 1 élément peut sembler être une sorte de contresens, mais si les concepteurs de la fonction partageaient cette opinion n'auraient-ils pas levé une exception pour toute valeur inférieure à 2 ... :-)
C'est en tout cas un moyen bien pratique d'arriver au résultat souhaité en une seule opération !

Enumérer tous les sous-ensembles d'un tableau

Il s'agit plus ici d'un "plaisir pervers" que d'une réelle utilité quotidienne il faut l'avouer, mais pour l'intérêt de la syntaxe voici comment énumer tous les sous ensembles d'un tableau (attention, si la taille du tableau dépasse quelques dizaines d'éléments le temps de calcul peut vite exploser ...) :

T[] tableau = …;
var subsets = from m in Enumerable.Range(0, 1 << tableau.Length)
              select
                  from
i in Enumerable.Range(0, tableau.Length)
                  where (m & (1 << i)) != 0
                  select arr[i];

Je vous laisse réfléchir à la syntaxe, peu évidente de prime abord, et le côté un peu "tordu" de cette astuce, mais elle répond à un besoin qui l'est tout autant. Et Linq peut aussi servir à cela.
Quelques clés pour comprendre : la requête ne retourne pas des items de "tableau" mais bien des sous ensembles de "tableau", c'est à dire des tableaux. Chaque tableau contient une combinaison unique d'éléments de "tableau". Si vous prenez tableau = B de notre premier exemple (c'est à dire la suite d'entiers 0 à 9), la requete Linq ci-dessus retournera 1024 sous-ensembles. Avouez que bien qu'un peu difficile à comprendre à première lecture, le procédé est d'une grande élégance... (Vous pouvez regarder le fonctionnement de cette requête de plus près grâce au projet démo téléchargeable en fin d'article).

Un peu de code...

Pour mieux comprendre les choses il est toujours préférable de faire tourner un bout de code. J'y ai pensé ! Vous trouverez un projet console complet plus bas ;-)

Conclusion

Igor montre au travers de ces différentes utilisations de Linq ce que je tente aussi de vous montrer dans mes billets : Linq est d'une grande souplesse, Linq ne se limite pas à faire du SQL en C#, Linq est un atout majeur pour le développeur qui sait s'en servir au quotidien. Initialiser un tableau, convertir un tableau dans un type différent, jouer sur les combinatoires, fabriquer des chaînes de caractères ou des séquences aléatoires de test, etc, etc. Il n'y a pas un bout de code dans lequel il ne soit possible de glisser un peu de Linq pour le rendre plus sobre, plus élégant et souvent plus performant.
Remercions Igor pour ces exemples de code (seule chose reprise de son billet, j'ai mon propre style, il a le sien :-) )

Le code complet

LinqFun.rar (99,18 kb)

Bonus 

Mitsu, qu'on ne présente plus, ça serait faire insulte à sa notoriété !, a proposé il y a quelques temps déjà une autre astuce Linq tout à fait amusante, elle consiste à obtenir la décomposition d'un nombre en facteurs premiers au travers de requêtes Linq et d'un class helper permettant d'écrire : 144.ToPrimeNumbersProduct() et d'obtenir en sortie "2^3 * 3^2 * 7". Je vous en conseille la lecture en suivant ce lien.

 

blog comments powered by Disqus