Programmation C sharp/Expression de requête

Les versions récentes du langage C sharp permettent d'utiliser une expression de requête sur un ensemble de données itérables en utilisant une syntaxe similaire à celle des requêtes de base de données. Cette intégration de la syntaxe de requête dans le langage s'appelle en anglais Language Integrated Query (LINQ).

Programmation C#
Programmation C#
Modifier ce modèle

Syntaxe modifier

Le mot clé from sert à introduire une expression de requête. Il est suivi par le nom de la variable qui référencera les valeurs itérées, du mot-clé in, de l'ensemble de données itéré et d'un nombre d'instructions de requête (sélection, groupement, tri, etc.).

L'ensemble de données doit implémenter l'interface IEnumerable<T> ou une interface dérivée comme IQueryable<T> ; le type T est alors utilisé par le compilateur pour définir le type générique pour la variable qui référencera les valeurs itérées. Si l'ensemble de données implémente l'interface IEnumerable (sans type d'élément spécifié, comme la classe ArrayList), le type d'élément doit être spécifié.

En clair, cela transforme un ensemble IEnumerable en un autre ensemble IEnumerable itérant des éléments du même type (ou IGrouping<K,T> pour une clause de groupement), mais filtrant et éventuellement réordonnant les éléments de l'ensemble initial.

Exemple : N'afficher que les entiers pairs d'un tableau

// L'ensemble des données
int[] les_nombres = new int[5] { 12, 11, 24, 28, 17 };

// Requête : nombres_pairs est de type IEnumerable<int>
var nombres_pairs =
	from nombre in les_nombres
	where (nombre % 2) == 0
	select nombre;

// Exécution de la requête : itération sur les résultats
foreach (int nombre_pair in nombres_pairs)
	Console.WriteLine("Nombre pair : {0}", nombre_pair);
// N'affiche que les nombres pairs du tableau les_nombres :
//   Nombre pair 12
//   Nombre pair 24
//   Nombre pair 28

La requête n'est pas exécutée immédiatement, mais lors de l'itération avec la boucle foreach. Elle est également déclenchée par l'appel à une méthode de l'interface IEnumerable nécessitant le parcours des éléments :

  • Pour obtenir des statistiques sur les résultats de la requête :
Count, Max, Average, First...
  • Pour convertir les résultats en tableau ou collection :
ToArray, ToList...
// les_nombres_pairs est de type int[]
var les_nombres_pairs =
	(from nombre in les_nombres
	where (nombre % 2) == 0
	select nombre).ToArray();

La requête semble réaliser l'équivalent d'une boucle combinée à une condition. Cependant elle permet également de trier les résultats, de les regrouper, et il est possible de passer la requête à une fonction ayant besoin d'un ensemble de valeurs itérables. Ces cas d'utilisations seraient beaucoup plus complexes à réaliser sans la syntaxe des expressions de requête.

Quand l'ensemble de données implémente l'interface IEnumerable sans type d'élément spécifié, comme la classe ArrayList par exemple, le type d'élément doit être spécifié pour la variable comme dans l'exemple suivant :

ArrayList listeDeNom = new ArrayList();
listeDeNom.add("exemple");
listeDeNom.add("de liste");
listeDeNom.add("contenant");
listeDeNom.add("des valeurs");

var nom_longs =
	from string nom in listeDeNom
	where nom.length > 8
	select nom;

Clauses de requête modifier

Différentes clauses suivent la définition de la source de données, traitées dans l'ordre, chacune introduite par un mot-clé :

  • where pour filtrer les données avec une condition,
  • join pour joindre les résultats avec une autre source de données,
  • group pour grouper les résultats (type T) ayant une valeur commune (la clé K) en objets de type IGrouping<K,T>,
  • orderby pour itérer les résultats dans un ordre particulier,
  • select pour définir les résultats à itérer (objet, structure, calcul...).

Filtre modifier

Le filtrage des éléments est introduit par le mot-clé where suivi d'une expression booléenne définissant la condition pour que l'élément apparaisse dans l'itération des résultats de la requête.

var petits_nombres_pairs =
	from nombre in les_nombres
	where ((nombre % 2) == 0) && (nombre < 20)
	select nombre;

Plusieurs clauses where peuvent être utilisées à la suite permettant de valider toutes les conditions, comme l'opérateur et utilisé dans l'exemple précédent.

Exemple équivalent au précédent :

var petits_nombres_pairs =
	from nombre in les_nombres
	where (nombre % 2) == 0
	where nombre < 20
	select nombre;

Sélection modifier

La clause select définit la valeur retournée à chaque itération des résultats de la requête, et détermine aussi le type du paramètre générique des résultats itérables.

// Requête : nombres_pairs_romains est de type IEnumerable<string>
var nombre_pairs_romains =
	from nombre in les_nombres
	where (nombre % 2) == 0
	select NombreEnChiffresRomains(nombre);

// Exécution de la requête : itération sur les résultats
foreach (string nombre_pair_romain in nombres_pairs_romains)
	Console.WriteLine("Nombre pair : {0}", nombre_pair_romain);
// Si l'hypothétique fonction NombreEnChiffresRomains
// retourne bien la représentation en chiffres romains :
//   Nombre pair XII
//   Nombre pair XXIV
//   Nombre pair XXVIII

Tri modifier

Les résultats peuvent être triant en utilisant la clause orderby. Ce mot-clé est suivi d'une liste d'expressions définissant chacune la valeur de critère pour définir l'ordre des résultats. Chacun de ces critères est optionnellement suivi du mot-clé ascending pour un tri dans l'ordre croissant, ou du mot-clé descending pour un tri dans l'ordre décroissant. S'il n'est pas spécifié, le tri est en ordre croissant.

Exemple simple :

// L'ensemble des données
string[] les_mois = new int[12]
{
	"janvier", "février", "mars", "avril", "mai", "juin",
	"juillet", "août", "septembre", "octobre", "novembre", "décembre"
};

// Requête : les_mois_ordonnes est de type IEnumerable<string>
var les_mois_ordonnes =
	from nom_du_mois in les_mois
	orderby nom_du_mois ascending
	select nom_du_mois;

// Exécution de la requête : itération sur les mois ordonnés
foreach (string mois in les_mois_ordonnes)
	Console.WriteLine("- {0}", mois );
// Affiche les mois dans l'ordre alphabétique :
// - août
// - avril
// - décembre
// - février
// - janvier
// - juillet
// - juin
// - mai
// - mars
// - novembre
// - octobre
// - septembre

Exemple avec un tableau d'objets utilisé dans une requête où second critère de tri est spécifié en cas d'égalité pour le premier :

// L'ensemble des données
public class VenteDuMois
{
	public string Mois { get; set; }
	public List<int> Montants { get; set; }
}
List<VenteDuMois> ventes_par_mois = new List<VenteDuMois>
{
	new VenteDuMois { Mois = "janvier", Montants = new List<int> { 220, 158, 301 } },
	new VenteDuMois { Mois = "février", Montants = new List<int> { 162, 143, 157 } },
	new VenteDuMois { Mois = "mars",    Montants = new List<int> { 138, 187, 231 } },
	new VenteDuMois { Mois = "avril",   Montants = new List<int> { 11, 55, 400 } },
	new VenteDuMois { Mois = "mai",     Montants = new List<int> { 311, 117, 90, 180 } },
};

// Requête : meilleurs_mois est de type IEnumerable<object>
var meilleurs_mois =
	from vente_du_mois in ventes_par_mois
	orderby vente_du_mois.Montants.Sum() descending, vente_du_mois.Name ascending
	select new { Name=vente_du_mois.Mois, Total=vente_du_mois.Montants.Sum() };

// Exécution de la requête : itération sur les meilleurs mois
foreach (string mois in meilleurs_mois)
	Console.WriteLine("- {0} => {1}", mois.Name, mois.Total );
// Affiche les mois dans l'ordre décroissant du total des montants :
// - mai => 698
// - janvier => 679
// - mars => 556
// - avril => 466
// - février => 462

Regroupement modifier

Le regroupement est introduit par le mot-clé group produisant une séquence d'objets de classe implémentant l'interface IGrouping<K,T>. Cette interface permet d'accéder à la clé K commune aux éléments membres du groupe par la propriété Key, et étend l'interface IEnumerable<T>. Cela signifie qu'une instance de cette interface permet d'itérer sur les éléments membres du groupe.

Le mot-clé group est suivi de l'objet groupé (définissant T pour l'interface générique IGrouping<K,T>), du mot-clé by suivi de la clé de regroupement (définissant K pour l'interface générique IGrouping<K,T>).

Exemple : Regrouper les dates d'un planning annuel par mois

// L'ensemble des données
public class DateReunion
{
	public string Mois { get; set; }
	public int jour { get; set; }
}
List<DateReunion> dates_planning = new List<DateReunion>
{
	new DateReunion { Mois = "janvier", jour = 22 },
	new DateReunion { Mois = "février", jour = 17 },
	new DateReunion { Mois = "mars",    jour = 11 },
	new DateReunion { Mois = "avril",   jour = 15 },
	new DateReunion { Mois = "janvier", jour = 12 },
	new DateReunion { Mois = "février", jour = 18 },
	new DateReunion { Mois = "mars",    jour =  8 },
	new DateReunion { Mois = "avril",   jour =  2 },
	new DateReunion { Mois = "janvier", jour = 26 },
	new DateReunion { Mois = "février", jour = 16 },
	new DateReunion { Mois = "mars",    jour = 20 },
	new DateReunion { Mois = "avril",   jour =  3 }
};

// Requête : planning_mensuel est de type IEnumerable<IGrouping<string,DateReunion>>
var planning_mensuel =
	from date_reunion in dates_planning
	group date_reunion by date_reunion.Mois; // -> IGrouping<string,DateReunion>

Pour effectuer des opérations supplémentaires sur les groupes, il faut ajouter une clause into pour définir le nom référençant le groupe, comme dans l'exemple suivant :

// Requête : planning_mensuel est de type IEnumerable<IGrouping<string,DateReunion>>
var planning_mensuel =
	from date_reunion in dates_planning
	group date_reunion by date_reunion.Mois into g // -> IGrouping<string,DateReunion>
	orderby g.Key  // par mois dans l'ordre croissant
	select g;

Deux boucles imbriquées sont nécessaires : la boucle de premier niveau itère sur les groupes, et la boucle de second niveau itère sur les membres de chaque groupe, comme illustré ci-dessous :

// Exécution de la requête : itération sur les résultats
foreach (IGrouping<string,DateReunion> group in planning_mensuel)
{
	Console.WriteLine("Dates en {0} :", group.Key);
	foreach (DateReunion date in group)
		Console.WriteLine("    le {0} {1}", date.jour, date.Mois);
}

L'exemple affiche les dates groupées par mois :

Dates en avril :
    le 15 avril
    le 2 avril
    le 3 avril
Dates en février :
    le 17 février
    le 18 février
    le 16 février
Dates en janvier :
    le 22 janvier
    le 12 janvier
    le 26 janvier
Dates en mars :
    le 11 mars
    le 8 mars
    le 20 mars

La clé de regroupement est une expression quelconque, et peut créer un nouvel objet pour regrouper plusieurs critères de regroupement.

Combinaison de sources de données modifier

Jointure modifier

Il est possible de faire une jointure libre en utilisant plusieurs clauses from sur des sources de données indépendantes, comme dans l'exemple ci-dessous.

string[] projects = { "wikibooks", "wikinews", "wikiversity" };
string[] langs = { "fr", "es", "de" };

var project_domains =
	from project in projects
	where project != "wikinews" // sauf wikinews
	from lang in langs
	where project != "wikiversity" || lang != 'de' // sauf (wikiversity, de)
	select lang+"."+project+".org";

// Exécution de la requête :
foreach (string domain in project_domains)
	Console.WriteLine("- {0}", domain);
// Affiche :
// - fr.wikibooks.org
// - fr.wikiversity.org
// - es.wikibooks.org
// - es.wikiversity.org
// - de.wikibooks.org

Sous-requête modifier

Il est possible de créer une sous-requête pour sélectionner des données d'une collection stockée dans un objet. Pour cela, une clause from est ajoutée à la première, référençant la variable de celle-ci.

Exemple :

// L'ensemble des données
public class VenteDuMois
{
	public string Mois { get; set; }
	public List<int> Montants { get; set; }
}
List<VenteDuMois> ventes_par_mois = new List<VenteDuMois>
{
	new VenteDuMois { Mois = "janvier", Montants = new List<int> { 220, 158, 301 } },
	new VenteDuMois { Mois = "février", Montants = new List<int> { 162, 143, 157 } },
	new VenteDuMois { Mois = "mars",    Montants = new List<int> { 138, 187, 231 } },
	new VenteDuMois { Mois = "avril",   Montants = new List<int> { 11, 55, 400 } },
	new VenteDuMois { Mois = "mai",     Montants = new List<int> { 311, 117, 90, 180 } },
};

// Requête : meilleurs_mois est de type IEnumerable<object>
var grands_montants =
	from vente_du_mois in ventes_par_mois
	from montant in vente_du_mois.Montants
	where montant >= 200
	select new { Name=vente_du_mois.Mois, Montant=montant };

// Exécution de la requête : itération sur les montants plus grands que 200
foreach (string mois in grands_montants)
	Console.WriteLine("- {0} => {1}", mois.Name, mois.Montant );
// Affiche les grands montants :
// - janvier => 220
// - janvier => 301
// - mars => 231
// - avril => 400
// - mai => 311