Blog Arolla

Mon coloc’ est un IEnumerable : comment éviter les conflits

Mon coloc’ est un IEnumerable. Ne rigolez pas, c’est très sérieux. En tant que développeu·r·se, j’ai loué un petit espace mémoire avec vue sur le CPU. Et je me retrouve avec un Type bizarre, qui a une drôle d’interface. Je suis obligé, c’est le fils du framework. Mais je suis sûr que si j’apprends à bien le connaître, on pourra faire des trucs sympas ensemble ! Il fait venir plein d’amis et même des chiens et des chats. Il a l’air un peu procrastinateur : parfois il accumule des tonnes de trucs à faire, et parfois il travaille comme un fou. Il faut que je le surveille pour m’assurer qu’il ne s’étale pas trop en espace mémoire et CPU.

Mon coloc’ se présente

Les « collections »

Il existe de nombreux types de « collections » (appelées aussi séquences) en .NET, avec différentes possibilités. Les plus courantes sont bien sûr le simple tableau (Array) ou la liste (List). Il y a des « collections » plus élaborées comme Hashset ou ImmutableSortedDictionary, et d’autres plus spécialisées comme HttpHeaders ou PackagePartCollection, sans oublier le résultat d’une requête en base de données comme la collection DbSet. Nous pouvons aussi créer nos propres « collections ».

Toutes ces collections ont un point commun : on peut les énumérer, c’est-à-dire parcourir leurs éléments un par un, du début à la fin. En tant que consommateur·rice de la collection, il n’est pas vraiment nécessaire de savoir de quel type précis elle est. En général, on se contente d’une instruction foreach. Tout cela est permis par le fait que ces « collections » implémentent l’interface IEnumerable.

IEnumerable

Mon coloc’ IEnumerable possède une seule méthode : GetEnumerator(). Elle permet de récupérer un énumérateur (IEnumerator). À son tour, l’énumérateur possède les méthodes MoveNext() et Current qui permettent de parcourir la collection ou « énumérer l’énumérateur de l’énumérable », si vous préférez.

[Exemple : PresentationTest.Explicit_IEnumerable]

[TestMethod]
public void Explicit_IEnumerable()
{
    IEnumerable<string> enumerable = new List<string>() {
        "Cyril",
        "Olfa",
        "Mathilde",
        "Karim",
    };

    using IEnumerator<string> enumerator = enumerable.GetEnumerator(); 
    while (enumerator.MoveNext())
    {
        var person = enumerator.Current;
        Console.WriteLine(person);
    }
}

Notez que l’instruction foreach utilise les méthodes de IEnumerable. Elle peut aussi exploiter des classes qui ont les méthodes GetEnumerator, Current et MoveNext.

LINQ et l’approche ensembliste

IEnumerable nous permet aussi de traiter une collection ou une séquence comme une entité unique, de lui appliquer des traitements pour créer de nouvelles collections, comme trier, filtrer ou projeter. Nous pouvons aussi combiner des « collections » par concaténation ou jointure.
LINQ (language-integrated query) est un jeu de fonctionnalités incluses dans .NET et permettant d’exprimer ces opérations sous une forme ressemblant au langage SQL. Cette formulation améliore la lisibilité des traitements dans de nombreux cas.
L’exemple ci-dessous filtre et trie une liste en utilisant le langage LINQ.
[Exemple : PresentationTests.Linq_Filter_Order_IEnumerable]

[TestMethod]
public void Linq_Filter_Order_IEnumerable()
{
    IEnumerable<string> enumerable = new List<string>() {
        "Cyril",
        "Olfa",
        "Mathilde",
        "Karim",
    };

    var filteredAndOrdered =
        from person in enumerable
        where person.Contains("i")
        orderby person
        select person;

    Check.That(filteredAndOrdered)
         .ContainsExactly("Cyril", "Karim", "Mathilde");
}

Les différentes clauses d’une expression LINQ sont converties en appels de méthodes d’extension de IEnumerable, qui sont présentes dans le namespace System.Linq. On peut également utiliser les méthodes d’extension directement. L’exemple ci-dessous est équivalent au précédent.
[Exemple : PresentationTests.Linq_Method_Filter_Order_IEnumerable]

