C #: Iterador vs función que devuelve IEnumerable

Oct 15 2020

Digamos que tenemos dos métodos:

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

que modifican el estado de mi objeto. No sé si la función usa yieldinside o simplemente devuelve un List<int>.

Y luego me gustaría tomar la salida y pasar a otras dos funciones:

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

Entonces, solo en función de la interfaz de función ( DoSomethingX), no puedo decir si esta es una operación válida o no:

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

Porque en el caso del iterador resultará en llamar DoSomethingXdos veces.

¿Es esto algún tipo de inconsistencia o estoy usando el iterador / IEnumerablede manera incorrecta? ¿Dónde está el problema?

Respuestas

4 JohnathanBarclay Oct 15 2020 at 19:13

Si un método regresa IEnumerable, solo debe confiar en el hecho de que es iterable, nada más, ya que la implementación de ese método está sujeta a cambios.

Si tiene control sobre el método y sabe que los consumidores requieren un tipo más específico, devuelva un tipo más específico.

Si no tiene el control y necesita implementar el comportamiento de la lista, siempre puede convertir el IEnumerableen una lista:

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

Leer sobre CQS (separación de consultas de comandos): Wikipedia

En resumen, existen 2 tipos de métodos:

  1. comandos, por ejemplo void DoSomething(string data): cambian el estado de su instancia
  2. consultas, por ejemplo IEnumerable<Person> GetPeople(Filter filter), que no cambia de estado, solo devuelven algunos datos

A veces, los comandos pueden devolver valor, es decir PersonID CreatePerson(...);

Entonces, si refactoriza su código para:

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

resolverá tu problema.

El IEnumerabletipo es solo una interfaz que le permite iterar sobre algunos elementos. No tienes idea de lo que hay dentro de la implementación concreta.
Por lo general, la implementación de IEnumerableno debería tener efectos secundarios, es decir, cambiar el estado de algunos objetos.

Si está lidiando con IEnumerableese estado de cambios, siempre puede materializarlo usando la .ToList()extensión LINQ. Luego, puede pasarlo a cualquier método sin efectos secundarios adicionales.

OlivierJacot-Descombes Oct 15 2020 at 20:11

La asignación var values = DoSomethingX();no enumera la colección o el iterador que devuelve.

  1. Puede devolver un List<int>. En este caso, la lista está asignada a values. Más precisamente: una referencia a esta lista.

  2. Si DoSomethingXes un método iterador, entonces C # crea un objeto de una clase anónima que se implementa IEnumerable<int>como una máquina de estado. Este objeto está asignado a values.

Por lo tanto, no hay diferencia en el uso de estos dos IEnumerable<int>. No importa si se basa en una colección o en un método iterador. La iteración generalmente comienza en un bucle for-each o cuando se llama a un método LINQ como ToListo ToArray.

Puede iterar manualmente. Primero tienes que obtener el enumerador:

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

Luego llame a los métodos de este enumerador:

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

Cuando itera una IEnumerableo IEnumerable<T>dos veces, entonces, por supuesto, su código se ejecuta dos veces. Si cree que esto puede llevar mucho tiempo, por ejemplo, si el iterador consulta una base de datos o el sistema de archivos, debe llamar .ToList()antes de llamarlo dos veces:

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

Por otro lado, al llamar .ToList()cuando el enumerable se basa en una colección, realiza una operación de copia de memoria innecesaria y que consume mucho tiempo.

Por lo general, puede decir del contexto si es necesario o no. De lo contrario, lo mejor que puede hacer es probar otras interfaces:

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

Pero dudo que sea una buena práctica. Las interfaces deben consumirse sin preocuparse por su implementación. Hacerlo desafía su propósito.