C #: Iterator vs fonction retournant IEnumerable

Oct 15 2020

Disons que deux méthodes sont disponibles:

IEnumerable<int> DoSomething1();
IEnumerable<int> DoSomething2();

qui modifient l'état de mon objet. Je ne sais pas si la fonction utilise à l' yieldintérieur ou renvoie simplement un fichier List<int>.

Et puis j'aimerais prendre la sortie et passer à deux autres fonctions:

void SendToUser(IEnumerable<int> values);
void PrintOut(IEnumerable<int> values);

Ensuite, juste en fonction de la fonction interface ( DoSomethingX), je ne peux pas dire si c'est une opération valide ou non:

var values = DoSomethingX();
SendToUser(values);
PrintOut(values)

Parce que dans le cas de l'itérateur, cela entraînera un appel DoSomethingXdeux fois.

Est-ce une sorte d'incohérence ou j'utilise iterator / IEnumerablede la mauvaise manière? Où est le problème?

Réponses

4 JohnathanBarclay Oct 15 2020 at 19:13

Si une méthode retourne, IEnumerablevous ne devez vous fier qu'au fait qu'elle est itérable, rien de plus, car l'implémentation de cette méthode est sujette à changement.

Si vous contrôlez la méthode et que vous savez que les consommateurs ont besoin d'un type plus spécifique, renvoyez un type plus spécifique.

Si vous n'avez pas le contrôle et devez implémenter le comportement de liste, vous pouvez toujours convertir le IEnumerableen une liste:

var values = DoSomethingX().ToList();
SendToUser(values);
PrintOut(values);
1 apocalypse Oct 15 2020 at 19:50

En savoir plus sur CQS (séparation des requêtes de commande): Wikipedia

En bref, il existe 2 types de méthodes:

  1. commandes, par exemple void DoSomething(string data)- elles changent l'état de votre instance
  2. requêtes, par exemple IEnumerable<Person> GetPeople(Filter filter)- qui ne change pas d'état, elles renvoient juste des données

Parfois, les commandes peuvent renvoyer une valeur, c'est-à-dire PersonID CreatePerson(...);

Donc, si vous refactorisez votre code pour:

void DoSomething1();
void DoSomething2();
IEnumerable<int> GetData();

cela résoudra votre problème.

Le IEnumerabletype est juste une interface qui vous permet d'itérer sur certains éléments. Vous n'avez aucune idée de ce qu'il y a dans la mise en œuvre concrète.
Habituellement, la mise en œuvre de IEnumerablene devrait avoir aucun effet secondaire, c'est-à-dire changer l'état de certains objets.

Si vous traitez avec IEnumerablecet état de changement, vous pouvez toujours le matérialiser à l'aide de l' .ToList()extension LINQ. Ensuite, vous pouvez le transmettre à n'importe quelle méthode sans effets secondaires supplémentaires.

OlivierJacot-Descombes Oct 15 2020 at 20:11

L'affectation var values = DoSomethingX();n'énumère pas la collection ou l'itérateur qu'elle renvoie.

  1. Il peut renvoyer un fichier List<int>. Dans ce cas, la liste est affectée à values. Plus précisément: une référence à cette liste.

  2. Si DoSomethingXest une méthode itératrice, alors C # crée un objet d'une classe anonyme implémentée en IEnumerable<int>tant que machine à états. Cet objet est affecté à values.

Il n'y a donc aucune différence dans l'utilisation de ces deux IEnumerable<int>. Peu importe qu'il soit basé sur une collection ou sur une méthode itératrice. L'itération commence généralement dans une boucle for-each ou lors de l'appel d'une méthode LINQ telle que ToListou ToArray.

Vous pouvez effectuer une itération manuellement. Vous devez d'abord obtenir l'énumérateur:

IEnumerator<int> enumerator = values.GetEnumerator();

Appelez ensuite les méthodes de cet énumérateur:

while (enumerator.MoveNext()) { // Enumeration starts here.
    int value = enumerator.Current;
    ...
}

Lorsque vous itérez plus IEnumerableou IEnumerable<T>deux fois, son code s'exécute bien sûr deux fois. Si vous pensez que cela peut prendre du temps, par exemple si l'itérateur interroge une base de données ou le système de fichiers, vous devez appeler .ToList()avant de l'appeler deux fois:

var records = QueryDatabaseReturnsIEnumerable().ToList();
SendToUser(records);
PrintOut(records);

D'autre part, en appelant .ToList()lorsque l'énumérable est basé sur une collection, vous effectuez une opération de copie inutile et chronophage.

Habituellement, vous pouvez dire à partir du contexte, si cela est nécessaire ou non. Sinon, le mieux que vous puissiez faire est de tester d'autres interfaces:

IEnumerable<int> values = DoSomethingX();
if (!(values is IList<int>) && !(values is ICollection<int>)) {
    values = values.ToList();
}
SendToUser(values);
PrintOut(values);

Mais je doute que ce soit une bonne pratique. Les interfaces doivent être consommées sans se soucier de leur implémentation. Cela va à l'encontre de leur objectif.