[TestMethod]
public void Linq_Method_Filter_Order_IEnumerable()
{
    IEnumerable<string> enumerable = new List<string>() {
        "Cyril",
        "Olfa",
        "Mathilde",
        "Karim",
    };
    var filteredAndOrdered = enumerable
        .Where(person => person.Contains("i"))
        .OrderBy(person => person);
    Check.That(filteredAndOrdered)
        .ContainsExactly("Cyril", "Karim", "Mathilde");
}

Les clauses where et orderby ont été remplacées par les méthodes Where et OrderBy ; comme il n'y a pas de projection, la clause select disparaît.

Mon coloc’ procrastine : calcul différé et non différé

Il est important de comprendre que la requête est exécutée au moment où l’énumérable est itéré (appel de IEnumerable.GetIterator), et non lors de sa création. Cela se passe lors d’une instruction foreach, ou lors de l’appel de méthode telle que les méthodes de conversion (ToArray, ToList...) ou de comptage (Count, Sum...) et d’autres. Par conséquent, les méthodes différées telles que Where ou OrderBy ne sont exécutées que si on itère avec un foreach sur la collection ou si on transforme le résultat en appliquant une des méthodes de calcul non différées ou instantanées.

Ainsi il faut faire attention lors de l’utilisation mixte de méthodes différées et non différées. Par exemple, si on appelle la méthode Count puis on itère dessus avec un foreach, la séquence sera itérée deux fois.

Pour tous les opérateurs cités ci-dessous, vous pouvez trouver des exemples d’utilisation dans le dépôt github mentionné à la fin de l’article.

Opérateurs différés

Les opérateurs ou méthodes différés tel que Where, comme leur nom l’indique, ne sont pas exécutés lors de l’appel mais leur calcul est différé au moment où on itère dessus. Ces opérateurs sont reconnaissables facilement car le type de retour est soit un IEnumerable soit IOrderEnumerable.
Prenons l’exemple suivant. Le calcul de la séquence namesContainingTo n’est exécuté qu’à l’appel de la méthode ToList.
[Exemple : EnumerableTest.Should_return_names_containing_to]

var names = new List<string> {"Aline", "Tom", "Fen", "Fatou", "Amine", "Jackito",
    "Ritom", "Yosr", "Cyril", "Olfa", "Mathilde", "Nabil"};

IEnumerable<string> namesContainingTo = names.Where(n => n.Contains("to"));
var namesContainingToList = namesContainingTo.ToList();

Les opérateurs différés peuvent être groupés fonctionnellement. Ainsi, on retrouve les types suivants.
Dans ce qui suit, chaque paragraphe correspond à un groupe fonctionnel d’opérateurs différés. A la fin de chaque paragraphe, on trouve le nom de la classe de test contenant les exemples d’utilisation de ces opérateurs sous cette forme [Test : ClassTestName].

Restriction

Where est utilisé pour filtrer les éléments d’une séquence. Il a donc besoin de la séquence à filtrer et un prédicat de restriction. On a deux signatures de cette méthode. La première correspond à un prédicat avec un seul argument qui correspond à l’élément courant. La deuxième signature correspond à un prédicat avec deux arguments, l’élément courant et son index.

[Test : RestrictionTest]

Projection

Select est utilisé pour projeter une séquence d’un type donné (correspondant au type des éléments de la collection en entrée) vers un autre type (qui peut être différent du type en entrée). Il retourne un élément en sortie pour chaque élément en entrée. Autrement dit le nombre d’éléments du résultat est égal au nombre d’éléments de la séquence d’origine.

SelectMany est un autre opérateur de projection. Contrairement à Select il peut retourner zéro, un ou plusieurs éléments de sortie pour un élément en entrée.

[Test : SelectTest]

Pagination

Take retourne un sous ensemble de la séquence initiale à partir du premier. Le sous ensemble correspond au nombre d’éléments passé en paramètre.

TakeWhile retourne un sous ensemble de la séquence initiale à partir du premier. Le sous ensemble correspond au nombre d’éléments passé en paramètre et qui correspondent à la condition passée en deuxième paramètre sous forme de prédicat.

Skip saute le nombre d’éléments souhaités pour retourner le reste de la séquence.
SkipWhile ignore les éléments de la séquence d’entrée tant que la condition est vérifiée et renvoie les éléments suivants.

[Test : SkipDeleteTest]

Concaténation

Concat concatène deux séquences. Les deux séquences doivent avoir le même type d’objet.

[Test : ConcatTest]

Tri

OrderBy trie les éléments d’une séquence en entrée dans un ordre croissant. Le tri se base sur le prédicat keySelector qui retourne la valeur clé pour classer les éléments.

