Introduction aux requêtes LINQ en C#

Une requête est une expression qui récupère des données d’une source de données. Des sources de données différentes ont divers langages de requête natifs, par exemple, SQL pour les bases de données relationnelles et XQuery pour XML. Les développeurs doivent apprendre un nouveau langage de requête pour chaque type de source de données ou format de données qu’ils doivent prendre en charge. LINQ simplifie cette situation en proposant un modèle de langage C# cohérent pour des types de formats et de sources de données. Dans une requête LINQ, vous travaillez toujours avec des objets C#. Vous utilisez les mêmes modèles d’encodage de base pour interroger et transformer des données en documents XML, bases de données SQL, collections .NET et tout autre format quand un fournisseur LINQ est disponible.

Les trois parties d’une opération de requête

Toutes les opérations de requête LINQ se composent de trois actions distinctes :

  1. Obtention de la source de données
  2. Création de la requête
  3. exécutez la requête.

L’exemple suivant montre comment les trois parties d’une opération de requête sont exprimées dans le code source. Cet exemple utilise un tableau d’entiers comme source de données pour des raisons pratiques. Toutefois, ces mêmes concepts s’appliquent également aux autres sources de données. Le reste de l’article fait référence à cet exemple.

// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

// 3. Query execution.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

L’illustration suivante montre l’intégralité de l’opération de requête. Dans LINQ, l’exécution de la requête est distincte de la requête elle-même. En d’autres termes, vous ne récupérez aucune donnée en créant une variable de requête.

Diagramme de l’opération de requête LINQ complète.

Source de données

La source de données de l’exemple précédent est un tableau, elle prend en charge l’interface générique IEnumerable<T>. Cela signifie qu’elle peut être interrogée avec LINQ. Une requête est exécutée dans une instruction foreach, et foreach nécessite IEnumerable ou IEnumerable<T>. Les types qui prennent en charge IEnumerable<T> ou une interface dérivée, comme l’interface générique IQueryable<T>, sont appelés des types requêtables.

Un type requêtable ne nécessite aucune modification ni traitement spécial pour servir de source de données LINQ. Si les données sources ne sont pas déjà en mémoire en tant que type requêtable, le fournisseur LINQ doit le représenter comme tel. Par exemple, LINQ to XML charge un document XML dans un type requêtable XElement :

// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

Avec EntityFramework, vous créez un mappage relationnel d’objets entre les classes C# et votre schéma de base de données. Vous écrivez vos requêtes sur les objets, et à l’exécution, EntityFramework gère la communication avec la base de données. Dans l’exemple suivant, Customers représente une table spécifique de la base de données et le type du résultat de la requête, IQueryable<T>, dérive de IEnumerable<T>.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.
IQueryable<Customer> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust;

Pour plus d’informations sur la création de types spécifiques de sources de données, consultez la documentation relative aux différents fournisseurs LINQ. Toutefois, la règle de base est très simple : une source de données LINQ est un objet qui prend en charge l’interface générique IEnumerable<T> ou une interface qui hérite de celui-ci, généralement IQueryable<T>.

Remarque

