C# - Utilisation de LINQ

LINQ (Language Integrated Query) est un composant intégré au .NET Framework depuis la version 3.5. Il ajoute aux langages utilisant le .NET Framework tels que le C#, le VB.NET et bien d’autres des méthodes permettant de manipuler les classes énumérables (les tableaux, les listes, les piles, les files, …) beaucoup plus facilement. Voici un exemple d’utilisation :

string[] names = new [] { "Amélie", "Bernard", "Benoit", "Pascal", "Xavier" };
foreach (string name in names.Where(n => n.StartsWith("B")))
    Console.WriteLine(n); // Affiche "Bernard" et "Benoit"

Comme vous pouvez le voir, avec une syntaxe particulièrement succinte et facilement compréhensible, il est possible d’effectuer des opérations d’habitude fastidieuses.


Les fonctions lambdas

Tout d’abord, pour comprendre correctement la syntaxe LINQ, il est nécessaire de savoir écrire des expressions lambda. Vous avez sûrement du remarquer l’expressionn => n.StartsWith("B") qui n’est pas très familière à première vue. Nous allons expliquer ces expressions dans un premier temps.

En C# et dans les autres langages .NET, les fonctions sont des objets, c’est-à-dire qu’il est tout à fait possible de passer des fonctions en paramètre, ou de les stocker dans une variable, comme ceci :

public int Square(int x)
{
    return x * x;
}
public void Main(params string[] args)
{
    var a = Square;
    Console.WriteLine(Square(10));
    Console.WriteLine(a(10));
}

Vous allez me dire "mais si les fonctions sont des variables, ça veut dire qu’elles ont aussi un type non ?". Exactement, il existe en fait deux types pour représenter les fonctions : Action et Func.

  • Le type Action correspond aux fonctions ne renvoyant pas de valeur, ce sont les méthodes.
  • Le type Func correspond donc aux autres fonctions.
Action et Func sont des types génériques afin de pouvoir spécifier le type de la valeur de retour et des paramètres, par exemple :

public int Square(int x)
{
    return x * x;
}
public void Main(params string[] args)
{
    Func<int, int> a = Square;
    Action<string[]> b = Main;
}

Il existe une autre notation pour écrire des fonctions, la notation lambda qui permet d’écrire une fonction dans votre code, tout comme un string ou encore un int. Cette notation est composée de deux parties distinctes séparées par le signe =>. La partie gauche correspond aux paramètres de la fonction et la partie droite à la valeur de retour de la fonction. Si la fonction ne possède aucun paramètre, on utilisera () pour la partie gauche. Si elle en comprend plusieurs, on utilisera cette notation : (a, b, c). Il est d’usage de n’utiliser uniquement des lettres afin de conserver la compacité du code et de facilité la compréhension.

Tout comme dans une fonction classique, on peut appeler les paramètres avec n’importe quel nom, mais avec une nouvelle règle, ce nom ne doit pas rentrer en conflit avec d’autres variables du bloc courant. Voici notre chère fonction Square écrite en notation lambda :

public void Main(params string[] args)
{
    Func<int, int> Square = x => x * x;
    Console.WriteLine(Square(10));
}

Voici quelques autres exemples pour vous familiariser avec cette notation :

Func<int> Rand = () => new Random().Next();
Action<string> Write = s => Console.WriteLine(s);
Func<int, bool> Odd = n => n % 2 == 1;


LINQ : La notation classique

Maintenant que nous comprenons et que nous pouvons écrire des expressions lambda, revenons à LINQ. Vous pouvez certainement déjà comprendre la signification du premier exemple que je vous ai montré. En effet, la fonction Where prend en paramètre une autre fonction. J’aurais pu écrire l’exemple sous cette forme équivalente, bien que beaucoup plus longue :

public bool StartWithB(string name)
{
    return name.StartsWith("B");
}
public void Main(params string[] args)
{
    string[] names = new [] { "Amélie", "Bernard", "Benoit", "Pascal", "Xavier" };
    foreach (string name in names.Where(StartWithB))
        Console.WriteLine(n);
}

