Retorne todos os enumeráveis ​​com retorno de rendimento de uma só vez; sem percorrer

164

Eu tenho a seguinte função para obter erros de validação para um cartão. Minha pergunta diz respeito a lidar com GetErrors. Ambos os métodos têm o mesmo tipo de retorno IEnumerable<ErrorInfo>.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    var errors = GetMoreErrors(card);
    foreach (var e in errors)
        yield return e;

    // further yield returns for more validation errors
}

É possível retornar todos os erros GetMoreErrorssem precisar enumerá-los?

Pensar nisso provavelmente é uma pergunta estúpida, mas quero ter certeza de que não estou errado.

John Oxley
fonte
Estou feliz (e curioso!) Ao ver mais perguntas sobre retorno de rendimento surgirem - eu mesmo não entendo direito. Não é uma pergunta estúpida!
JoshJordan
O que é GetCardProductionValidationErrorsFor?
Andrew Hare
4
o que há de errado com o retorno GetMoreErrors (card); ?
Sam Saffron
10
@Sam: "ainda mais o rendimento retornos para mais erros de validação"
Jon Skeet
1
Do ponto de vista de uma linguagem não ambígua, um problema é que o método não pode saber se existe algo que implemente T e IEnumerable <T>. Então você precisa de uma construção diferente no rendimento. Dito isto, com certeza seria bom ter uma maneira de fazer isso. Rendimento retorno rendimento foo, talvez, onde foo implementa IEnumerable <T>?
William Jockusch 01/01

Respostas:

140

Definitivamente, não é uma pergunta estúpida, e é algo que o F # suporta yield!para uma coleção inteira versus yieldum único item. (Isso pode ser muito útil em termos de recursão da cauda ...)

Infelizmente, não há suporte para C #.

No entanto, se você tiver vários métodos, cada um retornando um IEnumerable<ErrorInfo>, poderá usar Enumerable.Concatpara tornar seu código mais simples:

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return GetMoreErrors(card).Concat(GetOtherErrors())
                              .Concat(GetValidationErrors())
                              .Concat(AnyMoreErrors())
                              .Concat(ICantBelieveHowManyErrorsYouHave());
}

Porém, existe uma diferença muito importante entre as duas implementações: esta chamará todos os métodos imediatamente , mesmo que use apenas os iteradores retornados um de cada vez. Seu código existente aguardará até que ele repita tudo GetMoreErrors()antes de perguntar sobre os próximos erros.

Geralmente isso não é importante, mas vale a pena entender o que acontecerá quando.

Jon Skeet
fonte
3
Wes Dyer tem um artigo interessante mencionando esse padrão. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Correção menor para os passantes - é System.Linq.Enumeration.Concat <> (primeiro, segundo). Não IEnumeration.Concat ().
Redcalx #
@ the-locster: Não sei o que você quer dizer. É definitivamente Enumerável, em vez de Enumeração. Você poderia esclarecer seu comentário?
perfil completo de Jon Skeet
@ Jon Skeet - O que exatamente você quer dizer com isso chamará os métodos imediatamente? Fiz um teste e parece que está adiando completamente as chamadas de método até que algo seja realmente iterado. O código abaixo
Steven Oxley
5
@ Steven: Não. Ele está chamando os métodos - mas no seu caso GetOtherErrors()(etc) estão adiando seus resultados (como eles são implementados usando blocos iteradores). Tente alterá-los para retornar uma nova matriz ou algo assim, e você verá o que quero dizer.
quer
26

Você pode configurar todas as fontes de erro como esta (nomes de métodos emprestados da resposta de Jon Skeet).

private static IEnumerable<IEnumerable<ErrorInfo>> GetErrorSources(Card card)
{
    yield return GetMoreErrors(card);
    yield return GetOtherErrors();
    yield return GetValidationErrors();
    yield return AnyMoreErrors();
    yield return ICantBelieveHowManyErrorsYouHave();
}

Você pode iterar sobre eles ao mesmo tempo.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    foreach (var errorSource in GetErrorSources(card))
        foreach (var error in errorSource)
            yield return error;
}

Como alternativa, você pode achatar as fontes de erro com SelectMany.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return GetErrorSources(card).SelectMany(e => e);
}

A execução dos métodos GetErrorSourcestambém será atrasada.

Adam Boddington
fonte
16

Eu vim com um yield_trecho rápido :

animação de uso cortado yield_

