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ę.


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.


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.


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>

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

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.


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.

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.