2009-08-13 14 views
119

Tengo la siguiente función para obtener errores de validación para una tarjeta. Mi pregunta se relaciona con tratar con GetErrors. Ambos métodos tienen el mismo tipo de devolución IEnumerable<ErrorInfo>.rendimiento de rendimiento anidado con IEnumerable

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 en GetMoreErrors sin tener que enumerar a través de ellos?

Pensando en esto, esta es probablemente una pregunta estúpida, pero quiero asegurarme de que no me estoy equivocando.

+0

Estoy contento (¡y me siento curioso!) De ver aparecer más preguntas de rendimiento. No lo entiendo del todo. ¡No es una pregunta estúpida! – JoshJordan

+0

¿Qué es 'GetCardProductionValidationErrorsFor'? –

+4

cuál es incorrecto con * devuelve GetMoreErrors (tarjeta); *? –

Respuesta

107

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

Desafortunadamente no es compatible con C#.

Sin embargo, si dispone de varios métodos A cada una IEnumerable<ErrorInfo>, puede utilizar Enumerable.Concat para hacer su código más simple:

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

Hay una diferencia muy importante entre las dos implementaciones sin embargo: éste va a llamar a todos los métodos inmediatamente, aunque solo utilizará los iteradores devueltos de a uno por vez. Su código existente esperará hasta que se bucle a través de todo en GetMoreErrors() antes incluso pregunta sobre los próximos errores.

Por lo general, esto no es importante, pero vale la pena entender qué va a pasar cuando.

+2

Wes Dyer tiene un artículo interesante que menciona este patrón. http://blogs.msdn.com/wesdyer/archive/2007/03/23/all-about-iterators.aspx – JohannesH

+1

Corrección leve para los transeúntes: es System.Linq.Enumeration.Concat <> (first, second) . No enEnumeración.Concat() – redcalx

+0

@ the-locster: No estoy seguro de lo que quieres decir. Definitivamente es Enumerable en lugar de Enumeration. ¿Podría aclarar su comentario? –

6

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

Considere que el rendimiento 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. Usted 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, que seguirá añadiendo 1 elemento a la enumeración, lo que es 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; 
    } 
    } 
} 
5

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

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

Colecciones

  • La persona que llama puede estar seguro de que existan tanto en la recogida y todos los elementos cuando se devuelve 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 pueden enumerar - y que es prácticamente todo lo que podemos decir con seguridad.
  • Una secuencia devuelta en sí misma no se puede modificar.
  • Cada elemento se puede crear como parte de la ejecución de la secuencia (es decir, devolver IEnumerable<T> permite la evaluación diferida, pero List<T> no lo hace).
  • Una secuencia puede ser infinita y dejar que la persona que llama decida cuántos elementos se deben devolver.
+0

Devolver una colección puede ocasionar 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 de antemano. Además, si delega en otro método que devuelve una secuencia, entonces capturarlo como una colección implica una copia adicional, y no sabe cuántos elementos (y, por lo tanto, la cantidad de gastos generales) esto podría implicar. Por lo tanto, solo es una buena idea devolver la recopilación cuando ya está allí y puede devolverse directamente sin copiar (o envolverla como de solo lectura). En todos los demás casos, la secuencia es una mejor opción –

+0

Estoy de acuerdo, y si tiene la impresión de que dije que devolver una colección siempre es una buena idea, no entendió mi punto. Intentaba resaltar el hecho de que existen diferencias entre devolver una colección y devolver una secuencia. Trataré de hacerlo más claro. –

14

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

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

Como alternativa puede 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 GetErrorSources se retrasará también.

9

me ocurrió una rápida yield_ fragmento:

yield_ snipped usage animation

Aquí está el XML fragmento:

<?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> 
1

Me sorprende que nadie ha pensado para recomendar un método de extensión simple en IEnumerable<IEnumerable<T>> para hacer que este código mantenga su ejecución diferida. Soy fan 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ógamos.

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 se podría utilizar en su 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 
} 

Del mismo modo, se puede suprimir la función de envoltura alrededor DoGetErrors y sólo se mueven a la UnWrap callsite.

+0

Probablemente nadie pensó en un método de extensión porque 'DoGetErrors (card) .SelectMany (x => x)' hace lo mismo y preserva el comportamiento diferido. Que es exactamente lo que Adam sugiere en [su respuesta] (http://stackoverflow.com/a/22912410/1300910). –