OrderByDescending trie les éléments d’une séquence en entrée dans un ordre décroissant. Pareil que OrderBy le tri se base sur le prédicat keySelector.

Il faut faire attention avec les méthodes de tri car si deux éléments ont la même clé, le tri n’est pas garanti de retourner le même ordre tout le temps. Les éléments ayant la même clé peuvent être retournés dans un ordre lors d’un premier appel et retournés dans l’ordre inverse lors d’un deuxième appel.

ThenBy et ThenByDescending s’appliquent sur une séquence de type IOrderedEnumerable et se basent sur le prédicat keySelector pour trier la collection. Donc ils peuvent être utilisés à la suite d’un appel à OrderBy ou OrderByDescending.

Reverse renvoie une séquence en inversant les éléments de la collection en entrée.

[Test : OrderTest]

Jointure

Join permet d’effectuer la jointure de deux collections en se basant sur les clés des éléments des deux collections.

GroupJoin permet d’effectuer la jointure de deux collections. A la différence de Join, elle regroupe les éléments de la même clé dans une la même collection.

[Test : JoinTest]

Regroupement

GroupBy permet de regrouper les éléments d’une séquence en entrée. Le prédicat keySelector permet de définir la clé du regroupement, on peut spécifier le comparateur à utiliser pour vérifier que deux clés sont égales et on peut spécifier l’élément qui apparaitra dans le résultat du regroupement.

[Test : GroupTest]

Initialisation

Distinct permet de supprimer les doublons d’une collection.

Union permet de réunir deux collections sans les doublons. Cette méthode va donc enlever les doublons des deux collections avant de réunir les résultats.

Intersect retourne l’intersection des deux collections en enlevant les doublons.

[Test : InitializationOperationTest]

Conversion

Cast convertit tous les éléments d’une collection vers le type spécifié.

OfType retourne les éléments qui correspondent au type spécifié.

AsEnumerable convertit la collection en entrée en un IEnumerable.

[Test : ConversionTest]

Génération

Les méthodes de génération ne sont pas des méthodes d’extension contrairement aux méthodes vues ci-dessus.

Range crée une collection d’entiers. Cette collection commence à l’entier passé en paramètre et contient le nombre d’éléments passé en deuxième paramètre.

Repeat crée une collection contenant la même valeur autant de fois que le nombre d’éléments passé en deuxième paramètre.

Empty génère une collection vide du type choisi.

[Test : GenerationTest]

Et des opérateurs dédiés aux éléments

DefaultIfEmpty retourne une collection avec un élément par défaut si la collection d’origine est vide.

[Test : ConversionTest]

Opérateurs non différés

En plus des opérateurs différés connaissables à partir de leurs signatures retournant les types IEnumerable ou IOrderedEnumerable, on trouve les opérateurs non différés. Ces opérateurs retournent des valeurs calculées de type autre que IEnumerable et IOrderedEnumerable. Ces opérateurs agissent sur les éléments de la collection pour les compter, les convertir, vérifier l’égalité...

Conversion

Ces opérateurs permettent de convertir une collection de type IEnumerable vers le type souhaité.

ToArray() pour créer un tableau T[]

ToList() pour créer une liste d’éléments de type T

ToDictionary<K, E>() pour créer un dictionnaire avec des clés de type K et des éléments de type E. Les clés doivent être uniques dans la collection en entrée sinon cet opérateur lance une exception.

ToLookup<K, T>() pour créer un objet de type Lookup<K, E>. Contrairement à ToDictionnary(), cet opérateur accepte les clés qui ne sont pas uniques. On peut donc accéder aux éléments de la même clé en indexant le tableau en utilisant cette clé.

Egalité

SequenceEqual vérifie si deux collections sont égales en utilisant la méthode Equals.

[Test : EqualityTest]

Comptage

Ces opérateurs effectuent des comptes sur les éléments de la collection d’entrée.

Count() retourne le nombre d’éléments de la collection, ou le nombre d’éléments correspondant à une condition en paramètre.

LongCount() retourne le nombre d’éléments au format Int64.

Sum() retourne la somme des valeurs contenues dans les éléments de la collection en entrée.

Min() retourne la plus petite valeur de la séquence d’entrée.

Max() retourne la plus grande valeur de la séquence d’entrée.

Average() retourne la moyenne des valeurs contenues dans une collection.

