C #: Iterador vs función que devuelve IEnumerable
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
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);
Leer sobre CQS (separación de consultas de comandos): Wikipedia
En resumen, existen 2 tipos de métodos:
- comandos, por ejemplo
void DoSomething(string data): cambian el estado de su instancia - 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.
La asignación var values = DoSomethingX();no enumera la colección o el iterador que devuelve.
Puede devolver un
List<int>. En este caso, la lista está asignada avalues. Más precisamente: una referencia a esta lista.Si
DoSomethingXes un método iterador, entonces C # crea un objeto de una clase anónima que se implementaIEnumerable<int>como una máquina de estado. Este objeto está asignado avalues.
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.