Devuelva todos los enumerables con rendimiento devuelto de una vez; sin recorrer

164

Tengo la siguiente función para obtener errores de validación para una tarjeta. Mi pregunta se refiere a tratar con GetErrors. Ambos métodos tienen el mismo 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
}

¿Es posible devolver todos los errores GetMoreErrorssin tener que enumerarlos?

Pensar en esto probablemente sea una pregunta estúpida, pero quiero asegurarme de que no me estoy equivocando.

John Oxley
fuente
Estoy feliz (¡y curioso!) De ver que surgen más preguntas sobre el rendimiento de rendimiento, no lo entiendo por mí mismo. ¡No es una pregunta estúpida!
JoshJordan
¿Qué es GetCardProductionValidationErrorsFor?
Andrew Hare
44
¿ Qué hay de malo en devolver GetMoreErrors (tarjeta); ?
Sam Saffron
10
@Sam: "más rendimiento vuelve para más errores de validación"
Jon Skeet
1
Desde el punto de vista de un lenguaje no ambiguo, un problema es que el método no puede saber si hay algo que implemente T e IEnumerable <T>. Entonces necesitas una construcción diferente en el rendimiento. Dicho esto, seguro que sería bueno tener una manera de hacer esto. Rendimiento rendimiento rendimiento foo, tal vez, donde foo implementa IEnumerable <T>?
William Jockusch

Respuestas:

141

Definitivamente no es una pregunta estúpida, y es algo que F # admite con yield!una colección completa en comparación yieldcon un solo elemento. (Eso puede ser muy útil en términos de recursión de cola ...)

Lamentablemente no es compatible con C #.

Sin embargo, si tiene varios métodos, cada uno devuelve un IEnumerable<ErrorInfo>, puede usar Enumerable.Concatpara simplificar su código:

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

Sin embargo, hay una diferencia muy importante entre las dos implementaciones: esta llamará a todos los métodos de inmediato , a pesar de que solo usará los iteradores devueltos uno a la vez. Su código existente esperará hasta que se repita todo GetMoreErrors()antes de que incluso le pregunte sobre los próximos errores.

Por lo general, esto no es importante, pero vale la pena entender qué sucederá cuándo.

Jon Skeet
fuente
3
Wes Dyer tiene un artículo interesante que menciona este patrón. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Corrección menor para los transeúntes: es System.Linq.Enumeration.Concat <> (primero, segundo). No IEnumeration.Concat ().
redcalx
@ the-locster: no estoy seguro de lo que quieres decir. Definitivamente es Enumerable en lugar de Enumeration. ¿Podrías aclarar tu comentario?
Jon Skeet
@ Jon Skeet: ¿qué quiere decir exactamente que llamará a los métodos de inmediato? Ejecuté una prueba y parece que está aplazando completamente las llamadas al método hasta que algo se repite. Código aquí: pastebin.com/0kj5QtfD
Steven Oxley
55
@ Steven: No. Está llamando a los métodos, pero en su caso GetOtherErrors()(etc.) están aplazando sus resultados (ya que se implementan utilizando bloques iteradores). Intente cambiarlos para devolver una nueva matriz o algo así, y verá lo que quiero decir.
Jon Skeet
26

Puede configurar todas las fuentes de error como esta (nombres de métodos tomados de la respuesta 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();
}

Luego puede iterar sobre ellos al mismo tiempo.

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

Alternativamente, podría aplanar las fuentes de error con SelectMany.

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

La ejecución de los métodos en GetErrorSourcestambién se retrasará.

Adam Boddington
fuente
17

Se me ocurrió un yield_fragmento rápido :

animación de uso recortado

Aquí está el fragmento XML:

<?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
fuente
2
¿Cómo es esto una respuesta a la pregunta?
Ian Kemp
1
@ Ian, así es como debe hacer el retorno de rendimiento anidado en C #. No hay yield!, como en F #.
John Gietzen
esta no es una respuesta a la pregunta
divyang4481
8

No veo nada malo en tu función, diría que está haciendo lo que quieres.

Piense en el Rendimiento como que devuelve un elemento en la Enumeración final cada vez que se invoca, de modo que cuando lo tiene en el bucle foreach así, cada vez que se invoca devuelve 1 elemento. Tiene la capacidad de poner declaraciones condicionales en su foreach para filtrar el conjunto de resultados. (simplemente por no ceder en sus criterios de exclusión)

Si agrega rendimientos posteriores más adelante en el método, continuará agregando 1 elemento a la enumeración, haciendo posible hacer cosas 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
fuente
4

Me sorprende que nadie haya pensado en recomendar un método de Extensión simple IEnumerable<IEnumerable<T>>para que este código mantenga su ejecución diferida. Soy fanático de la ejecución diferida por muchas razones, una de ellas es que la huella de memoria es pequeña incluso para enumerables enormes y monótonos.

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;
            }
        }
    }
}

Y podrías usarlo en tu caso así

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
}

Del mismo modo, puede eliminar la función de envoltura DoGetErrorsy simplemente pasar UnWrapal sitio de llamadas.

Frank Bryce
fuente
2
Probablemente nadie pensó en un método de Extensión porque DoGetErrors(card).SelectMany(x => x)hace lo mismo y conserva el comportamiento diferido. Que es exactamente lo que Adam sugiere en su respuesta .
huysentruitw
3

Sí, es posible devolver todos los errores a la vez. Solo devuelve un List<T>o ReadOnlyCollection<T>.

Al devolver un IEnumerable<T>, estás devolviendo una secuencia de algo. En la superficie que puede parecer idéntica a la devolución de la colección, pero hay una serie de diferencias, debe tener en cuenta.

Colecciones

  • La persona que llama puede estar segura de que tanto la colección como todos los elementos existirán cuando se devuelva la colección. Si la colección debe crearse por llamada, devolver una colección es una muy mala idea.
  • La mayoría de las colecciones se pueden modificar cuando se devuelven.
  • La colección es de tamaño finito.

Secuencias

  • Se puede enumerar, y eso es casi todo lo que podemos decir con certeza.
  • Una secuencia devuelta en sí no se puede modificar.
  • Cada elemento puede crearse como parte de la ejecución de la secuencia (es decir, el retorno IEnumerable<T>permite una evaluación diferida, el retorno List<T>no).
  • Una secuencia puede ser infinita y, por lo tanto, dejar que la persona que llama decida cuántos elementos deben devolverse.
Brian Rasmussen
fuente
Devolver una colección puede resultar en una sobrecarga irrazonable si todo lo que el cliente realmente necesita es enumerar a través de ella, ya que asigna las estructuras de datos para todos los elementos por adelantado. Además, si delega a otro método que devuelve una secuencia, capturarlo como una colección implica una copia adicional, y no sabe cuántos elementos (y, por lo tanto, cuántos gastos generales) puede implicar esto. Por lo tanto, es una buena idea devolver la colección cuando ya está allí y puede devolverse directamente sin copiar (o empaquetar como solo lectura). En todos los demás casos, la secuencia es una mejor opción
Pavel Minaev
Estoy de acuerdo, y si tienes la impresión de que dije que devolver una colección siempre es una buena idea, te equivocaste. Intenté resaltar el hecho de que existen diferencias entre devolver una colección y devolver una secuencia. Trataré de hacerlo más claro.
Brian Rasmussen