Introduction

Cela fait un bon moment que j'ai écrit la partie 1 et la partie 2 de cette série du blog mais j'espère que les choses vont maintenant bouger un peu plus rapidement.

La principale avancée est que le projet dispose maintenant d'un dépôt de source sur Google Code, au lieu d'être simplement inclus dans un fichier zip pour chaque billet de blog. J'ai dû donner au projet un titre à ce moment, et j'ai choisi Edulinq, je l'espère pour des raisons évidentes. J'ai changé les espaces de noms, etc. dans le code, et maintenant le tag de cette série du blog est aussi devenu Edulinq. Bon, assez de préambule. Continuons la réimplémentation de LINQ, cette fois il s'agit de l'opérateur Select.

Qu'est-ce que c'est ?

Comme le Where, le Select a deux surcharges :

 
CacherSélectionnez
public static IEnumerable<TResult> Select<TSource, TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource, TResult> selector) 

public static IEnumerable<TResult> Select<TSource, TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource, int, TResult> selector)

Cette fois encore, elles fonctionnent toutes les deux de la même manière - mais la deuxième surcharge permet à l'index dans la séquence d'être utilisé comme partie de la projection.

D'abord des choses simples : la méthode projette une séquence vers une autre : le délégué « selector » est appliqué à chaque élément en entrée l'un après l'autre, pour donner un élément de sortie. Les remarques sur le comportement sont exactement les mêmes que pour le Where (au point que je coupe et colle celles-ci à partir du billet précédent du blog et en les peaufinant juste) :

  • la séquence d'entrée n'est pas modifiée en aucune manière ;
  • la méthode utilise l'exécution différée - tant que vous n'avez pas commencé à récupérer les éléments de la séquence de sortie, il ne commence pas à récupérer les éléments de la séquence en entrée ;
  • malgré l'exécution différée, il permettra de valider immédiatement que les paramètres ne sont pas null ;
  • il renvoie un flux de ses résultats : il n'a besoin de traiter qu'un résultat à la fois ;
  • il va parcourir la séquence en entrée exactement une fois chaque fois que vous itérez sur la séquence de sortie ;
  • la fonction « selector » est appelée exactement une fois par valeur renvoyée ;
  • libérer un itérateur sur la séquence de sortie va libérer l'itérateur associé à la séquence en entrée.

Qu'allons-nous tester ?

Les tests sont très semblables à ceux faits pour le Where - exceptés les cas où nous avons testé l'aspect de filtrage pour le Where, nous allons maintenant tester l'aspect de projection du Select.

Il y a quelques tests intéressants. Premièrement, on peut dire que la méthode est générique avec deux types de paramètres au lieu d'un - elle comporte des paramètres de type TSource et de TResult. Ils sont assez parlants, mais cela signifie que ça vaut le coup d'avoir un test pour le cas où les arguments sont de types différents - telle que la conversion d'un int en une chaîne de caractères :

 
CacherSélectionnez
[Test] 
public void SimpleProjectionToDifferentType() 
{ 
    int[] source = { 1, 5, 2 }; 
    var result = source.Select(x => x.ToString()); 
    result.AssertSequenceEqual("1", "5", "2"); 
}

Deuxièmement, j'ai un test qui montre quel genre de situations bizarres on peut obtenir si on inclut les effets de bord dans la requête. Bien sûr, nous aurions pu faire cela avec le Where, mais c'est plus clair avec le Select :

 
CacherSélectionnez
[Test] 
public void SideEffectsInProjection() 
{ 
    int[] source = new int[3]; // Les valeurs initiales ne seront pas pertinentes
    int count = 0&#160;; 
    var query = source.Select(x => count++); 
    query.AssertSequenceEqual(0, 1, 2); 
    query.AssertSequenceEqual(3, 4, 5); 
    count = 10; 
    query.AssertSequenceEqual(10, 11, 12); 
}

Remarquez comment nous sommes arrivés à appeler une seule fois le Select, mais les résultats de l'itération sur les résultats changent à chaque fois - parce que la variable « count » a été capturée et est modifiée dans la projection. S'il vous plaît ne faites pas ce genre de choses.

Troisièmement, nous pouvons maintenant écrire les expressions de requête qui contiennent à la fois les clauses « Select » et « Where » :

 
CacherSélectionnez
[Test] 
public void WhereAndSelect() 
{ 
    int[] source = { 1, 3, 4, 2, 8, 1 }; 
    var result = from x in source 
                 where x < 4 
                 select x * 2&#160;; 
    result.AssertSequenceEqual(2, 6, 4, 2); 
}

Il n'y a rien de stupéfiant à tout cela, bien sûr - si vous avez déjà utilisé LINQ to Objects, j'espère que tout cela doit vous mettre très à l'aise et vous sembler familier.

Voyons l'implémentation !

Surprise, surprise, nous implémenterons le « Select » en grande partie de la même manière que le Where. Encore une fois, j'ai simplement copié le fichier d'implémentation et peaufiné un peu - les deux méthodes sont vraiment similaires à ce point. En particulier :

  • nous allons utiliser des blocs itérateurs pour faciliter le renvoi des séquences ;
  • la sémantique de blocs itérateurs signifie que nous devons séparer la validation d'argument de l'exécution réelle. (Depuis que j'ai écrit le précédent billet, j'ai appris que VB11 aura les itérateurs anonymes, ce qui évitera ce problème. Ouf. Ça ne semble pas normal d'envier les utilisateurs de VB mais je vais apprendre à vivre avec ça.) ;
  • nous allons utiliser le foreach dans les blocs itérateurs afin de s'assurer que nous libérons bien l'itérateur de la séquence en entrée de manière appropriée - à condition que notre itérateur de séquence de sortie soit disposé ou que nous finissions de parcourir les éléments en entrée, bien sûr.

Je vais passer directement à du code, puisque c'est si semblable au Where. Ça ne vaut pas non plus la peine de vous montrer la version avec l'index — parce que la différence est minime.

 
CacherSélectionnez
public static IEnumerable<TResult> Select<TSource, TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource, TResult> selector) 
{ 
    if (source == null) 
    { 
        throw new ArgumentNullException("source"); 
    } 
    if (selector == null) 
    { 
        throw new ArgumentNullException("selector"); 
    } 
    return SelectImpl(source, selector); 
} 

private static IEnumerable<TResult> SelectImpl<TSource, TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource, TResult> selector) 
{ 
    foreach (TSource item in source) 
    { 
        yield return selector(item); 
    } 
}

Simple, n'est-ce pas ? Encore une fois, la méthode faisant le « travail » réel est encore plus courte que la validation d'arguments.

Conclusion

Bien que je ne tienne généralement pas à ennuyer mes lecteurs (ce qui peut être surprenant pour certains d'entre vous) ce fut vraiment un billet banal, je l'admets. J'ai souligné « tout comme pour le Where » à plusieurs reprises au point de vous ennuyer délibérément — parce que ça montre à l'évidence qu'il n'y a pas vraiment autant de choses difficiles à comprendre qu'on aurait pu s'y attendre.

Quelque chose de légèrement différent la prochaine fois (ce qui, j'espère, sera dans les prochains jours). Je ne suis pas tout à fait sûr de ce que ce sera, mais il y a encore un très grand nombre de méthodes à traiter…

Remerciements

Je tiens ici à remercier Jon Skeet de m'avoir autorisé à traduire son article Reimplementing LINQ to Objects: Part 3 - "Select" (and a rename...)

Je remercie Tomlev pour sa relecture technique et ses propositions.

Je remercie également Torgar pour sa relecture orthographique et ses propositions.