Restituisce tutti gli enumerabili con rendimento in una volta; senza loop


164

Ho la seguente funzione per ottenere errori di convalida per una carta. La mia domanda riguarda la gestione di GetErrors. Entrambi i metodi hanno lo stesso tipo restituito 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
}

È possibile restituire tutti gli errori GetMoreErrorssenza doverli enumerare?

Pensandoci, questa è probabilmente una domanda stupida, ma voglio assicurarmi di non sbagliare.


Sono felice (e curioso!) Di vedere più domande sul rendimento - non capisco bene da solo. Non è una domanda stupida!
JoshJordan,

Che cosa è GetCardProductionValidationErrorsFor?
Andrew Hare,

4
Cosa c'è che non va nel ritorno GetMoreErrors (carta); ?
Sam Saffron,

10
@Sam: "ulteriori rendimenti per ulteriori errori di convalida"
Jon Skeet,

1
Dal punto di vista di un linguaggio non ambiguo, un problema è che il metodo non può sapere se esiste qualcosa che implementa sia T che IEnumerable <T>. Quindi hai bisogno di un costrutto diverso nella resa. Detto questo, sarebbe bello avere un modo per farlo. Resa rendimento resa foo, forse, dove foo implementa IEnumerable <T>?
William Jockusch,

Risposte:


140

Non è assolutamente una domanda stupida, ed è qualcosa con cui F # supporta yield!un'intera collezione contro yieldun singolo articolo. (Questo può essere molto utile in termini di ricorsione della coda ...)

Sfortunatamente non è supportato in C #.

Tuttavia, se hai diversi metodi ciascuno che restituisce un IEnumerable<ErrorInfo>, puoi usare Enumerable.Concatper rendere il tuo 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: questa chiamerà immediatamente tutti i metodi , anche se utilizzerà solo gli iteratori restituiti uno alla volta. Il codice esistente attenderà fino a quando non viene eseguito il ciclo completo di tutto, GetMoreErrors()prima ancora di chiedere i prossimi errori.

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


3
Wes Dyer ha un articolo interessante che menziona questo schema. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH

1
Correzione minore per i passanti: è System.Linq.Enumeration.Concat <> (primo, secondo). Non IEnumeration.Concat ().
redcalx,

@ the-locster: non sono sicuro di cosa intendi. È sicuramente enumerabile piuttosto che enumerazione. Potresti chiarire il tuo commento?
Jon Skeet,

@Jon Skeet - Cosa vuoi dire esattamente che chiamerà immediatamente i metodi? Ho eseguito un test e sembra che stia rinviando completamente le chiamate del metodo fino a quando qualcosa non viene effettivamente ripetuto. Codice qui: pastebin.com/0kj5QtfD
Steven Oxley

5
@Steven: No. Sta chiamando i metodi, ma nel tuo caso GetOtherErrors()(ecc.) Stanno rinviando i loro risultati (poiché sono implementati usando blocchi iteratori). Prova a cambiarli per restituire un nuovo array o qualcosa del genere e vedrai cosa intendo.
Jon Skeet,

26

È possibile impostare tutte le fonti di errore in questo modo (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();
}

È quindi possibile iterare 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, è possibile appiattire le fonti di errore con SelectMany.

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

Anche l'esecuzione dei metodi GetErrorSourcesverrà ritardata.


16

Mi è venuto in mente un breve yield_frammento:

resa_ animazione di utilizzo frammentata

Ecco lo snippet 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>

2
In che modo questa è una risposta alla domanda?
Ian Kemp,

@Ian, è così che devi fare un rendimento annidato in C #. Non c'è yield!, come in F #.
John Gietzen,

questa non è una risposta alla domanda
divyang4481

8

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

Pensa alla resa come a restituire un elemento nell'enumerazione finale ogni volta che viene invocato, quindi quando lo hai nel ciclo foreach in questo modo, ogni volta che viene invocato restituisce 1 elemento. Hai la possibilità di inserire istruzioni condizionali nel tuo foreach per filtrare il gruppo di risultati. (semplicemente non cedendo ai criteri di esclusione)

Se aggiungi ulteriori raccolti in seguito nel metodo, continuerà ad aggiungere 1 elemento all'enumerazione, rendendo possibile fare cose come ...

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

4

Sono sorpreso che nessuno abbia pensato di raccomandare un semplice metodo di estensione IEnumerable<IEnumerable<T>>per fare in modo che questo codice mantenga la sua esecuzione differita. Sono un fan dell'esecuzione differita per molte ragioni, uno di questi è che l'impronta di memoria è piccola anche per enormi enumerabili.

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 potresti usarlo nel tuo caso in questo modo

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, puoi eliminare la funzione wrapper DoGetErrorse passare UnWrapal sito di chiamata.


2
Probabilmente nessuno ha pensato a un metodo di estensione perché DoGetErrors(card).SelectMany(x => x)fa lo stesso e preserva il comportamento differito. Questo è esattamente ciò che Adam suggerisce nella sua risposta .
huysentruitw,

3

Sì, è possibile restituire tutti gli errori contemporaneamente. Restituisci solo un List<T>o ReadOnlyCollection<T>.

Restituendo un IEnumerable<T>stai restituendo una sequenza di qualcosa. In apparenza che potrebbe sembrare identico a restituire la collezione, ma ci sono alcune differenze, dovresti tenere a mente.

collezioni

  • Il chiamante può essere sicuro che al momento della restituzione della raccolta saranno presenti sia la raccolta che tutti gli articoli. 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

  • Può essere elencato - e questo è praticamente tutto ciò che possiamo dire con certezza.
  • Una sequenza restituita non può essere modificata.
  • Ogni elemento può essere creato durante l'esecuzione della sequenza (ovvero il ritorno IEnumerable<T>consente una valutazione lenta, il ritorno List<T>no).
  • Una sequenza può essere infinita e quindi lasciare al chiamante la decisione di quanti elementi devono essere restituiti.

La restituzione di una raccolta può comportare un sovraccarico irragionevole se tutto ciò di cui il cliente ha davvero bisogno è di enumerarlo, poiché si allocano in anticipo le strutture di dati per tutti gli elementi. Inoltre, se si esegue la delega a un altro metodo che restituisce una sequenza, la sua acquisizione come raccolta comporta una copia aggiuntiva e non si conosce il numero di elementi (e quindi l'overhead) che ciò potrebbe potenzialmente comportare. Pertanto, è una buona idea restituire la raccolta quando è già presente e può essere restituita direttamente senza copiarla (o spostarla come sola lettura). In tutti gli altri casi, la sequenza è una scelta migliore
Pavel Minaev il

Sono d'accordo, e se hai l'impressione che ho detto che restituire una collezione è sempre una buona idea, ti sei perso il punto. Stavo cercando di evidenziare il fatto che ci sono differenze tra la restituzione di una raccolta e la restituzione di una sequenza. Proverò a renderlo più chiaro.
Brian Rasmussen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.