Aqui está o XML do trecho:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Author>John Gietzen</Author>
      <Description>yield! expansion for C#</Description>
      <Shortcut>yield_</Shortcut>
      <Title>Yield All</Title>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="true">
          <Default>items</Default>
          <ID>items</ID>
        </Literal>
        <Literal Editable="true">
          <Default>i</Default>
          <ID>i</ID>
        </Literal>
      </Declarations>
      <Code Language="CSharp"><![CDATA[foreach (var $i$ in $items$) yield return $i$$end$;]]></Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>
John Gietzen
fonte
2
Como isso é uma resposta para a pergunta?
31419 Ian Kemp
@ Ian, é assim que você deve fazer um retorno de rendimento aninhado em c #. Não existe yield!, como em F #.
John Gietzen
esta não é uma resposta para a pergunta
divyang4481 10/01
8

Não vejo nada de errado com sua função, eu diria que está fazendo o que você deseja.

Pense no Yield como retornando um elemento na Enumeração final toda vez que for chamado, portanto, quando você o tiver no loop foreach assim, toda vez que for chamado, ele retornará 1 elemento. Você tem a capacidade de colocar instruções condicionais em seu foreach para filtrar o conjunto de resultados. (simplesmente não cumprindo seus critérios de exclusão)

Se você adicionar rendimentos subsequentes posteriormente no método, ele continuará adicionando 1 elemento à enumeração, possibilitando fazer coisas como ...

public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists)
{
  foreach (IEnumerable<string> list in lists)
  {
    foreach (string s in list)
    {
      yield return s;
    }
  }
}
Tim Jarvis
fonte
4

Estou surpreso que ninguém tenha pensado em recomendar um método simples de extensão IEnumerable<IEnumerable<T>>para fazer esse código manter sua execução adiada. Sou fã de execução adiada por muitas razões, uma delas é que a pegada de memória é pequena, mesmo para enumeráveis ​​enormes.

public static class EnumearbleExtensions
{
    public static IEnumerable<T> UnWrap<T>(this IEnumerable<IEnumerable<T>> list)
    {
        foreach(var innerList in list)
        {
            foreach(T item in innerList)
            {
                yield return item;
            }
        }
    }
}

E você pode usá-lo no seu caso como este

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return DoGetErrors(card).UnWrap();
}

private static IEnumerable<IEnumerable<ErrorInfo>> DoGetErrors(Card card)
{
    yield return GetMoreErrors(card);

    // further yield returns for more validation errors
}

Da mesma forma, você pode acabar com a função de invólucro DoGetErrorse simplesmente mudar UnWrappara o local da chamada.

Frank Bryce
fonte
2
Provavelmente ninguém pensou em um método de extensão porque DoGetErrors(card).SelectMany(x => x)faz o mesmo e preserva o comportamento adiado. O que é exatamente o que Adam sugere em sua resposta .
huysentruitw
3

Sim, é possível retornar todos os erros de uma só vez. Basta retornar um List<T>ou ReadOnlyCollection<T>.

Ao retornar um, IEnumerable<T>você está retornando uma sequência de algo. Na superfície, isso pode parecer idêntico ao retorno da coleção, mas há várias diferenças, você deve ter em mente.

Colecções

  • O chamador pode ter certeza de que a coleção e todos os itens existirão quando a coleção for retornada. Se a coleção precisar ser criada por chamada, retornar uma coleção é uma péssima idéia.
  • A maioria das coleções pode ser modificada quando retornada.
  • A coleção é de tamanho finito.

Sequências

  • Pode ser enumerado - e isso é tudo o que podemos dizer com certeza.
  • Uma sequência retornada em si não pode ser modificada.
  • Cada elemento pode ser criado como parte da execução da sequência (ou seja, o retorno IEnumerable<T>permite uma avaliação lenta, o retorno List<T>não).
  • Uma sequência pode ser infinita e, assim, deixar para o chamador decidir quantos elementos devem ser retornados.
Brian Rasmussen
fonte
O retorno de uma coleção pode resultar em uma sobrecarga irracional se tudo o que o cliente realmente precisa é enumerá-lo, já que você aloca previamente as estruturas de dados para todos os elementos. Além disso, se você delegar para outro método que está retornando uma sequência, capturá-la como uma coleção envolve cópia extra e você não sabe quantos itens (e, portanto, quanto sobrecarga) isso pode envolver. Portanto, é apenas uma boa idéia retornar a coleção quando ela já estiver lá e puder ser retornada diretamente sem copiar (ou agrupar como somente leitura). Em todos os outros casos, a sequência é uma escolha melhor
Pavel Minaev
Eu concordo, e se você teve a impressão de que eu disse que devolver uma coleção é sempre uma boa ideia, você não entendeu. Eu estava tentando destacar o fato de que existem diferenças entre retornar uma coleção e retornar uma sequência. Vou tentar deixar mais claro.
22711 Brian Rasmussen