C #: Iterator vs função retornando IEnumerable
Digamos que você tenha dois métodos:
IEnumerable<int> DoSomething1();
IEnumerable<int> DoSomething2();
que modificam o estado do meu objeto. Não sei se a função usa yieldinside ou apenas retorna um List<int>.
E então eu gostaria de pegar a saída e passar para duas outras funções:
void SendToUser(IEnumerable<int> values);
void PrintOut(IEnumerable<int> values);
Então, apenas com base na função interface ( DoSomethingX) não posso dizer se esta é uma operação válida ou não:
var values = DoSomethingX();
SendToUser(values);
PrintOut(values)
Porque no caso do iterador, isso resultará na chamada DoSomethingXduas vezes.
É algum tipo de inconsistência ou estou usando o iterador / IEnumerableda maneira errada? Onde está o problema?
Respostas
Se um método retornar IEnumerable, você deve confiar apenas no fato de que ele é iterável, nada mais, pois a implementação desse método está sujeita a alterações.
Se você tem controle sobre o método e sabe que os consumidores exigem um tipo mais específico, retorne um tipo mais específico.
Se você não tem controle e precisa implementar o comportamento de lista, pode sempre converter o IEnumerableem uma lista:
var values = DoSomethingX().ToList();
SendToUser(values);
PrintOut(values);
Leia sobre CQS (separação de consulta de comando): Wikipedia
Resumindo, existem 2 tipos de métodos:
- comandos, por exemplo
void DoSomething(string data)- eles mudam o estado da sua instância - consultas, por exemplo
IEnumerable<Person> GetPeople(Filter filter)- que não muda de estado, eles apenas retornam alguns dados
Às vezes, os comandos podem retornar valor, ou seja, PersonID CreatePerson(...);
Portanto, se você refatorar seu código para:
void DoSomething1();
void DoSomething2();
IEnumerable<int> GetData();
vai resolver o seu problema.
O IEnumerabletipo é apenas uma interface que permite iterar alguns elementos. Você não tem ideia do que está dentro da implementação concreta.
Normalmente, a implementação de IEnumerablenão deve ter efeitos colaterais, ou seja, alterar o estado de alguns objetos.
Se você está lidando com IEnumerableesse estado de mudanças, você sempre pode materializá-lo usando a .ToList()extensão LINQ. Em seguida, você pode passá-lo para qualquer método sem quaisquer efeitos colaterais extras.
A atribuição var values = DoSomethingX();não enumera a coleção ou iterador que ela retorna.
Pode retornar a
List<int>. Neste caso, a lista é atribuída avalues. Mais precisamente: uma referência a esta lista.Se
DoSomethingXfor um método iterador, o C # cria um objeto de uma classe anônima implementadaIEnumerable<int>como uma máquina de estado. Este objeto está atribuídovalues.
Portanto, não há diferença no uso desses dois IEnumerable<int>. Não importa se é baseado em uma coleção ou em um método iterador. A iteração geralmente começa em um loop for-each ou ao chamar um método LINQ como ToListou ToArray.
Você pode iterar manualmente. Primeiro você precisa obter o enumerador:
IEnumerator<int> enumerator = values.GetEnumerator();
Em seguida, chame os métodos deste enumerador:
while (enumerator.MoveNext()) { // Enumeration starts here.
int value = enumerator.Current;
...
}
Quando você itera mais IEnumerableou IEnumerable<T>duas vezes, é claro que seu código é executado duas vezes. Se você acha que isso pode ser demorado, por exemplo, se o iterador consulta um banco de dados ou o sistema de arquivos, você deve chamar .ToList()antes de chamá-lo duas vezes:
var records = QueryDatabaseReturnsIEnumerable().ToList();
SendToUser(records);
PrintOut(records);
Por outro lado, chamando .ToList()quando o enumerable é baseado em uma coleção, você executa uma memória desnecessária e uma operação de cópia demorada.
Normalmente você pode dizer a partir do contexto, se isso é necessário ou não. Caso contrário, o melhor que você pode fazer é testar outras interfaces:
IEnumerable<int> values = DoSomethingX();
if (!(values is IList<int>) && !(values is ICollection<int>)) {
values = values.ToList();
}
SendToUser(values);
PrintOut(values);
Mas duvido que seja uma boa prática. As interfaces devem ser consumidas sem se preocupar com sua implementação. Fazer isso desafia seu propósito.