Возвратите все перечислимые с доходностью сразу; без цикла

164

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

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

Джон Оксли
источник
Я счастлив (и любопытно!) Видеть, как возникают новые вопросы о доходности - я сам не совсем понимаю. Не глупый вопрос!
JoshJordan
Что такое GetCardProductionValidationErrorsFor?
Эндрю Харе
4
что не так с возвратом GetMoreErrors (card); ?
Сэм Шафран
10
@Sam: «дальнейший доход приносит больше ошибок проверки»
Джон Скит
1
С точки зрения не неоднозначного языка, одна проблема заключается в том, что метод не может знать, существует ли что-либо, реализующее как T, так и IEnumerable <T>. Таким образом, вам нужна другая конструкция в урожайности. Тем не менее, было бы неплохо иметь способ сделать это. Yield return yield foo, возможно, где foo реализует IEnumerable <T>?
Уильям Джокуш

Ответы:

141

Это определенно не глупый вопрос, и это то, что F # поддерживает yield!для всей коллекции против 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()прежде чем он даже спросит о следующих ошибках.

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

Джон Скит
источник
3
У Уэса Дайера есть интересная статья, упоминающая эту модель. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Незначительная коррекция для прохожих - это System.Linq.Enumeration.Concat <> (первая, вторая). Не IEnumeration.Concat ().
Redcalx
@ the-locster: Я не уверен, что вы имеете в виду. Это определенно Enumerable, а не Enumeration. Не могли бы вы уточнить свой комментарий?
Джон Скит
@ Джон Скит - Что именно вы имеете в виду, что он будет вызывать методы немедленно? Я запустил тест, и похоже, что он полностью откладывает вызовы методов до тех пор, пока что-то фактически не будет повторено. Код здесь: pastebin.com/0kj5QtfD
Стивен Оксли
5
@ Стивен: Нет. Он вызывает методы - но в вашем случае GetOtherErrors()(и т. Д.) Откладывают их результаты (поскольку они реализованы с использованием блоков итераторов). Попробуйте изменить их, чтобы получить новый массив или что-то в этом роде, и вы поймете, что я имею в виду.
Джон Скит
26

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

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также будет отложено.

Адам Боддингтон
источник
16

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

yield_ snipped использование анимации

Вот фрагмент 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
Как это ответ на вопрос?
Ян Кемп
1
@ Ян, вот как вы должны делать вложенный возврат в C #. Нет yield!, как в F #.
Джон Гитцен
это не ответ на вопрос
divyang4481
8

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

Думайте о 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;
    }
  }
}
Тим Джарвис
источник
4

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

Фрэнк Брайс
источник
2
Вероятно, никто не думал о методе Extension, потому что DoGetErrors(card).SelectMany(x => x)делает то же самое и сохраняет отложенное поведение. Это именно то, что Адам предлагает в своем ответе .
huysentruitw
3

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

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

Коллекции

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

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

  • Можно перечислить - и это почти все, что мы можем сказать наверняка.
  • Сама возвращаемая последовательность не может быть изменена.
  • Каждый элемент может быть создан как часть выполнения последовательности (т. Е. Возврат IEnumerable<T>допускает ленивую оценку, возврат List<T>- нет).
  • Последовательность может быть бесконечной, и, таким образом, вызывающая сторона может решить, сколько элементов должно быть возвращено.
Брайан Расмуссен
источник
Возврат коллекции может привести к необоснованным накладным расходам, если все, что действительно нужно клиенту, - это перечислить через него, поскольку вы заранее выделяете структуры данных для всех элементов. Кроме того, если вы делегируете другому методу, возвращающему последовательность, то захват ее как коллекции включает дополнительное копирование, и вы не знаете, сколько элементов (и, следовательно, сколько накладных расходов) это может потребовать. Таким образом, будет хорошей идеей возвращать коллекцию, когда она уже существует и может быть возвращена напрямую без копирования (или упакована как только для чтения). Во всех остальных случаях последовательность - лучший выбор
Павел Минаев
Я согласен, и если у вас сложилось впечатление, что я сказал, что возвращать коллекцию всегда хорошая идея, вы упустили мою мысль. Я пытался подчеркнуть тот факт, что существуют различия между возвратом коллекции и возвращением последовательности. Я постараюсь прояснить ситуацию.
Брайан Расмуссен