Zwróć wszystkie wyliczalne z wydajnością na raz; bez zapętlania

164

Mam następującą funkcję, aby uzyskać błędy walidacji karty. Moje pytanie dotyczy radzenia sobie z GetErrors. Obie metody mają ten sam typ zwrotu 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
}

Czy można zwrócić wszystkie błędy GetMoreErrorsbez konieczności ich wyliczania?

Myśląc o tym, jest to prawdopodobnie głupie pytanie, ale chcę się upewnić, że się nie pomylę.

John Oxley
źródło
Jestem szczęśliwy (i ciekawy!), Widząc, że pojawia się więcej pytań dotyczących zwrotu zysku - sam tego nie rozumiem. To nie jest głupie pytanie!
JoshJordan
Co to jest GetCardProductionValidationErrorsFor?
Andrew Hare
4
co jest nie tak z return GetMoreErrors (karta); ?
Sam Saffron
10
@Sam: „dalsze zyski zwracają się po więcej błędów walidacji”
Jon Skeet,
1
Z punktu widzenia niejednoznacznego języka jednym problemem jest to, że metoda nie może wiedzieć, czy istnieje coś, co implementuje zarówno T, jak i IEnumerable <T>. Potrzebujesz więc innej konstrukcji w wydajności. To powiedziawszy, z pewnością byłoby miło mieć na to sposób. Wydajność zwrotu zysku foo, być może, gdzie foo implementuje IEnumerable <T>?
William Jockusch

Odpowiedzi:

141

To zdecydowanie nie jest głupie pytanie i jest to coś, co F # obsługuje yield!dla całej kolekcji w porównaniu yieldz pojedynczym elementem. (Może to być bardzo przydatne w przypadku rekurencji ogonowej ...)

Niestety nie jest obsługiwany w C #.

Jeśli jednak masz kilka metod, z których każda zwraca an IEnumerable<ErrorInfo>, możesz użyć, Enumerable.Concataby uprościć kod:

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

Jest jednak jedna bardzo ważna różnica między tymi dwiema implementacjami: ta wywoła wszystkie metody natychmiast , nawet jeśli będzie używać zwracanych iteratorów tylko po jednej naraz. Twój istniejący kod będzie czekał, aż zostanie zapętlony, GetMoreErrors()zanim zapyta o kolejne błędy.

Zwykle nie jest to ważne, ale warto zrozumieć, co się stanie i kiedy.

Jon Skeet
źródło
3
Wes Dyer ma ciekawy artykuł wspominający o tym wzorze. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Drobna poprawka dla przechodniów - to System.Linq.Enumeration.Concat <> (pierwszy, drugi). Nie IEnumeration.Concat ().
redcalx
@ the-locster: Nie jestem pewien, co masz na myśli. Jest to zdecydowanie bardziej wyliczalne niż wyliczenie. Czy mógłbyś wyjaśnić swój komentarz?
Jon Skeet
@Jon Skeet - Co dokładnie masz na myśli, że wywoła metody natychmiast? Uruchomiłem test i wygląda na to, że całkowicie odracza wywołania metody, dopóki coś nie zostanie faktycznie iterowane. Kod tutaj: pastebin.com/0kj5QtfD
Steven Oxley
5
@Steven: Nie. To wywołanie metody - ale w Twoim przypadku GetOtherErrors()(etc) są odroczenia ich wyniki (jak są one realizowane za pomocą iteratora bloki). Spróbuj je zmienić, aby zwrócić nową tablicę lub coś w tym rodzaju, a zobaczysz, o co mi chodzi.
Jon Skeet
26

Możesz skonfigurować wszystkie źródła błędów w ten sposób (nazwy metod zapożyczone z odpowiedzi Jona Skeeta).

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

Następnie możesz je powtarzać w tym samym czasie.

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

Alternatywnie możesz spłaszczyć źródła błędów za pomocą SelectMany.

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

Wykonanie metod w również GetErrorSourcesbędzie opóźnione.

Adam Boddington
źródło
17

Wymyśliłem krótki yield_fragment:

yield_ cutped animacja użytkowania

Oto fragment kodu 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
źródło
2
Jak to jest odpowiedź na pytanie?
Ian Kemp
1
@Ian, w ten sposób musisz wykonać zagnieżdżony zwrot zysku w C #. Nie ma yield!, jak w F #.
John Gietzen
to nie jest odpowiedź na pytanie
divyang4481
8

