Вложенная доходность возврата с помощью IEnumerable

У меня есть следующая функция, чтобы получить ошибки проверки для карты. Мой вопрос относится к работе с GetErrors. Оба метода имеют одинаковый тип возврата 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
}

Можно ли вернуть все ошибки в GetMoreErrors, не перебирая их?

Размышление об этом, вероятно, является глупым вопросом, но я хочу убедиться, что я не ошибусь.

Ответ 1

Это определенно не глупый вопрос, и это то, что F # поддерживает с yield! для целого набора vs yield для одного элемента. (Это может быть очень полезно с точки зрения рекурсии хвоста...)

К сожалению, он не поддерживается в С#.

Однако, если у вас есть несколько методов, возвращающих IEnumerable<ErrorInfo>, вы можете использовать Enumerable.Concat, чтобы сделать ваш код проще:

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

Существует одно очень важное различие между этими двумя реализациями: этот метод сразу вызовет все методы, хотя он будет использовать только возвращенные итераторы по одному. Ваш существующий код будет ждать, пока он не зациклится на все в GetMoreErrors(), прежде чем он даже спросит о следующих ошибках.

Обычно это не важно, но стоит понять, что произойдет, когда.

Ответ 2

Вы можете настроить все источники ошибок, подобные этому (имена методов, заимствованные из ответа Джона Скита).

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

Затем вы можете перебирать их в одно и то же время.

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

В качестве альтернативы вы можете сгладить источники ошибок с помощью SelectMany.

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

Выполнение методов в GetErrorSources также будет задерживаться.

Ответ 3

Я придумал быстрый yield_ фрагмент:

yield_ snipped usage animation

Здесь фрагмент 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>

Ответ 4

Я не вижу ничего плохого в вашей функции, я бы сказал, что он делает то, что вы хотите.

Подумайте о Доходе как возврате элемента в последнем перечислении каждый раз, когда он вызывается, поэтому, когда вы его в цикле foreach, как это, каждый раз, когда он вызывается, он возвращает 1 элемент. У вас есть возможность помещать условные утверждения в ваш foreach для фильтрации набора результатов. (просто не уступая по вашим критериям исключения)

Если вы добавите последующие результаты позже в метод, он будет продолжать добавлять 1 элемент в перечисление, что позволяет делать такие вещи, как...

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

Ответ 5

Да, возможно сразу вернуть все ошибки. Просто верните List<T> или ReadOnlyCollection<T>.

Вернув IEnumerable<T>, вы возвращаете последовательность чего-то. На поверхности, которая может показаться идентичной возврату коллекции, но есть ряд отличий, вы должны иметь в виду.

Коллекции

  • Вызывающий может быть уверен, что и коллекция, и все элементы будут существовать при возврате коллекции. Если сбор должен быть создан для каждого звонка, возвращение коллекции - действительно плохая идея.
  • Большинство коллекций могут быть изменены при возврате.
  • Коллекция имеет конечный размер.

Последовательности

  • Можно перечислить - и это почти все, что мы можем сказать наверняка.
  • Невозможно изменить измененную возвращенную последовательность.
  • Каждый элемент может быть создан как часть выполнения последовательности (т.е. возврат IEnumerable<T> допускает ленивую оценку, возврат List<T> нет).
  • Последовательность может быть бесконечной и, таким образом, оставлять ее вызывающей стороне, чтобы решить, сколько элементов должно быть возвращено.

Ответ 6

Я удивлен, что никто не подумал рекомендовать простой метод Extension на IEnumerable<IEnumerable<T>>, чтобы этот код сохранял свое отсроченное выполнение. Я поклонник отложенного исполнения по многим причинам, один из которых заключается в том, что объем памяти невелик даже для огромных перечислений.

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

И вы можете использовать его в своем случае, как это

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
}

Аналогично, вы можете обойтись с помощью функции-оболочки вокруг DoGetErrors и просто переместить UnWrap на вызывающий.