Geben Sie alle Enumerables mit Yield Return auf einmal zurück. ohne durchzuschleifen

164

Ich habe die folgende Funktion, um Validierungsfehler für eine Karte zu erhalten. Meine Frage bezieht sich auf den Umgang mit GetErrors. Beide Methoden haben den gleichen Rückgabetyp 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
}

Ist es möglich, alle Fehler zurückzugeben, GetMoreErrorsohne sie aufzählen zu müssen?

Darüber nachzudenken ist wahrscheinlich eine dumme Frage, aber ich möchte sicherstellen, dass ich nichts falsch mache.

John Oxley
quelle
Ich bin froh (und neugierig!), Weitere Fragen zur Rendite zu sehen - ich verstehe es selbst nicht ganz. Keine blöde Frage!
JoshJordan
Was ist GetCardProductionValidationErrorsFor?
Andrew Hare
4
Was ist falsch an der Rückgabe? GetMoreErrors (Karte); ?
Sam Saffron
10
@ Sam: "Weitere Ertragsrenditen für weitere Validierungsfehler"
Jon Skeet
1
Unter dem Gesichtspunkt einer nicht mehrdeutigen Sprache besteht ein Problem darin, dass die Methode nicht wissen kann, ob es etwas gibt, das sowohl T als auch IEnumerable <T> implementiert. Sie benötigen also ein anderes Konstrukt in der Ausbeute. Das heißt, es wäre sicher schön, einen Weg zu haben, dies zu tun. Rendite Rendite Rendite foo vielleicht, wo foo IEnumerable <T> implementiert?
William Jockusch

Antworten:

140

Es ist definitiv keine dumme Frage, und es ist etwas, das F # yield!für eine ganze Sammlung im Vergleich yieldzu einem einzelnen Artikel unterstützt. (Das kann sehr nützlich sein in Bezug auf die Schwanzrekursion ...)

Leider wird es in C # nicht unterstützt.

Wenn Sie jedoch mehrere Methoden haben, die jeweils eine zurückgeben IEnumerable<ErrorInfo>, können Sie Enumerable.ConcatIhren Code vereinfachen:

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

Es gibt jedoch einen sehr wichtigen Unterschied zwischen den beiden Implementierungen: Diese ruft alle Methoden sofort auf , obwohl nur die zurückgegebenen Iteratoren einzeln verwendet werden. Ihr vorhandener Code wartet, bis er alles durchlaufen hat, GetMoreErrors()bevor er überhaupt nach den nächsten Fehlern fragt .

Normalerweise ist dies nicht wichtig, aber es lohnt sich zu verstehen, was wann passieren wird.

Jon Skeet
quelle
3
Wes Dyer hat einen interessanten Artikel, der dieses Muster erwähnt. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Kleinere Korrektur für Passanten - es ist System.Linq.Enumeration.Concat <> (erste, zweite). Nicht IEnumeration.Concat ().
Redcalx
@ the-locster: Ich bin mir nicht sicher, was du meinst. Es ist definitiv eher Enumerable als Enumeration. Könnten Sie Ihren Kommentar klarstellen?
Jon Skeet
@ Jon Skeet - Was genau meinst du damit, dass die Methoden sofort aufgerufen werden? Ich habe einen Test durchgeführt und es sieht so aus, als würde er die Methodenaufrufe vollständig verschieben, bis tatsächlich etwas iteriert wird. Code hier: pastebin.com/0kj5QtfD
Steven Oxley
5
@Steven: Nein. Es ruft die Methoden auf - aber in Ihrem Fall GetOtherErrors()(usw.) verschieben sie ihre Ergebnisse (da sie mithilfe von Iteratorblöcken implementiert werden). Versuchen Sie, sie zu ändern, um ein neues Array oder ähnliches zurückzugeben, und Sie werden sehen, was ich meine.
Jon Skeet
26

Sie können alle Fehlerquellen wie folgt einrichten (Methodennamen aus Jon Skeets Antwort).

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

Sie können sie dann gleichzeitig durchlaufen.

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

Alternativ können Sie die Fehlerquellen mit reduzieren SelectMany.

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

Die Ausführung der Methoden in GetErrorSourceswird ebenfalls verzögert.

Adam Boddington
quelle
16

Ich habe mir einen kurzen yield_Ausschnitt ausgedacht:

Yield_ Snipped Use Animation

Hier ist das XML-Snippet:

<?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
quelle
2
Wie ist das eine Antwort auf die Frage?
Ian Kemp
@ Ian, so müssen Sie verschachtelte Renditen in C # zurückgeben. Es gibt keine yield!, wie in F #.
John Gietzen
Dies ist keine Antwort auf die Frage
divyang4481
8

