Manipulando aviso para possível enumeração múltipla de IEnumerable

No meu código na necessidade de usar um IEnumerable várias vezes, assim, obter o erro Resharper de “Possível enumeração múltipla de IEnumerable “.

Código de amostra:

 public List Foo(IEnumerable objects) { if (objects == null || !objects.Any()) throw new ArgumentException(); var firstObject = objects.First(); var list = DoSomeThing(firstObject); var secondList = DoSomeThingElse(objects); list.AddRange(secondList); return list; } 
  • Eu posso alterar o parâmetro de objects para ser List e, em seguida, evitar a enumeração múltipla possível, mas, em seguida, não obtenho o object mais alto que posso manipular.
  • Outra coisa que posso fazer é converter o IEnumerable para List no início do método:

  public List Foo(IEnumerable objects) { var objectList = objects.ToList(); // ... } 

Mas isso é apenas estranho .

O que você faria nesse cenário?

    O problema com o uso de IEnumerable como parâmetro é que ele informa aos chamadores “Desejo enumerar isso”. Não lhes diz quantas vezes você deseja enumerar.

    Eu posso alterar o parâmetro de objects para ser List e, em seguida, evitar a enumeração múltipla possível, mas, em seguida, não obtenho o object mais alto que posso manipular .

    O objective de pegar o object mais alto é nobre, mas deixa espaço para muitas suposições. Você realmente quer que alguém passe uma consulta LINQ to SQL para esse método, apenas para você enumerá-lo duas vezes (obtendo resultados potencialmente diferentes a cada vez?)

    A falta semântica aqui é que um chamador, que talvez não tenha tempo para ler os detalhes do método, pode presumir que você só faz uma iteração uma vez – então eles lhe passam um object caro. Sua assinatura de método não indica de qualquer maneira.

    Ao alterar a assinatura do método para IList / ICollection , você, pelo menos, deixará mais claro para o chamador quais são suas expectativas e poderá evitar erros dispendiosos.

    Caso contrário, a maioria dos desenvolvedores que olharem para o método pode presumir que você só faz uma iteração uma vez. Se tomar um IEnumerable é tão importante, você deve considerar fazer o .ToList() no início do método.

    É uma pena que o .NET não tenha uma interface IEnumerable + Count + Indexer, sem os methods Add / Remove etc., que é o que eu suspeito que resolveria esse problema.

    Se os seus dados forem sempre repetíveis, talvez não se preocupem com isso. No entanto, você também pode desenrolá-lo – isso é especialmente útil se os dados recebidos puderem ser grandes (por exemplo, leitura de disco / rede):

     if(objects == null) throw new ArgumentException(); using(var iter = objects.GetEnumerator()) { if(!iter.MoveNext()) throw new ArgumentException(); var firstObject = iter.Current; var list = DoSomeThing(firstObject); while(iter.MoveNext()) { list.Add(DoSomeThingElse(iter.Current)); } return list; } 

    Note que eu mudei a semântica do DoSomethingElse um pouco, mas isso é principalmente para mostrar o uso desenrolado. Você poderia voltar a envolver o iterador, por exemplo. Você poderia torná-lo um bloco iterador também, o que poderia ser bom; então não há list – e você yield return dos itens à medida que você os obtivesse, em vez de adicioná-los a uma lista a ser retornada.

    Se o objective é realmente evitar enumerações múltiplas do que a resposta de Marc Gravell é o único a ler, mas mantendo a mesma semântica você poderia simplesmente remover as chamadas Any e First redundantes e ir com:

     public List Foo(IEnumerable objects) { if (objects == null) throw new ArgumentNullException("objects"); var first = objects.FirstOrDefault(); if (first == null) throw new ArgumentException( "Empty enumerable not supported.", "objects"); var list = DoSomeThing(first); var secondList = DoSomeThingElse(objects); list.AddRange(secondList); return list; } 

    Observe que isso pressupõe que você IEnumerable não é genérico ou, pelo menos, está restrito a ser um tipo de referência.

    Eu normalmente sobrecarrego meu método com IEnumerable e IList nessa situação.

     public static IEnumerable Method( this IList source ){... } public static IEnumerable Method( this IEnumerable source ) { /*input checks on source parameter here*/ return Method( source.ToList() ); } 

    Eu tomo cuidado para explicar nos comentários resumidos dos methods que chamando IEnumerable irá executar um .ToList ().

    O programador pode escolher .ToList () em um nível mais alto se várias operações estiverem sendo concatenadas e, em seguida, chamar a sobrecarga IList ou deixar minha sobrecarga IEnumerable cuidar disso.

    Usando IReadOnlyCollection ou IReadOnlyList na assinatura do método em vez de IEnumerable , tem a vantagem de tornar explícito que você pode precisar verificar a contagem antes de iterar ou iterar várias vezes por algum outro motivo.

    No entanto, eles têm uma enorme desvantagem que causará problemas se você tentar refatorar seu código para usar interfaces, por exemplo, para torná-lo mais testável e amigável ao proxy dynamic. O ponto-chave é que o IList não herda do IReadOnlyList e, da mesma forma, para outras collections e suas respectivas interfaces somente para leitura. (Em suma, isso ocorre porque o .NET 4.5 queria manter a compatibilidade ABI com versões anteriores. Mas eles nem mesmo aproveitaram a oportunidade para mudar isso no núcleo .NET. )

    Isto significa que se você obter um IList de alguma parte do programa e quiser passá-lo para outra parte que espera um IReadOnlyList , você não pode! Você pode no entanto passar um IList como um IEnumerable .

    No final, IEnumerable é a única interface somente leitura suportada por todas as collections .NET, incluindo todas as interfaces de coleção. Qualquer outra alternativa voltará a te morder quando perceber que você se livrou de algumas escolhas de arquitetura. Então eu acho que é o tipo apropriado para usar em assinaturas de function para expressar que você quer apenas uma coleção somente leitura.

    (Observe que você sempre pode escrever um método de extensão IReadOnlyList ToReadOnly(this IList list) que é simples se o tipo subjacente suportar ambas as interfaces, mas você precisa adicioná-lo manualmente em todos os lugares ao refatorar, IEnumerable é sempre compatível.

    Como sempre, isso não é absoluto, se você estiver escrevendo um código com base no database em que a enumeração múltipla acidental seria um desastre, talvez você prefira um compromisso diferente.

    Em primeiro lugar, esse aviso nem sempre significa muito. Eu normalmente desabilitei depois de ter certeza que não é um gargalo de desempenho. Significa apenas que o IEnumerable é avaliado duas vezes, o que geralmente não é um problema, a menos que a própria evaluation demore muito tempo. Mesmo que demore muito tempo, neste caso você usa apenas um elemento na primeira vez.

    Neste cenário você também pode explorar ainda mais os poderosos methods de extensão linq.

     var firstObject = objects.First(); return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList(); 

    É possível avaliar apenas o IEnumerable uma vez neste caso com alguma complicação, mas primeiro o perfil e ver se é realmente um problema.