Le fonctionnement de LINQ est simple, il ajoute donc des fonctions aux classes énumérables qui parcourent leurs éléments et applique une fonction sur chacun d’eux. Elle agit en fonction du résultat de la fonction donnée en paramètre. Voici d’autres exemples de fonctions LINQ et leur description :

  • OrderBy : Permet de trier les éléments d’une collection en utilisant le résultat de la fonction donnée en paramètre
  • First : Renvoi le premier élément de la collection où la fonction donnée en paramètre a retourné true
  • Count : Compte le nombre d’éléments où la fonction en paramètre a retourné true
  • Distinct : Retourne une collection ou les doublons ont été supprimés. La valeur utilisée pour le filtre est la valeur de retour de la fonction donnée en paramètre. Si aucune fonction n’est passée en paramètre, les éléments sont eux-mêmes utilisés comme valeur de filtre.
  • Select : Exécute la fonction donnée en paramètre sur chaque élément et renvoi une nouvelle collection contenant les résultats des fonctions.
Certaines fonction LINQ (notament Where ou OrderBy renvoient eux-même des collections, permettant d’enchainer les appels aux fonction LINQ. Voici quelques examples d’utilisaiton des fonctions citées ci-dessus (en utilisant la collection names) :

// Tri des noms selon la deuxième lettre
names.OrderBy(n => n[1]); // Renverra "Pascal", "Xavier", "Bernard", "Benoit", "Amélie" 

// Idem, puis recuperation du premier élément contenant "er"
names.OrderBy(n => n[1]).First(n => n.Contains("er")); // Renverra "Xavier"

// Décompte des noms commencant par la lettre B
names.Count(n => n.StartsWith("B")); // Renverra 2

// Affichage de la liste des premières lettres des noms, sans doublons
names.Select(n => n.SubString(0, 1)).Distinct(); // Renverra A B P X


LINQ : La notation proche du SQL

Il existe une seconde notation pour utiliser les fonctions fournies par LINQ. Vous avez certainement du remarquer qu’une grande partie des fonctions LINQ sont inspirées du langage SQL, qui s’avère très efficace sur la gestion des bases de données (comparables aux collections). Depuis le .NET Framework 3.5, il est possible d’utiliser une syntaxe similaire au SQL dans nos programmes C#, voici un exemple et son équivalent en notation classique :

// Tri inverse des noms selon la troisième lettre et affichage des trois premières lettres
from n in names
where n.StartsWith("B")
orderby n[2] descending
select n.Substring(0, 3); // Renverra "Ber" et "Ben"

// Notation classique
names.Where(n => n.StartsWith("B")).OrderByDescending(n => n[1]).Select(n => n.SubString(0, 3));

Effectivement, on remarque directement la ressemblance avec le langage SQL. À noter que cette notation ne peut être utilisée que pour renvoyer des collections, pour remplacer des fonctions comme Where, OrderBy, Select.

On peut décomposer cette notation en trois blocs :

  • Le bloc « from » :from n in names. On spécifie ici la collection sur laquelle on souhaite travailler et le nom de la variable utilisée pour représenter un élément de la collection. La syntaxe est similaire au foreach.
  • Les blocs des filtres :where n.StartsWith("B"). On peut utiliser plusieurs filtres tels que where, orderby. On utilise la variable définie ci-dessus pour définir les filtres.
    • Le filtre where attend une valeur booléene comme paramètre. Il remplace la fonction Where
    • Avec le filtre orderby, on peut spécifier des mots clés tels que ascending ou descending pour préciser le sens du tri. Ce filtre remplace les fonctions OrderBy et OrderByDescending
  • En enfin « select » :select n.Substring(0, 3) où l’on spécifie la valeur qui sera renvoyée. C’est l’équivalent de la fonction Select.

Intégration et fonctions LINQ

Il est bon de noter que certains types sont nativement des collections. Par exemple le type string est une collection de char, il est donc possible d’utiliser les fonctions LINQ sur une chaine de caractère.

Voici une liste des fonctions fournies par LINQ les plus utilisées et des exemple pour comprendre plus aisément leur fonctionnement (les exemples ci-dessous utilisent la collection names définie plus haut) :

  • Select : Exécute la fonction donnée en paramètre sur chaque élément et renvoi une nouvelle collection contenant les résultats des fonctions.
  • SelectMany : Semblable à la fonction Select, mis à part que les résultats de la fonction passée en paramètre doivent être des collections. Ces résultats seront à leur tour fusionnés en une grande collection.

// Récupération des trois premières lettres de chaque nom
names.Select(n => n.SubString(0, 3)) // Renverra "Amé", "Ber", "Ben", "Pas", "Xav"

// Récupération de la liste des lettres utilisées dans chaque noms
// Chaque string étant une collection de char, le SelectMany récupère directement la liste des lettres des noms
names.SelectMany(n => n.ToLower()).Distinct().OrderBy(l => l); // Renverra a, b, c, d, e, i, l, m, n, o, p, r, s, t, v, x, é</code>

  • Sum : Renvoi la somme des résultats de la fonction passée en paramètre
  • Min, Max, Average : Renvoi le minimum, maximum ou la moyenne des résultats de la fonction passée en paramètre
  • Count : Renvoi le nombre d’éléments de la collection

// Taille totale des noms
names.Sum(n => n.Length) // Renverra 31

// Taille maximale des noms
names.Max(n => n.Length) // Renverra 7</code>

  • Take, Skip : Prend ou enlève les N premiers éléments d’une collection

names.Skip(3) // Renverra "Pascal", "Xavier"
names.Take(2) // Renverra "Amélie", "Bernard"

  • First, Last : Renvoi le premier ou le dernier élément de la collection
  • ElementAt : Renvoi le n-ième élément de la collection

names.First() // Renverra "Amélie"
names.ElementAt(3) // Renverra "Benoit"</code>

  • OrderBy : Permet de trier les éléments d’une collection en utilisant le résultat de la fonction donnée en paramètre
  • OrderByDescending : Inverse de OrderBy
  • Reverse : Inverse l’ordre des éléments de la collection

names.Reverse() // Renverra "Xavier", "Pascal", "Benoit", "Bernard", "Amélie"

  • Where : Filtre les éléments de la collection en ne renvoyant que ceux ou le résultat de la fonction donnée en paramètre est true
  • Single : Retourne le seul élément de la collection ou le résultat de la fonction donnée en paramètre est true. Si plusieurs résultats renvoient true, une erreur est envoyée.
  • Distinct : Retourne une collection ou les doublons ont été supprimés. La valeur utilisée pour le filtre est la valeur de retour de la fonction donnée en paramètre. Si aucune fonction n’est passée en paramètre, les éléments sont eux-mêmes utilisés comme valeur de filtre.

names.Single(n => n.StartsWith("X")) // Renverra "Xavier"</code>


Extensions LINQ

LINQ est un composant fournissant des fonctions supplémentaires pour les collections. Une collection étant un ensemble d’éléments, il est tout à fait possible de construire des collections personnalisées. En suivant ce principe, de nombreuses extensions pour LINQ ont vues le jour.

Par exemple, les bases de données peuvent être considérées comme des collections. Des extensions somme DbLinq ou DotConnect ont été développées. Elles permettent d’interroger directement la base de données avec les fonctions LINQ.

Dans le même esprit, de nombreux services peuvent être considérés comme des collections : pensez à Google, ou encore Twitter. En effet, il existe aussi des connecteurs LINQ pour ces services : Linq2Twitter, GLinq. Voici une liste de quelques services disposant de connecteurs pour vous donner des idées :

  • MySQL, Ingres, Oracle, PostgresSQL, SQL Server, SQLite
  • Recherche Windows (recherche parmi les fichiers et documents)
  • Google
  • CSV
  • Twitter
  • Wikipedia



// Restons en contact

Si vous êtes intéressés par mon travail ou mes projets, vous pouvez me suivre et me contacter sur les réseaux suivants :

// Statistiques

Actualités
3
Articles
5
Projets
16