LINQ, acronyme de Language INtegrated Query, se traduit par Requête Intégrée au Langage. Il offre un modèle unifié pour interroger des données provenant de diverses sources et formats, d'où le terme "intégré". N'introduisant pas de nouveau langage mais s'appuyant sur les structures existantes, il est qualifié d'"intégré au langage".
Principes Fondamentaux
Fonctionnellement, LINQ se divise en deux catégories :
- LINQ to Object : Interroge des collections en mémoire. Les requêtes sont compilées directement en code .NET pour exécution.
- LINQ to Provider : Interroge des sources de données personnalisées. Le développeur fournit un "Provider" spécifique à la source de données pour traduire et exécuter la requête. Des formats comme XML ou JSON peuvent servir de sources de données via des Providers dédiés, donnant naissance à des appellations comme LINQ to XML.
Syntaxiquement, LINQ se présente sous deux formes :
- Style SQL : La syntaxe ressemble à celle de SQL. Pour certaines requêtes complexes, ce style peut améliorer la clarté, notamment pour
SelectManyetJoin. Il offre une lisibilité supérieure mais ne prend pas en charge toutes les fonctions LINQ standard ni les fonctions personnalisées. Il s'agit essentiellement d'un sucre syntaxique. - Style Fonctionnel : Implémenté via des méthodes d'extension C#. Ces méthodes peuvent provenir de la bibliothèque standard ou être définies par l'utilisateur. C'est un style de programmation natif où le code compilé se résume à des appels de fonctions. Il prend en charge toutes les fonctions LINQ standard et personnalisées. Cependant, avec l'augmentation de la complexité des requêtes, sa lisibilité peut diminuer par rapport au style SQL.
LINQ to Object est souvent utilisé pour des requêtes de type ORM (Object-Relational Mapping). LINQ to XML est employé pour interroger des données structurées en XML. Pour utiliser LINQ, l'objet source doit implémenter IEnumerable. (Pour simplifier, le terme "collection" dans cet article fait référence aux objets IEnumerable, y compris les ICollection explicites). De plus, les requêtes LINQ sont fréquemment chaînées, où une opération sur un IEnumerable<T1> retourne un IEnumerable<T2>.
Une requête LINQ to Object ressemble à ceci :
var userIds = from user in users
where user.Name.Contains("Dupont")
select user.Id;
L'équivalent en utilisant les méthodes d'extension LINQ est :
var userIds = users
.Where(u => u.Name.Contains("Dupont"))
.Select(u => u.Id);
LINQ permet de définir des variables intermédiaires au sein d'une requête. Par exemple, pour trouver les nombres dont le carré dépasse la moyenne d'un tableau :
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var squaredNumbersAboveAverage = from number in numbers
let average = numbers.Average()
let squared = Math.Pow(number, 2)
where squared > average
select number;
// La moyenne est 4.5, et squaredNumbersAboveAverage contiendra { 3, 4, 5, 6, 7, 8, 9 }
La méthode Select accepte fréquemment un délégué Func<TSource, TResult>. Elle peut également accepter Func<TSource, int, TResult>, comme le montre cet exemple pour ajouter un numéro de ligne :
var collectionWithRowNumber = collection
.Select((item, index) => new { Item = item, RowNumber = index })
.ToList();
Voyons un exemple avec LINQ to XML. Considérons le fichier XML suivant :
<?xml version="1.0" encoding="utf-8" ?>
<Employees>
<Employee>
<EmpId>1</EmpId>
<Name>Liam</Name>
<Sex>Male</Sex>
</Employee>
<Employee>
<EmpId>2</EmpId>
<Name>Olivia</Name>
<Sex>Female</Sex>
</Employee>
</Employees>
Pour interroger les éléments Employee dont le nœud Sex est "Male" :
XElement employeesXml = XElement.Load("Employees.xml");
var maleEmployees = from emp in employeesXml.Elements("Employee")
where (string)emp.Element("Sex") == "Male"
select emp;
L'équivalent avec les méthodes d'extension :
var maleEmployees = employeesXml.Elements("Employee")
.Where(emp => (string)emp.Element("Sex") == "Male");
LINQ to XML simplifie grandement la manipulation des documents XML. Les fonctionnalités avancées méritent une exploration plus poussée.
LINQ propose de nombreuses méthodes puissantes. Nous allons en explorer quelques-unes qui simplifient des opérations potentiellement complexes.
Récupération d'Éléments : First, Last, Single et leurs variantes
First, FirstOrDefault, Last, LastOrDefault, Single et SingleOrDefault permettent de récupérer rapidement le premier ou le dernier élément d'une collection. Les versions sans OrDefault lèvent une expection si la collection est vide. Les variantes OrDefault retournent la valeur par défaut du type (null pour les types référence, 0 pour les int, etc.) dans ce cas. La différence majeure entre Single/SingleOrDefault et les autres réside dans leur exigence qu'il n'y ait qu'un seul élément correspondant. Si plusieurs éléments satisfont la condition, Single/SingleOrDefault lèvent une expection.
new[] { "apple", "banana" }.First(x => x.StartsWith("b")); // Retourne "banana"
new[] { "apple", "banana" }.First(x => x.StartsWith("c")); // Lève InvalidOperationException
new[] { "apple", "banana" }.FirstOrDefault(x => x.StartsWith("c")); // Retourne null
new[] { "apple", "banana" }.Single(x => x.StartsWith("b")); // Retourne "banana"
new[] { "apple", "banana" }.Single(x => x.StartsWith("c")); // Lève InvalidOperationException
new[] { "apple", "banana" }.SingleOrDefault(x => x.StartsWith("c")); // Retourne null
new[] { "apple", "apple" }.Single(); // Lève InvalidOperationException car plusieurs éléments
En pratique, utilisez Single/SingleOrDefault lorsque vous attendez un résultat unique (par exemple, la recherche d'un utilisateur par son numéro de téléphone). Pour les autres cas, privilégiez First/FirstOrDefault. Bien que FirstOrDefault puisse vérifier l'existence d'un élément, la méthode Any est plus performante pour cette seule vérification.
Différence d'Ensembles : Except
La méthode Except retourne les éléments du premier ensemble qui ne sont présents dans aucun des autres ensembles spécifiés. Elle élimine les doublons.
int[] ensemble1 = { 1, 2, 3, 4 };
int[] ensemble2 = { 0, 2, 3, 5 };
IEnumerable<int> difference = ensemble1.Except(ensemble2);
// difference contiendra { 1, 4 }
Pour les types personnalisés, Except nécessite que le type implémente l'interface IEquatable<T> pour définir la logique de comparaison. Alternativement, on peut fournir un IEqualityComparer<T>.
public class Utilisateur : IEquatable<Utilisateur>
{
public string Nom { get; set; }
public bool Equals(Utilisateur other)
{
return other != null && Nom == other.Nom;
}
public override int GetHashCode()
{
return Nom?.GetHashCode() ?? 0;
}
}
// ...
var liste1 = new List<Utilisateur> { new Utilisateur { Nom = "Alice" }, new Utilisateur { Nom = "Bob" } };
var liste2 = new List<Utilisateur> { new Utilisateur { Nom = "Bob" }, new Utilisateur { Nom = "Charlie" } };
var resultatExcept = liste1.Except(liste2);
// resultatExcept contiendra Utilisateur { Nom = "Alice" }
Aplatissement de Collections : SelectMany
SelectMany est utilisé pour aplatir des collections multidimensionnelles en une seule collection unidimensionnelle.
var matrice = new int[][]
{
new int[] {1, 2, 3},
new int[] {4, 5, 6},
};
var aplatie = matrice.SelectMany(ligne => ligne);
// aplatie contiendra { 1, 2, 3, 4, 5, 6 }
Un exemple plus concret concerne l'extraction de tous les employés à partir d'une liste de départements :
public class Employe { public string Nom { get; set; } }
public class Departement { public Employe[] Employes { get; set; } }
var departements = new[]
{
new Departement { Employes = new [] { new Employe { Nom = "Bob" }, new Employe { Nom = "Alice" } } },
new Departement { Employes = new [] { new Employe { Nom = "Charlie" }, new Employe { Nom = "David" } } }
};
var tousLesEmployes = departements.SelectMany(dep => dep.Employes);
// tousLesEmployes contiendra Bob, Alice, Charlie, David
Produit Cartésien avec SelectMany
SelectMany permet également de réaliser des produits cartésiens entre plusieurs collections.
var couleurs = new List<string> { "Rouge", "Vert" };
var tailles = new List<string> { "P", "M", "G" };
// Méthode SQL-like
var combinaisonsSql = from c in couleurs
from t in tailles
select $"{c}-{t}";
// Méthode avec SelectMany
var combinaisonsSelectMany = couleurs.SelectMany(c => tailles.Select(t => $"{c}-{t}"));
// Les deux retournent : Rouge-P, Rouge-M, Rouge-G, Vert-P, Vert-M, Vert-G
Pour un produit cartésien de N collections, une approche récursive utilisant SelectMany est nécessaire.
Agrégation avec Aggregate
La méthode Aggregate applique une fonction d'accumulation à chaque élément d'une séquence, résultant en une valeur unique.
int[] nombres = { 1, 2, 3, 4, 5 };
int somme = nombres.Aggregate((sommePrecedente, elementCourant) => sommePrecedente + elementCourant);
// somme sera 15 (1+2+3+4+5)
Aggregate peut aussi être initialisé avec une valeur de départ.
var items = Enumerable.Range(1, 12); // Nombres de 1 à 12
var resultatAgregation = items.Aggregate(
new { Total = 0, Pair = 0, Quartiles = new List<int>>() },
(accumulateur, item) => new
{
Total = accumulateur.Total + 1,
Pair = accumulateur.Pair + (item % 2 == 0 ? 1 : 0),
Quartiles = (accumulateur.Total + 1) % 4 == 0 ? new List<int>(accumulateur.Quartiles) { item } : accumulateur.Quartiles
}
);
// resultatAgregation contiendra: Total = 12, Pair = 6, Quartiles = [4, 8, 12]
Jointures avec LINQ
LINQ supporte divers types de jointures similaires à SQL : Inner Join, Left Join, Right Join, Cross Join.
var gauche = new List<string>>() { "a", "b", "c" };
var droite = new List<string>>() { "a", "c", "d" };
// Inner Join
var innerJoin = from g in gauche
join d in droite on g equals d
select new { Left = g, Right = d };
// Résultat: {a,a}, {c,c}
// Left Join (simulé)
var leftJoin = from g in gauche
join d in droite on g equals d into temp
from t in temp.DefaultIfEmpty()
select new { Left = g, Right = t };
// Résultat: {a,a}, {b,null}, {c,c}
// Cross Join
var crossJoin = from g in gauche
from d in droite
select new { Left = g, Right = d };
// Résultat: Toutes les combinaisons possibles de g et d
Pour les jointures sur plusieurs clés, on utilise des objets anonymes pour comparer les paires de clés.
Pagination : Skip et Take
Skip(n) saute les n premiers éléments, tandis que Take(n) sélectionne les n premiers éléments. Combinés, ils permettent l'implémentation de la pagination.
var valeurs = new[] { 5, 4, 3, 2, 1 };
var apresSaut = valeurs.Skip(2); // { 3, 2, 1 }
var selection = valeurs.Take(3); // { 5, 4, 3 }
var page = valeurs.Skip(1).Take(2); // { 4, 3 }
Les méthodes SkipWhile et TakeWhile appliquent la condition de manière séquentielle jusqu'à ce qu'elle soit remplie ou non.
Manipulation de Séquences : Zip, OfType, Cast
Zip combine les éléments de deux séquences en paires positionnelles.
int[] nombres = { 3, 5, 7 };
string[] mots = { "trois", "cinq", "sept", "non_utilisé" };
var paires = nombres.Zip(mots, (n, m) => $"{n} = {m}");
// paires contiendra: "3 = trois", "5 = cinq", "7 = sept"
OfType<T> filtre une séquence pour ne conserver que les éléments d'un type spécifié. Cast<T> tente de convertir tous les éléments d'une séquence vers un type spécifié, levant une exception en cas d'échec.
interface IFigure { }
class Cercle : IFigure { }
class Carre : IFigure { }
var figures = new IFigure[] { new Cercle(), new Carre(), new Cercle() };
var cercles = figures.OfType<Cercle>(); // Ne conserve que les instances de Cercle
// var carresCast = figures.Cast<Carre>(); // Lèverait une exception car IFigure n'est pas directement castable en Carre
Regroupement et Indexation : ToLookup, Distinct
ToLookup crée une structure indexée où les éléments sont groupés par une clé spécifiée.
string[] mots = { "un", "deux", "trois", "quatre" };
var rechercheParLongueur = mots.ToLookup(mot => mot.Length);
var motsDeTroisLettres = rechercheParLongueur[3]; // Contient "un", "deux"
Distinct supprime les éléments en double d'une séquence. Pour les types personnalisés, un IEqualityComparer<T> est nécessaire pour définir la notion de duplication.
Conversion en Dictionnaire : ToDictionary
ToDictionary transforme une séquence en un dictionnaire, où chaque élément devient une paire clé-valeur.
IEnumerable<Utilisateur> utilisateurs = ObtenirUtilisateurs();
Dictionary<int, Utilisateur> utilisateursParId = utilisateurs.ToDictionary(u => u.Id);
// Si on veut seulement le nom comme valeur:
Dictionary<int, string> nomsUtilisateursParId = utilisateurs.ToDictionary(u => u.Id, u => u.Nom);
Il est crucial que les clés générées soient uniques, sinon une exception sera levée.
Autres Méthodes Utiles
Enumerable.RangeetEnumerable.Repeat: Génèrent des séquences de nombres ou de valeurs répétées.AnyetAll: Vérifient si au moins un élément (Any) ou tous les éléments (All) satisfont une condition.ConcatetUnion:Concatjoint deux séquences sans supprimer les doublons, tandis queUnionles supprime.GroupBy: Regroupe les éléments d'une séquence selon une clé spécifiée.DefaultIfEmpty: Fournit une valeur par défaut si une séquence est vide.SequenceEqual: Compare deux séquences pour vérifier si elles sont identiques dans l'ordre et le contenu.
LINQ regorge de méthodes facilitant la manipulation et l'interrogation de données. La maîtrise de ces méthodes améliore significativement la produtcivité du développeur C#.