Notez bien qu'il est indispensable de bien avoir assimilé C# 2.0, en particulier les notions de génériques et de méthodes anonymes pour la compréhension de ce chapitre. Notre but est de construire l'équivalent d'un mini Linq en C# 2.0.
Pour notre démonstration, nous prendrons un exemple relativement simple : Nous souhaitons effectuer la moyenne des notes moyennes d'un ensemble d'élèves. Pour cela, il nous suffit de créer deux objets entités représentant l'élève et la note :
public class NoteENT
{
private float _note;
private DateTime _date;
public float Note { get { return _note; } set { _note = value; } }
public DateTime Date { get { return _date; } set { _date = value; } }
}
public class EleveENT
{
private string _nom;
private string _prenom;
private IEnumerable<NoteENT> _notes;
public string Nom { get { return _nom; } set { _nom = value; } }
public string Prenom { get { return _prenom; } set { _prenom = value; } }
public IEnumerable<NoteENT> Notes { get { return _notes; } set { _notes = value; } }
}
Nous manipulerons une liste d'élèves, sur laquelle nous effectuerons des requêtes. Afin de ne pas alourdir notre exemple, nous nous contenterons de la liste générique du Framework 2.0, à savoir List<T>:
List<EleveENT> l = new List<EleveENT>();
EleveENT el1 = new EleveENT();
el1.Nom = "Martin";
el1.Prenom = "Michel";
List<NoteENT> nos1 = new List<NoteENT>();
NoteENT no1 = new NoteENT();
no1.Note = 3;
no1.Date = new DateTime(2006, 12, 12);
NoteENT no2 = new NoteENT();
no2.Note = 9.5f;
no2.Date = new DateTime(2007, 4, 1);
nos1.Add(no1);
nos1.Add(no2);
el1.Notes = nos1;
EleveENT el2 = new EleveENT();
el2.Nom = "Durand";
el2.Prenom = "Cyril";
List<NoteENT> nos2 = new List<NoteENT>();
NoteENT no3 = new NoteENT();
no3.Note = 17;
no3.Date = new DateTime(2006, 12, 12);
NoteENT no4 = new NoteENT();
no4.Note = 15;
no4.Date = new DateTime(2007, 4, 1);
nos2.Add(no3);
nos2.Add(no4);
el2.Notes = nos2;
l.Add(el1);
l.Add(el2);
Il est nécessaire de créer une classe permettant de manipuler une liste d'objets tels que des élèves ou des notes. Nous souhaitons pouvoir sélectionner des critères (le nom de l'élève, ou l'ensemble de ses notes, ou bien une note, etc.). Pour créer cette fonctionnalité, il faut créer une méthode de sélection prenant en argument une « List<T> » mais ce serait oublier l'implémentation de l'interface IEnumerable<T> qui est en mesure de nous offrir une plus grande souplesse d'emploi. Un deuxième argument à notre méthode est indispensable pour préciser le critère que l'on souhaite sélectionner. L'usage d'un délégué jouera le rôle d'un pointeur de fonction dans laquelle nous pourrons préciser ce critère. Ce délégué prendra donc en charge une source à partir de laquelle nous produirons un résultat.
public delegate TResult Func<TSource, TResult>(TSource source);
public static class ComplementEnumerable
{
public static IEnumerable<TResult> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
foreach (TSource s in source)
yield return selector.Invoke(s);
}
}
L'emploi d'une méthode anonyme en argument de la méthode « Select » plutôt qu'une référence à une méthode dont la définition serait figée concentre en une seule ligne de code les éléments sélectionnés. Ceci apporte une meilleure lecture et favorise sans doute d'éventuels changements. Testons la méthode précédente :
IEnumerable<IEnumerable<NoteENT>> t = ComplementEnumerable.Select<EleveENT, IEnumerable<NoteENT>>(l,
delegate(EleveENT c) { return c.Notes; });
L'élément retourné par la méthode anonyme est parfaitement visible, il s'agit de la propriété « Notes » de l'objet « EleveENT », il s'agit donc bien d'une sélection.
Si on souhaite affiner celle-ci en récupérant tous les sous-ensembles de notes des élèves, il suffit de sélectionner la propriété Note de l'objet « NoteENT » :
IEnumerable<IEnumerable<float>> t = ComplementEnumerable.Select<EleveENT, IEnumerable<float>>(l,
delegate(EleveENT c)
{
return ComplementEnumerable.Select<NoteENT, float>(c.Notes, delegate(NoteENT n)
{ return n.Note; }
);
});
Le résultat renvoie bien tous les sous-ensembles des notes obtenues par l'ensemble des élèves. La première remarque qui peut venir à l'esprit, est la lourdeur de la syntaxe, même si le résultat répond parfaitement à notre attente.
Cherchons à ajouter d'autres fonctionnalités à la classe « ComplementEnumerable ». Généralement, lorsque vous réalisez une sélection, vous pouvez fixer des conditions. Ces dernières doivent se vérifier. L'ajout d'une méthode « Where » pouvant prendre en argument une méthode anonyme renvoyant un booléen est parfaitement adapté à la prise en compte de conditions :
public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> selector)
{
foreach (TSource s in source)
if (selector.Invoke(s))
yield return s;
}
Le type du premier argument de la méthode est du même type que celui retourné par celle-ci. Elle ne renvoie que les éléments répondant aux conditions fixées.
L'utilisation de la clause « Where » peut se faire par exemple comme suit :
IEnumerable<IEnumerable<float>> t = ComplementEnumerable.Select<EleveENT, IEnumerable<float>>(l,
delegate(EleveENT c)
{
return ComplementEnumerable.Select<NoteENT, float>(ComplementEnumerable.Where<NoteENT>(c.Notes,
delegate(NoteENT n)
{ return n.Date >= new DateTime(2007,4,1); }
), delegate(NoteENT n)
{ return n.Note; }
);
});
La requête ci-dessus permet de récolter l'ensemble des groupes de notes attribuées après le 1er Avril 2007. La syntaxe s'alourdit mais nous parvenons toujours à nos volontés.
A l'origine, nous souhaitions obtenir une moyenne. Il nous faut donc développer cette fonctionnalité. Complétons notre classe « ComplementEnumerable » par une méthode nommée « Average » permettant d'obtenir la moyenne des notes :
public static float Average(IEnumerable<float> source)
{
double num1 = 0;
long num2 = 0;
foreach (float num3 in source)
{
num1 += num3;
num2++;
}
return (float)(num1 / (double) num2);
}
Utilisons cette nouvelle méthode en calculant la moyenne des notes moyennes de l'ensemble des élèves :
float r = ComplementEnumerable.Average(ComplementEnumerable.Select<EleveENT, float>(l, delegate(EleveENT c)
{
return ComplementEnumerable.Average(ComplementEnumerable.Select<NoteENT, float>(c.Notes, delegate(NoteENT n)
{ return n.Note; }
));
}
));
Nous pouvons alléger cette syntaxe en ajoutant une autre définition pour la méthode « Average » :
public static float Average<TSource>(IEnumerable<TSource> Source, Func<TSource, float> selector)
{
return ComplementEnumerable.Average(ComplementEnumerable.Select<TSource, float>(Source, selector));
}
Cette définition utilise astucieusement la combinaison d'une sélection et d'une moyenne. Grâce à cet ajout de méthode dans la classe « ComplementEnumerable » la syntaxe de notre "mini-Linq" s'en trouve alors légèrement simplifiée dans un premier temps :
float r = ComplementEnumerable.Average(ComplementEnumerable.Select<EleveENT, float>(l, delegate(EleveENT c)
{
return ComplementEnumerable.Average<NoteENT>(c.Notes, delegate(NoteENT n)
{ return n.Note; }
);
}
));
Et si nous poussons la simplification complètement :
float r = ComplementEnumerable.Average<EleveENT>(l, delegate(EleveENT c)
{
return ComplementEnumerable.Average<NoteENT>(c.Notes, delegate(NoteENT n)
{ return n.Note; }
);
}
);
Avouez que si la syntaxe est parfois lourde, la classe « ComplementEnumerable » répond parfaitement à notre attente. Veuillez bien noter que nous avons effectué tout cela uniquement avec Visual Studio 2005 et C# 2.0 ! L'objectif avoué dans les prochains chapitres est de reprendre intégralement le code de notre « Mini-Linq » sous Visual Studio 2008 et de le faire évoluer avec la version 3.0 du compilateur C#.
S'il s'agit de votre première lecture, vous pouvez passer au chapitre suivant consacré à la déduction de type et à l'utilisation des expressions Lambda.