C #: Iterator vs função retornando IEnumerable

Oct 15 2020

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

4 JohnathanBarclay Oct 15 2020 at 19:13

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);
1 apocalypse Oct 15 2020 at 19:50

Leia sobre CQS (separação de consulta de comando): Wikipedia

Resumindo, existem 2 tipos de métodos:

  1. comandos, por exemplo void DoSomething(string data)- eles mudam o estado da sua instância
  2. 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.

OlivierJacot-Descombes Oct 15 2020 at 20:11

A atribuição var values = DoSomethingX();não enumera a coleção ou iterador que ela retorna.

  1. Pode retornar a List<int>. Neste caso, a lista é atribuída a values. Mais precisamente: uma referência a esta lista.

  2. Se DoSomethingXfor um método iterador, o C # cria um objeto de uma classe anônima implementada IEnumerable<int>como uma máquina de estado. Este objeto está atribuído values.

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.