Aggregate() exécute une fonction sur chaque élément de la collection et la valeur retournée par une itération passe en entrée de l’itération suivante.

[Test : CountTest]

Les quantificateurs

Les quantificateurs permettent de tester l’existence d’une valeur dans une collection.

Any() permet de vérifier si la collection contient un élément si on utilise la signature sans paramètre et vérifie si un élément correspondant à la condition en paramètre existe si on utilise la signature paramétrée.

All() retourne true si tous les éléments de la collection vérifient la condition en paramètre.

Contains() retourne true si l’élément passé en paramètre est égal à un élément de la collection.

[Test : QuantifierTest]

Les opérateurs de récupération des éléments

First() retourne le premier élément de la collection ou le premier élément de la collection qui vérifie la condition en paramètre. Elle lance une exception de type InvalidOperationException si aucun élément n'est trouvé.

FirstOrDefault() est équivalent à la méthode First() mais retourne la valeur par défaut du type si aucun élément n’est trouvé.

Last() retourne le dernier élément correspondant au prédicat passé en paramètre. Elle lance une exception de type InvalidOperationException si aucun élément n’est trouvé.

LastOrDefault() est équivalent à Last() mais retourne la valeur par défaut du type si aucun élément n’est trouvé.

Single() retourne le seul élément correspondant au prédicat passé en paramètre. Elle retourne une InvalidOperationException si aucun élément ne correspond au prédicat.

SingleOrDefault() est équivalent à Single() mais retourne la valeur par défaut du type si aucun élément n’est trouvé.

ElementAt() retourne l’élément dont l’index est spécifié en paramètre. Elle lance une exception de type ArgumentOutOfRangeException si l’index est négatif ou supérieur au nombre d’éléments de la collection.

ElementAtOrDefault() est équivalent à ElementAt() mais retourne null si l’index est négatif ou supérieur à la taille de la collection.

[Test : FounderTest]

Comment éviter les conflits avec mon coloc’

Il faut retenir que chaque appel d’un opérateur non différé (Count, Any...) engendre une itération sur tout ou partie des les éléments de la collection . Deux appels consécutifs d’opérateurs non différés engendrent donc deux itérations sur la collection.

Afin d’optimiser l’exécution, il vaut mieux éviter les appels successifs des opérateurs non différés. Ainsi on évite une énumération multiple de la séquence.

Dans les sections suivantes, on va découvrir cela dans des cas d’utilisation réels.

Les cas de figure

Lorsque je connais déjà bien mon coloc’, je sais quand il va procrastiner ou travailler – c’est-à-dire que si l’IEnumerable est produit dans un code maîtrisé, nous pouvons déterminer s’il contient des opérations différées, et quelles sont ces opérations.

Lorsque l’IEnumerable provient d’un code que nous ne maîtrisons pas, nous ne savons pas s’il contient des opérations différées. La documentation peut donner des indications, mais le comportement peut aussi changer lors d’une mise à jour.

IEnumerable à l’intérieur du domaine

Réception d’un énumérable non différé

Dans ce cas de figure, notre fonction utilise un énumérable qui provient d’une base de code maîtrisée. Nous avons donc une bonne connaissance de cet énumérable : nous savons s’il est différé ou non, et connaissons son type (la plupart du temps, List ou tableau).

Si l’énumérable n’est pas différé, nous n’aurons pas de problème en effectuant plusieurs énumérations : nous ne ferons que parcourir plusieurs fois une structure.

Réception d’un énumérable différé

Si l’énumérable est différé, toutes les opérations différées sont effectuées à chaque fois que nous énumérons la collection. Cela peut aller jusqu’à répéter une requête en base de données . Dans ce cas nous devons éviter d’effectuer plusieurs énumérations. Les possibilités sont alors les suivantes :

Ajouter des opérations différées

La fonction AddDeferredOperations ci-dessous ne provoque aucune énumération.

[Exemple : EnumerableKnownReceiveTests. Receive_Deferred_AddDeferred_NoEnumeration]

public IEnumerable<Person> AddDeferredOperations(IEnumerable<Person> friends)
{
    return friends
        .Where(friend => friend.Name.StartsWith("F"))
        .OrderBy(friend => friend.Name);
}

Effectuer une et une seule énumération, pour produire un non-énumérable :

[Exemple : EnumerableKnownReceiveTests.Receive_Deferred_AddImmediate_Enumerates]