Nie widzę nic złego w twojej funkcji, powiedziałbym, że robi to, co chcesz.

Pomyśl o Yield jako o zwracaniu elementu w końcowym wyliczeniu za każdym razem, gdy jest on wywoływany, więc jeśli masz go w takiej pętli foreach, za każdym razem, gdy jest wywoływany, zwraca 1 element. Masz możliwość umieszczania instrukcji warunkowych w swoim foreach, aby filtrować zestaw wyników. (po prostu nie poddając się kryteriom wykluczenia)

Jeśli dodasz kolejne plony później w metodzie, będzie on nadal dodawać 1 element do wyliczenia, umożliwiając wykonanie takich czynności jak ...

public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists)
{
  foreach (IEnumerable<string> list in lists)
  {
    foreach (string s in list)
    {
      yield return s;
    }
  }
}
Tim Jarvis
źródło
4

Jestem zaskoczony, że nikt nie pomyślał o poleceniu prostej metody rozszerzenia, IEnumerable<IEnumerable<T>>aby ten kod zachował odroczone wykonanie. Jestem fanem odroczonego wykonania z wielu powodów, jednym z nich jest to, że ślad pamięci jest niewielki nawet dla ogromnych wyliczeń.

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

I możesz tego użyć w swoim przypadku w ten sposób

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
}

Podobnie możesz pozbyć się funkcji opakowującej DoGetErrorsi po prostu przejść UnWrapdo callsite.

Frank Bryce
źródło
2
Prawdopodobnie nikt nie pomyślał o metodzie rozszerzenia, ponieważ DoGetErrors(card).SelectMany(x => x)robi to samo i zachowuje odroczone zachowanie. Dokładnie to sugeruje Adam w swojej odpowiedzi .
huysentruitw
3

Tak, istnieje możliwość natychmiastowego zwrócenia wszystkich błędów. Po prostu zwróć List<T>lub ReadOnlyCollection<T>.

Zwracając IEnumerable<T>i zwracasz sekwencję czegoś. Na pozór może wydawać się to identyczne jak zwrot kolekcji, ale istnieje kilka różnic, o których należy pamiętać.

Kolekcje

  • Wzywający może być pewien, że zarówno kolekcja, jak i wszystkie elementy będą istniały, gdy kolekcja zostanie zwrócona. Jeśli kolekcja musi zostać utworzona na wywołanie, zwracanie kolekcji jest naprawdę złym pomysłem.
  • Większość kolekcji można zmodyfikować po zwróceniu.
  • Kolekcja ma ograniczony rozmiar.

Sekwencje

  • Można to wyliczyć - i to prawie wszystko, co możemy powiedzieć na pewno.
  • Zwracanej sekwencji nie można modyfikować.
  • Każdy element może zostać utworzony jako część przebiegu sekwencji (tj. Zwracanie IEnumerable<T>pozwala na leniwą ocenę, a zwracanie List<T>nie).
  • Sekwencja może być nieskończona, dlatego wywołującemu należy pozostawić decyzję, ile elementów ma zostać zwróconych.
Brian Rasmussen
źródło
Zwracanie kolekcji może skutkować nieracjonalnym narzutem, jeśli klient naprawdę potrzebuje tylko wyliczenia za jej pośrednictwem, ponieważ z góry przydzielasz struktury danych dla wszystkich elementów. Ponadto, jeśli delegujesz do innej metody, która zwraca sekwencję, przechwytywanie jej jako kolekcji wymaga dodatkowego kopiowania i nie wiesz, ile elementów (a tym samym ile narzutów) może to potencjalnie wiązać. Dlatego dobrym pomysłem jest zwrócenie kolekcji tylko wtedy, gdy już tam jest i można ją zwrócić bezpośrednio bez kopiowania (lub opakować jako tylko do odczytu). We wszystkich innych przypadkach lepszym wyborem jest sekwencja
Pavel Minaev
Zgadzam się, a jeśli miałeś wrażenie, że powiedziałem, że zwrot kolekcji jest zawsze dobrym pomysłem, przegapiłeś mój punkt widzenia. Próbowałem podkreślić fakt, że istnieją różnice między zwróceniem kolekcji a zwróceniem sekwencji. Postaram się to wyjaśnić.
Brian Rasmussen