Ich sehe nichts falsches an Ihrer Funktion, ich würde sagen, dass sie tut, was Sie wollen.

Stellen Sie sich die Ausbeute so vor, dass sie bei jedem Aufruf ein Element in der endgültigen Aufzählung zurückgibt. Wenn Sie es also so in der foreach-Schleife haben, gibt es bei jedem Aufruf 1 Element zurück. Sie haben die Möglichkeit, bedingte Anweisungen in Ihr foreach einzufügen, um die Ergebnismenge zu filtern. (einfach indem Sie Ihre Ausschlusskriterien nicht erfüllen)

Wenn Sie später in der Methode nachfolgende Ausbeuten hinzufügen, wird der Aufzählung weiterhin 1 Element hinzugefügt, sodass Sie beispielsweise ...

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

Ich bin überrascht, dass niemand daran gedacht hat, eine einfache Erweiterungsmethode zu empfehlen IEnumerable<IEnumerable<T>>, damit dieser Code seine verzögerte Ausführung beibehält. Ich bin aus vielen Gründen ein Fan von verzögerter Ausführung. Einer davon ist, dass der Speicherbedarf selbst für sehr viele Aufzählungen gering ist.

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

Und Sie könnten es in Ihrem Fall so verwenden

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
}

Ebenso können Sie die Wrapper-Funktion aufheben DoGetErrorsund einfach UnWrapzur Call-Site wechseln.

Frank Bryce
quelle
2
Wahrscheinlich hat niemand über eine Erweiterungsmethode nachgedacht, weil DoGetErrors(card).SelectMany(x => x)sie dasselbe tut und das verzögerte Verhalten beibehält. Welches ist genau das, was Adam in seiner Antwort vorschlägt .
Huysentruitw
3

Ja, es ist möglich, alle Fehler auf einmal zurückzugeben. Geben Sie einfach ein List<T>oder zurück ReadOnlyCollection<T>.

Wenn IEnumerable<T>Sie ein zurückgeben, geben Sie eine Folge von etwas zurück. An der Oberfläche scheint dies identisch mit der Rücksendung der Sammlung zu sein, aber es gibt eine Reihe von Unterschieden, die Sie berücksichtigen sollten.

Sammlungen

  • Der Anrufer kann sicher sein, dass sowohl die Sammlung als auch alle Elemente vorhanden sind, wenn die Sammlung zurückgegeben wird. Wenn die Sammlung pro Aufruf erstellt werden muss, ist die Rückgabe einer Sammlung eine wirklich schlechte Idee.
  • Die meisten Sammlungen können bei Rückgabe geändert werden.
  • Die Sammlung ist von endlicher Größe.

Sequenzen

  • Kann aufgezählt werden - und das ist so ziemlich alles, was wir mit Sicherheit sagen können.
  • Eine zurückgegebene Sequenz selbst kann nicht geändert werden.
  • Jedes Element kann als Teil des Durchlaufens der Sequenz erstellt werden (dh das Zurückgeben IEnumerable<T>ermöglicht eine verzögerte Auswertung, das Zurückgeben List<T>nicht).
  • Eine Sequenz kann unendlich sein und es daher dem Aufrufer überlassen, zu entscheiden, wie viele Elemente zurückgegeben werden sollen.
Brian Rasmussen
quelle
Das Zurückgeben einer Sammlung kann zu einem unangemessenen Overhead führen, wenn der Client nur eine Aufzählung durchführen muss, da Sie die Datenstrukturen für alle Elemente im Voraus zuweisen. Wenn Sie an eine andere Methode delegieren, die eine Sequenz zurückgibt, erfordert das Erfassen als Sammlung zusätzliches Kopieren, und Sie wissen nicht, wie viele Elemente (und damit wie viel Overhead) dies möglicherweise bedeuten kann. Daher ist es nur eine gute Idee, die Sammlung zurückzugeben, wenn sie bereits vorhanden ist und direkt ohne Kopieren zurückgegeben werden kann (oder als schreibgeschützt verpackt wird). In allen anderen Fällen ist die Reihenfolge eine bessere Wahl
Pavel Minaev
Ich stimme zu, und wenn Sie den Eindruck haben, dass die Rückgabe einer Sammlung immer eine gute Idee ist, haben Sie meinen Standpunkt verfehlt. Ich habe versucht, die Tatsache hervorzuheben, dass es Unterschiede zwischen der Rückgabe einer Sammlung und der Rückgabe einer Sequenz gibt. Ich werde versuchen, es klarer zu machen.
Brian Rasmussen