public int AddImmediateOperation(IEnumerable<Person> friends)
{
    return friends.Count();
}

Effectuer une et une seule énumération, pour effectuer une opération sur chaque élément

[Exemple : EnumerableKnownReceiveTests. Receive_Deferred_Foreach_Enumerates]

private void DoSomethingForEach(IEnumerable<Person> friends)
{
    foreach (var friend in friends)
    {
        _logger.Debug("Applying operation");
    }
}

Effectuer une et une seule énumération, pour produire un nouvel énumérable

[Exemple : EnumerableKnownReceiveTests. Receive_Deferred_Transform_Enumerates]

private ILookup<char, Person> GroupByInitial(IEnumerable<Person> friends)
{
    return friends.ToLookup(friend => friend.Name[0]);
}

Puisque nous maîtrisons le code, pourquoi devrions-nous recevoir un IEnumerable ? Pourquoi pas un type concret comme List ou Array ? Même dans ce cas, recevoir un énumérable a plusieurs avantages : cela permet d’une part de traiter plusieurs types concrets avec le même code (évidemment « code maîtrisé » devient un peu plus flou dans ce cas) ; d’autre part cela permet de maintenir les opérations différées, avec en point de mire un scénario « stream » (ceci pourrait faire l’objet d’un prochain article).

Retourner un énumérable

Lorsque notre fonction renvoie une collection, la typer en énumérable présente plusieurs avantages.

Cela nous réserve le droit de changer le type concret à l’avenir, sans modifier le contrat. Ainsi nos consommateur·rice·s n’ont pas à modifier leur code si nous décidons de changer d’implémentation. Par exemple, si nous travaillons en outside-in, nous écrivons un code qui utilise un service qui renvoie IEnumerable. Pour une implémentation « bouchon » du service, nous renvoyons simplement un tableau en dur. Pour une « vraie » implémentation, nous utiliserons peut-être ImmutableSortedSet ! De plus, si nous faisons plusieurs implémentations, nous ne sommes pas tenus d’utiliser le même type pour toutes.

Lorsque nous voulons renvoyer un énumérable différé, nous devons bien sûr utiliser IEnumerable et bien l’indiquer dans la documentation !

Utiliser IEnumerable décourage aussi les consommateur·rice·s de modifier la collection renvoyée.

À l’inverse, si la méthode n’est pas publique (pas de contrat à respecter), et si on souhaite avoir un énumérable non différé, on peut renvoyer un type concret comme Array ou ImmutableList.

Je ne maîtrise pas la création de la collection

La collection vient d’une lib externe

Souvent dans notre code on a besoin d’appeler une librairie ou une API externe pour récupérer des données (de référence ou calculées). Le contrat du service externe peut très bien nous retourner une collection de type IEnumerable.

Dans le répertoire “CaseNotMasteringExternalData”, on a choisi de créer deux projets. “MyApplication” représente notre application qui a besoin d’un service externe de la librairie “ExternalLib”. Ce service simule la recherche des points relais les plus proches d’une adresse et retourne une collection de type IEnumerable.

Dans notre application, on va appeler ce service. Mais étant donné qu’on ne maîtrise pas le contenu, il vaut mieux ne pas retourner la collection telle quelle . Il vaut mieux aussi éviter d'appliquer un opérateur différé sur la collection et retourner directement son résultat. Pour éviter tout problème de synchronisation des données, il vaut mieux calculer le résultat du service externe immédiatement après son appel. Dans notre exemple, on va convertir la collection IEnumerable en IEnumerable (PickupPoint est une entité de notre domaine) et on calcule le résultat avec l’opérateur non différé ToList(). Ainsi, on ne dépend plus du comportement du service externe et on isole bien notre application.

Dépendance externe

Vous pouvez trouver l’exemple suivant dans le github.

Le contenu de la collection change

Imaginons ce cas d’utilisation :

  • On maîtrise son code et on a une méthode qui renvoie une liste d’éléments (3 éléments en l’occurrence).
  • On veut sauter le premier élément pour un besoin spécifique. On va donc créer la variable “newSequence” qui sera le résultat de la méthode Skip(1). On s’attend dans ce cas à sauter le premier élément et obtenir les deux derniers.
  • Pour une raison qui nous échappe, la collection d’origine a changé et le premier élément est supprimé.
  • Quand on calcule la collection “newSequence”, on ne trouve que le troisième élément qui reste alors qu’on s’attendait aux deux derniers.

