Вложенная доходность возврата с помощью 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
на вызывающий.