Introduction

Ce billet sera court et il y en aura probablement d'autres plus courts à venir. Je pense qu'il est logique de couvrir uniquement dans un billet les différents opérateurs qui sont vraiment similaires. (Count et LongCount me viennent à l'esprit). J'attends vos avis — si vous préférez des billets « trapus », s'il vous plaît dites-le dans les commentaires.

Ce billet portera sur l'opérateur de génération Range.

Qu'est-ce que c'est ?

Range n'a qu'une seule signature :

 
CacherSélectionnez
public static IEnumerable<int> Range( 
    int start, 
    int count)

Contrairement à la plupart des méthodes LINQ, ce n'est pas une méthode d'extension — c'est une bonne vieille méthode statique. Elle retourne un objet itérable qui donnera « count » entiers à partir de « start » et incrémentera à chaque fois — ainsi un appel à Enumerable.Range (6, 3) donnerait 6, puis 7, puis 8.

Comme elle n'attend pas une séquence en entrée, il n'y a aucun sens d'utiliser un flux ou une mémoire tampon pour l'entrée, mais :

  • les arguments doivent être validés rapidement, count ne peut pas être négatif, et il ne peut être de telle sorte que tout élément de la plage puisse déborder de Int32 ;
  • les valeurs seront cédées de façon différée - Range ne devrait pas être compliqué, plutôt que de créer (par exemple) un tableau de « count » éléments et à les renvoyer comme valeur de retour.

Comment allons-nous tester ?

Le test d'une bonne vieille méthode statique nous apporte un nouveau défi en termes de commutation entre l'implémentation « normale » de LINQ et celle d'Edulinq. Il s'agit d'un artefact des espaces de noms que j'utilise — les tests sont dans Edulinq.Tests, et l'implémentation est dans Edulinq. Les espaces de noms « Parent » sont toujours pris en compte quand le compilateur essaie de trouver un type, et ils prennent la priorité sur quoi que ce soit utilisant des directives — même une directive using tente explicitement de donner un alias au nom d'un type.

La solution (un peu moche) à ça que j'ai choisie est d'inclure une directive using pour créer un alias qui ne pourrait autrement pas être résolu — dans ce cas, RangeClass. La directive using fera un alias pour RangeClass vers System.Linq.Enumerable ou Edulinq.Enumerable. Les tests utilisent tous RangeClass.Range. J'ai aussi changé la façon dont je suis passé entre les différentes implémentations — j'ai maintenant deux configurations de projet, dont l'une définit le symbole de préprocesseur NORMAL_LINQ, et l'autre pas. Le RangeTest classe contient donc :

 
CacherSélectionnez
#if NORMAL_LINQ 
using RangeClass = System.Linq.Enumerable; 
#else 
using RangeClass = Edulinq.Enumerable; 
#endif

Il existe d'autres voies à cette approche, bien sûr :

  • je pourrais passer les tests à un autre espace de noms ;
  • je pourrais faire que les références du projet dépendent de la configuration… de sorte que la configuration « Normal LINQ » ne puisse pas être référencée dans le projet d'implémentation Edulinq, et la configuration de l'« implémentation Edulinq » ne serait pas référencée dans System.Core. Je pourrais alors juste utiliser Enumerable.Range avec une directive appropriée System.Linq à l'aide de la directive de préprocesseur NORMAL_LINQ, comme pour chacun des autres tests.

J'aime l'idée de la deuxième approche, mais cela signifie que je vais devoir manuellement bricoler avec le fichier projet de test — Visual Studio n'offre aucun moyen pour ajouter de façon conditionnelle une référence. Je peux le faire à une date ultérieure… Les réflexions sont les bienvenues.

Qu'allons-nous tester ?

Il n'y en a pas beaucoup que nous pouvons vraiment tester pour les plages — je n'ai que huit tests, dont aucun n'est particulièrement intéressant :

  • une plage valide simple doit regarder à droite lorsqu'elle est testée avec AssertSequenceEqual ;
  • la valeur de départ devrait être autorisée à être négative ;
  • Range (Int32.MinValue, 0) est une plage vide ;
  • Range (Int32.MaxValue, 1) donne simplement Int32.MaxValue ;
  • le nombre ne peut pas être négatif ;
  • le nombre peut être nul ;
  • start + count-1 ne peut pas dépasser Int32.MaxValue (ainsi Range (Int32.MaxValue, 2) n'est pas valide) ;
  • start + count - 1 peut être Int32.MaxValue (ainsi Range (Int32.MaxValue, 1) est valide).

Les deux derniers sont testés avec quelques exemples différents chacun — un début important et un petit nombre, un petit début et un nombre important, et des valeurs « assez grandes » pour les deux, le début et le nombre.

Notez que je n'ai pas tous les tests pour l'évaluation paresseuse — alors que je pouvais vérifier que la valeur de retour n'implémente aucune des autres interfaces de collection, il serait un peu bizarre de le faire. D'autre part, nous avons des tests qui ont un nombre énorme — de telle sorte que tout ce qui a vraiment essayé d'affecter une collection de cette taille serait presque certainement un échec…

Voyons l'implémentation !

Ce ne sera sûrement pas une surprise maintenant que nous allons utiliser une implémentation divisée, avec une méthode publique qui effectue la validation d'arguments rapidement, puis utilise une méthode privée avec un bloc itérateur pour effectuer l'itération actuelle.

Après avoir validé les arguments, nous savons que nous n'aurons jamais de débordement des limites de Int32, afin que nous puissions être assez décontracté dans la partie principale de la mise en œuvre.

 
CacherSélectionnez
public static IEnumerable<int> Range(int start, int count) 
{ 
    if (count < 0) 
    { 
        throw new ArgumentOutOfRangeException("count"); 
    } 
    // Convertir toute chose en long évite des débordements. Il existe d’autres moyens pour vérifier
    // le débordement, mais ce moyen rend le code correct dans la manière la plus évidente. 
    if ((long)start + (long)count - 1L > int.MaxValue) 
    { 
        throw new ArgumentOutOfRangeException("count"); 
    } 
    return RangeImpl(start, count); 
} 

private static IEnumerable<int> RangeImpl(int start, int count) 
{ 
    for (int i = 0; i < count; i++) 
    { 
        yield return start + i; 
    } 
}

Juste quelques points à noter ici :

  • on peut dire que c'est la combinaison de « start » et « count », qui n'est pas valide dans la deuxième vérification, plutôt que de simplement compter. Il serait peut-être joli pour permettre ArgumentOutOfRangeException (ou ArgumentException en général) de blâmer plusieurs arguments plutôt qu'un seul. Cependant, en utilisant « count » on trouve l'implémentation de la plateforme ;
  • il y a d'autres façons de réaliser la deuxième vérification, et je n'ai certainement pas besoin de faire tous les opérandes dans les longues expressions. Cependant, je pense que c'est le code le plus simple qui est clairement corrigé sur la base de la documentation. Je n'ai pas besoin de penser à toutes sortes de situations différentes et vérifier qu'elles fonctionnent toutes. L'arithmétique sera clairement valide lors de l'utilisation la plage de valeurs Int64, je n'ai donc pas besoin de m'inquiéter du débordement, et je n'ai pas besoin d'examiner un contexte vérifié ou non ;
  • il y a aussi d'autres moyens de boucle dans de la méthode d'un bloc itérateur privé, mais je pense que c'est le plus simple. Une autre solution de rechange évidente et facile est de garder deux valeurs, une pour le nombre de valeurs cédées et l'autre pour la prochaine valeur à céder, et les incrémenter la fois à chaque itération. Une approche plus complexe consisterait à utiliser une seule variable de la boucle — mais vous ne pouvez pas utiliser « value < start + count » au cas où la valeur finale est exactement Int32.MaxValue, et vous ne pouvez pas utiliser « value <= start + count - 1 » au cas où les arguments sont (int.MinValue, 0). Plutôt que de considérer toutes les affaires transfrontalières, je suis allé pour une solution évidemment correcte. Si vous vous souciez vraiment, vraiment de la performance de Range, vous voudriez enquêter sur les autres options diverses.

Avant d'écrire ce billet, je n'ai pas eu de bons tests pour Range (Int32.MaxValue, 1) et Range (Int32.MinValue, 0)… mais comme ils pourraient facilement tromper comme mentionné ci-dessus, je les ai maintenant inclus. Je trouve intéressant de voir comment le fait d'envisager des implémentations alternatives suggère des tests supplémentaires.

Conclusion

« Range » est une méthode utile à implémenter afin de tester d'autres opérateurs — « Count » en particulier. Maintenant que j'ai commencé sur les méthodes de non-extension quoique je puisse aussi bien faire les deux autres (Empty et Repeat). J'ai déjà implémenté « Empty », et j'espère être en mesure de le rédiger aujourd'hui. « Repeat » ne devrait pas prendre beaucoup plus de temps, et nous pourrons ensuite passer à « Count » et « LongCount ».

Je pense que ce code est un bon exemple de situations où il vaut la peine d'écrire du code « balourd » qui ressemble à de la documentation, plutôt que d'essayer d'écrire plus court possible, le code peut être légèrement plus efficace ce qui est plus difficile à penser. Sans doute il y aura plus de cela dans les futurs billets…

Remerciements

Je tiens ici à remercier Jon Skeet de m'avoir autorisé à traduire son article Reimplementing LINQ to Objects: Part 4 - Range.

Je remercie Tomlev pour sa relecture technique et ses propositions.

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