Ce cas est le fruit de l’appel de l’opérateur différé et donc le résultat ne sera calculé qu’au premier accès à l’itérateur de la collection.

Pour éviter ce piège, il vaut mieux encapsuler la collection dans une classe (first class pattern) et rendre cette classe immuable. Donc si on veut récupérer une collection, c’est un nouvel objet qui est retourné avec les données immuables et qui ne changeront pas en cours de route.

Le test “SkipDeleteTest” dans le github reprend ce cas de figure.

Les cas piégeux

OrderBy différé

Le tri est une opération coûteuse et OrderBy est un opérateur différé. Il faut donc éviter l’énumération multiple, qui réeffectue le tri à chaque fois. Sinon mon coloc’ va consommer trop de CPU !

Premier exemple : Dans le code ci-dessous, on génère un énumérable, on applique un ToArray() et on calcule deux fois sa somme. Le tableau de profiling indique que les sommes s’exécutent en 15ms, l’essentiel du temps est attribué au ToArray().

[TestMethod]
public void No_OrderBy_Resolved()
{
    IEnumerable<long> GetEnumerable()
    {
        Console.WriteLine("Generating Enumerable");
        return Enumerable.Range(1, 1000000)
            .Select(i => (long)i);
    }

    var unsortedEnumerable = GetEnumerable().ToArray();

    var x1 = unsortedEnumerable.Sum();
    var x2 = unsortedEnumerable.Sum();
}

Second exemple : le même code sans le ToArray(). Comme Sum() est le premier opérateur non différé, le travail est effectué deux fois.

[TestMethod]
public void No_OrderBy_Deferred()
{
    IEnumerable<long> GetEnumerable()
    {
        Console.WriteLine("Generating Enumerable");
        return Enumerable.Range(1, 1000000)
            .Select(i => (long)i);
    }

    var unsortedEnumerable = GetEnumerable();

    var x1 = unsortedEnumerable.Sum();
    var x2 = unsortedEnumerable.Sum();
}

Exposer un IEnumerable en résultat d’une requête en base

On peut choisir de retourner le type IEnumerable pour une méthode qui récupère une collection d’objets de type Person d’une base de données. Jusque-là rien d’inquiétant et pourtant on peut tomber dans un piège classique.

En effet, il est possible de se retrouver avec une exception de type ObjectDisposedException dans le cas où la connexion est fermée alors que la collection IEnumerable est utilisée et manipulée via des opérateurs différés.

[TestMethod]
public void Should_Throw_an_object_disposed_exception_when_get_enumerable_elements_after_disposing_dbcontext()
{
    IEnumerable<Person> queriedPersons = null;
    using (var dbContext = new PersonModelContext())
    {
        queriedPersons = GetPersons(dbContext);
    }

    Check.ThatCode(() => queriedPersons.Count())
	 .Throws<ObjectDisposedException>();
}

La méthode retourne un IEnumerable mais réellement elle retourne un IQueryable qui permet le requêtage en base en mode connecté. Par contre en mode déconnecté, si on énumère un IQueryable, une exception est lancée puisque la collection n’est plus accessible et la connexion est fermé e. IQueryable permet de gérer facilement les paginations aussi, mais pareil, si la connexion est fermée, on n’arrivera plus à accéder aux données. C’est pourquoi on conseille, dans ces cas, de calculer les collections en appliquant un opérateur non différé pour figer le résultat.

Dans cet exemple, certains penseront qu’un simple déplacement du “Check” à l’intérieur du “using”, le test passera au vert mais l'idéal serait plutôt de corriger la méthode GetPersons en calculant le résultat avant de le renvoyer.

Conclusion

Nous avons appris à connaître un peu mon coloc’ IEnumerable. Pour cohabiter pacifiquement, lorsque l’IEnumerable provient de l’extérieur du domaine, il est prudent de calculer l’énumération. À l’intérieur du domaine, nous devons nous méfier de l’utilisation multiple des opérateurs différés.

Mais il y a encore beaucoup de choses à dire sur le sujet, nous y reviendrons probablement bientôt !

GitHub

Tous les exemples sont dans le repo suivant :
https://github.com/iAmDorra/Enumerable

Références

https://docs.microsoft.com/fr-fr/dotnet/api/system.collections.generic.ienumerable-1?view=net-5.0

Plus de publications
Plus de publications

Comments are closed.