Les types tels que ArrayList qui prennent en charge l’interface non générique IEnumerable peuvent également être utilisés comme source de données Linq. Pour plus d’informations, consultez Guide pratique pour interroger une liste de tableaux avec LINQ (C#).

La requête

La requête spécifie les informations à récupérer à partir de la ou des sources de données. Si vous le souhaitez, une requête peut également spécifier la manière dont ces informations doivent être triées, regroupées et mises en forme avant d’être retournées. Une requête est stockée dans une variable de requête et initialisée avec une expression de requête. Vous utilisez la syntaxe de requête C# pour écrire des requêtes.

La requête de l’exemple précédent retourne tous les nombres pairs du tableau d’entiers. L’expression de requête contient trois clauses : from, where et select. (Si vous connaissez le langage SQL, vous remarquez que l’ordre des clauses est inverse à celui du langage SQL.) La clause from spécifie la source de données, la clause where applique le filtre et la clause select spécifie le type des éléments retournés. Toutes les clauses de requête sont évoquées de manière détaillée dans cette section. Pour le moment, le point important est que dans LINQ, la variable de requête elle-même n’effectue aucune action et ne retourne aucune donnée. Elle stocke simplement les informations qui seront nécessaires pour produire des résultats lors de l’exécution ultérieure de la requête. Pour obtenir plus d’informations sur la construction des requêtes, consultez Vue d’ensemble des opérateurs de requête standard (C#).

Remarque

Les requêtes peuvent également être exprimées à l’aide d’une syntaxe de méthode. Pour plus d’informations, consultez Syntaxe de requête et syntaxe de méthode dans LINQ.

Classification des opérateurs de requête standard en fonction de leur mode d'exécution

Les implémentations LINQ to Objects des méthodes d’opérateur de requête standard s’exécutent de deux manières principales : immédiate ou différée. En outre, les opérateurs de requête qui utilisent l’exécution différée peuvent être divisés en deux catégories : ceux prenant en charge la diffusion en continu et nonstreaming.

Immédiat

L’exécution immédiate signifie que la source de données est lue et que l’opération est effectuée une seule fois. Tous les opérateurs de requête standard qui retournent un résultat scalaire s’exécutent immédiatement. Ces requêtes sont par exemple Count, Max, Average et First. Ces méthodes s’exécutent sans instruction foreach explicite, car la requête elle-même doit utiliser foreach pour retourner un résultat. Ces requêtes retournent une seule valeur, et non une collection IEnumerable. Vous pouvez forcer l’exécution de toute requête immédiatement à l’aide des méthodes Enumerable.ToList ou Enumerable.ToArray. L’exécution immédiate permet la réutilisation des résultats de la requête, et non la déclaration de requête. Les résultats sont récupérés une fois, puis stockés pour une utilisation ultérieure. La requête suivante retourne un nombre de chiffres pairs du tableau source :

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

Pour forcer l’exécution immédiate de n’importe quelle requête et mettre en cache ses résultats, vous pouvez appeler les méthodes ToList ou ToArray.

List<int> numQuery2 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToArray();

Vous pouvez également forcer l’exécution en plaçant la boucle foreach immédiatement après l’expression de requête. Toutefois, en appelant ToList ou ToArray, vous mettez également en cache toutes les données dans un même objet de collection.

Reporté

L’exécution différée signifie que l’opération n’est pas effectuée au point où la requête est déclarée dans le code. L’opération est effectuée uniquement quand la variable de requête est énumérée, par exemple à l’aide d’une instruction foreach. Les résultats de l’exécution de la requête dépendent du contenu de la source de données à l’exécution de la requête, plutôt qu’au moment de sa définition. Si la variable de requête est énumérée plusieurs fois, les résultats peuvent différer chaque fois. Presque tous les opérateurs de requête standard dont le type de retour est IEnumerable<T> ou IOrderedEnumerable<TElement> s’exécutent de manière différée. L’exécution différée permet la réutilisation des requêtes, car la requête extrait les données mises à jour de la source de données chaque fois que les résultats de la requête sont itérés. Le code suivant affiche un exemple d’exécution différée :

foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

C’est dans l’instruction foreach que les résultats de requête sont récupérés. Par exemple, dans la requête précédente, la variable d’itération num contient chaque valeur (une à la fois) de la séquence retournée.

Étant donné que la variable de requête ne contient jamais les résultats de requête, vous pouvez l’exécuter aussi souvent que vous le voulez pour récupérer les données mises à jour. Par exemple, une application distincte peut mettre à jour une base de données en permanence. Dans votre application, vous pouvez créer une requête qui récupère les données les plus récentes, puis les exécuter à intervalles réguliers pour obtenir les résultats mis à jour.

Les opérateurs de requête utilisant l’exécution différée peuvent également être classés comme prenant en charge la diffusion en continu ou nonstreaming.

Diffusion en continu

Les opérateurs prenant en charge la diffusion en continu n’ont pas à lire toutes les données sources avant de générer des éléments. Au moment de l’exécution, un opérateur prenant en charge la diffusion en continu effectue son opération sur chaque élément source à mesure qu’il est lu et génère l’élément si nécessaire. Ce type d’opérateur continue à lire des éléments source jusqu’à ce qu’un élément de résultat puisse être produit. Cela signifie que plusieurs éléments source peuvent être lus pour produire un élément de résultat.

Nonstreaming

Les opérateurs nonstreaming doivent lire toutes les données sources avant de cesser temporairement l’exécution d’un élément de résultat. Les opérations telles que le tri ou le regroupement font partie de cette catégorie. Au moment de l’exécution, les opérateurs de requête nonstreaming lisent toutes les données sources, les placent dans une structure de données, puis effectuent l’opération et cessent temporairement l’exécution des éléments de résultat.

Tableau de classification

Le tableau suivant classe chaque méthode d’opérateur de requête standard en fonction de son mode d’exécution.

Notes

Si un opérateur est présent dans deux colonnes, deux séquences d’entrée sont impliquées dans l’opération et chacune d’elles est évaluée différemment. Dans ces cas, la première séquence dans la liste de paramètres est toujours évaluée de façon différée, avec diffusion en continu.

Opérateur de requête standard Type de retour Exécution immédiate Exécution différée avec diffusion en continu Exécution nonstreaming différée
Aggregate TSource X
All Boolean X
Any Boolean X
AsEnumerable IEnumerable<T> X
Average Valeur numérique unique X
Cast IEnumerable<T> X
Concat IEnumerable<T> X
Contains Boolean X
Count Int32 X
DefaultIfEmpty IEnumerable<T> X
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource? X
Empty IEnumerable<T> X
Except IEnumerable<T> X X
First TSource X
FirstOrDefault TSource? X
GroupBy IEnumerable<T> X
GroupJoin IEnumerable<T> X X
Intersect IEnumerable<T> X X
Join IEnumerable<T> X X
Last TSource X
LastOrDefault TSource? X
LongCount Int64 X
Max Valeur numérique unique, TSource, ou TResult? X
Min Valeur numérique unique, TSource, ou TResult? X
OfType IEnumerable<T> X
OrderBy IOrderedEnumerable<TElement> X
OrderByDescending IOrderedEnumerable<TElement> X
Range IEnumerable<T> X
Repeat IEnumerable<T> X
Reverse IEnumerable<T> X
Select IEnumerable<T> X
SelectMany IEnumerable<T> X
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource? X
Skip IEnumerable<T> X
SkipWhile IEnumerable<T> X
Sum Valeur numérique unique X
Take IEnumerable<T> X
TakeWhile IEnumerable<T> X
ThenBy IOrderedEnumerable<TElement> X
ThenByDescending IOrderedEnumerable<TElement> X
ToArray Tableau TSource[] X
ToDictionary Dictionary<TKey,TValue> X
ToList IList<T> X
ToLookup ILookup<TKey,TElement> X
Union IEnumerable<T> X
Where IEnumerable<T> X

LINQ to Objects

« LINQ to Objects » fait référence à l’utilisation de requêtes LINQ directement avec n’importe quelle collection IEnumerable ou IEnumerable<T>. Vous pouvez utiliser LINQ pour interroger toutes les collections énumérables telles que List<T>, Array ou Dictionary<TKey,TValue>. La collection peut être définie par l’utilisateur ou un type retourné par une API .NET. Avec l’approche LINQ, vous écrivez du code déclaratif qui décrit ce que vous voulez récupérer. LINQ to Objects fournit une excellente introduction à la programmation avec LINQ.

Les requêtes LINQ offrent trois principaux avantages par rapport aux boucles foreach classiques :

  • Elles sont plus concises et en lecture, en particulier durant le filtrage de plusieurs conditions.
  • Elles fournissent de puissantes fonctionnalités de filtrage, de classement et de regroupement avec un minimum de code d'application.
  • Elles peuvent être portées vers d'autres sources de données avec peu ou pas de changements.

Plus l’opération à effectuer sur les données est complexe, plus vous avez intérêt à utiliser LINQ plutôt que des techniques d’itération classiques.

Stocker les résultats d’une requête dans la mémoire

Une requête est en fait un ensemble d’instructions permettant de récupérer et d’organiser des données. Les requêtes sont exécutées de manière différée, à mesure que chaque élément dans le résultat est demandé. Lorsque vous utilisez foreach pour effectuer une itération sur les résultats, les éléments sont retournés lorsque vous y accédez. Pour évaluer une requête et stocker ses résultats sans exécuter une boucle foreach, appelez simplement l’une des méthodes suivantes sur la variable de requête :

Vous devez attribuer l’objet de collection retourné à une nouvelle variable lorsque vous stockez les résultats de la requête, comme indiqué dans l’exemple suivant :

List<int> numbers = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

IEnumerable<int> queryFactorsOfFour =
    from num in numbers
    where num % 4 == 0
    select num;

// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();

// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

Voir aussi