Доходность вложенных возвращение с интерфейс 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 без необходимости перечислять через них?

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

6 132

6 ответов:

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

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

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

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

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

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

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

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 будет слишком затягивается.

Я придумал быстрый 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>

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

подумайте о том, что Yield возвращает элемент в конечном перечислении каждый раз, когда он вызывается, поэтому, когда он находится в цикле 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;
    }
  }
}

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

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

коллекции

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

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

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

Я удивлен, что никто не подумал рекомендовать простой метод расширения на 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 в callsite.