2009-08-13 12 views
119

Ho la seguente funzione per ottenere errori di convalida per una scheda. La mia domanda riguarda la gestione di GetErrors. Entrambi i metodi hanno lo stesso tipo di ritorno IEnumerable<ErrorInfo>.Rendimento rendimento annidato 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 
} 

E 'possibile restituire tutti gli errori in GetMoreErrors senza dover enumerare attraverso di loro?

Pensandoci questa è probabilmente una domanda stupida, ma voglio essere sicuro che non sbaglierò.

+0

Sono felice (e curioso!) Di vedere che arrivano altre domande sul rendimento, non lo capisco nemmeno io. Non una domanda stupida! – JoshJordan

+0

Che cos'è 'GetCardProductionValidationErrorsFor'? –

+4

cosa non va con * return GetMoreErrors (card); *? –

risposta

107

Non è assolutamente una domanda stupida, ed è qualcosa che F # supporta con yield! per un'intera collezione vs yield per un singolo oggetto. (Che può essere molto utile in termini di ricorsione della coda ...)

Sfortunatamente non è supportato in C#.

Tuttavia, se si dispone di diversi metodi ogni ritorno un IEnumerable<ErrorInfo>, è possibile utilizzare Enumerable.Concat per rendere il codice più semplice:

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

C'è una differenza molto importante tra le due implementazioni però: questo chiamerà tutti i metodi immediatamente, anche se utilizzerà solo gli iteratori restituiti uno alla volta. Il codice esistente attenderà fino a quando non verrà eseguito il loop su tutto in GetMoreErrors() prima che anche chieda sugli errori successivi.

Di solito questo non è importante, ma vale la pena capire cosa succederà quando.

+2

Wes Dyer ha un interessante articolo che menziona questo modello. http://blogs.msdn.com/wesdyer/archive/2007/03/23/all-about-iterators.aspx – JohannesH

+1

Correzione minima per i passanti - è System.Linq.Enumeration.Concat <> (primo, secondo) . Non IEnumeration.Concat(). – redcalx

+0

@ the-locster: non sono sicuro di cosa intendi. È sicuramente Enumerable piuttosto che Enumeration. Potresti chiarire il tuo commento? –

6

Non vedo niente di sbagliato nella tua funzione, direi che sta facendo quello che vuoi.

Pensa che il rendimento restituisce un elemento nell'enumerazione finale ogni volta che viene richiamato, quindi quando lo hai nel ciclo foreach in questo modo, ogni volta che viene richiamato restituisce 1 elemento. Hai la possibilità di inserire istruzioni condizionali nel foreach per filtrare il set di risultati. (Semplicemente non cedere i suoi criteri di esclusione)

Se si aggiungono i rendimenti successivi in ​​seguito nel metodo, si continuerà ad aggiungere 1 elemento alla enumerazione, rendendo possibile fare le cose come ...

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

Sì, è possibile restituire tutti gli errori in una volta. Basta restituire un List<T> o ReadOnlyCollection<T>.

Restituendo un IEnumerable<T> si restituisce una sequenza di qualcosa. Sulla superficie che può sembrare identica a restituire la collezione, ma ci sono una serie di differenze, dovresti tenere a mente.

Collezioni

  • Il chiamante può essere sicuri che sia la raccolta e tutti gli articoli saranno esistere quando viene restituita la raccolta. Se la raccolta deve essere creata per chiamata, restituire una raccolta è una pessima idea.
  • La maggior parte delle raccolte può essere modificata al momento della restituzione.
  • La collezione è di dimensioni finite.

Sequenze

  • possono essere enumerati - e questo è più o meno tutto quello che possiamo dire con certezza.
  • Una sequenza restituita non può essere modificata.
  • Ogni elemento può essere creato come parte dell'esecuzione della sequenza (ad esempio restituendo IEnumerable<T> consente la valutazione lazy, restituendo List<T> non).
  • Una sequenza può essere infinita e quindi lasciarlo al chiamante per decidere quanti elementi devono essere restituiti.
+0

Il ripristino di una raccolta può comportare un sovraccarico irragionevole se tutto il cliente ha davvero bisogno di enumerare attraverso di essa, poiché si allocano le strutture di dati per tutti gli elementi in anticipo. Inoltre, se si delega a un altro metodo che restituisce una sequenza, acquisirla come raccolta comporta una copia aggiuntiva e non si sa quanti oggetti (e quindi quanto sovraccarico) ciò potrebbe implicare. Pertanto, è solo una buona idea restituire la raccolta quando è già presente e può essere restituita direttamente senza copiare (o incapsulata come readonly). In tutti gli altri casi, la sequenza è una scelta migliore –

+0

Sono d'accordo, e se hai l'impressione che ho detto che restituire una collezione è sempre una buona idea hai perso il mio punto. Stavo cercando di evidenziare il fatto che ci sono delle differenze tra restituire una collezione e restituire una sequenza. Proverò a renderlo più chiaro. –

14

È possibile impostare tutte le origini di errore come questa (i nomi dei metodi presi in prestito dalla risposta di 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(); 
} 

È possibile quindi eseguire l'iterazione su di essi contemporaneamente.

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

In alternativa si potrebbe appiattire le fonti di errore con SelectMany.

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

L'esecuzione dei metodi in GetErrorSources sarà ritardata troppo.

9

mi si avvicinò con un rapido yield_ frammento:

yield_ snipped usage animation

Ecco il XML frammento:

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

Mi sorprende che nessuno ha pensato di consigliare un metodo semplice estensione su IEnumerable<IEnumerable<T>> per fare in modo che questo codice mantenga la sua esecuzione differita. Sono un fan dell'esecuzione differita per molte ragioni, una delle quali è che l'impronta della memoria è piccola anche per le enumerables enorme-mongose.

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 si potrebbe usare nel vostro caso come questo

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 
} 

Allo stesso modo, è possibile farla finita con la funzione wrapper DoGetErrors e basta spostare UnWrap al callsite.

+0

Probabilmente nessuno ha pensato a un metodo di estensione perché "DoGetErrors (card) .SelectMany (x => x)" fa lo stesso e conserva il comportamento posticipato. Che è esattamente ciò che Adam suggerisce in [la sua risposta] (http://stackoverflow.com/a/22912410/1300910). –

Problemi correlati