嵌套的yield返回IEnumerable

时间:2009-08-13 04:26:41

标签: c# ienumerable yield yield-return

我有以下功能来获取卡的验证错误。我的问题涉及处理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 个答案:

答案 0 :(得分:130)

这绝对不是一个愚蠢的问题,而且F#支持整个集合的yield!与单个项目的yieldIEnumerable<ErrorInfo>。 (这在尾递归方面非常有用......)

不幸的是,C#不支持它。

但是,如果您有多个方法都返回Enumerable.Concat,则可以使用private static IEnumerable<ErrorInfo> GetErrors(Card card) { return GetMoreErrors(card).Concat(GetOtherErrors()) .Concat(GetValidationErrors()) .Concat(AnyMoreErrors()) .Concat(ICantBelieveHowManyErrorsYouHave()); } 使代码更简单:

GetMoreErrors()

这两个实现之间有一个非常重要的区别:这个方法会立即调用所有方法 ,即使它一次只使用一个返回的迭代器。您的现有代码将等到{{1}}中的所有内容循环,直到询问有关下一个错误。

通常这并不重要,但值得了解将会发生什么。

答案 1 :(得分:19)

您可以像这样设置所有错误来源(从Jon Skeet的答案中借用的方法名称)。

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中方法的执行也会延迟。

答案 2 :(得分:13)

我提出了一个快速的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>

答案 3 :(得分:7)

我认为你的功能没有任何问题,我会说它正在做你想要的。

将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 :(得分:3)

是的,可以立即返回所有错误。只需返回List<T>ReadOnlyCollection<T>即可。

通过返回IEnumerable<T>,您将返回一系列内容。表面上可能看起来与返回集合相同,但有许多区别,你应该记住。

集合

  • 调用者可以确保在返回集合时,集合和所有项都将存在。如果必须在每次调用时创建集合,则返回集合是一个非常糟糕的主意。
  • 返回时可以修改大多数馆藏。
  • 该系列的尺寸有限。

序列

  • 可以枚举 - 这几乎是我们可以肯定的。
  • 无法修改返回的序列本身。
  • 每个元素都可以作为贯穿序列的一部分创建(即返回IEnumerable<T>允许进行延迟评估,返回List<T>则不会。)
  • 一个序列可能是无限的,因此留给调用者来决定应该返回多少个元素。

答案 5 :(得分:3)

我很惊讶没有人想过在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移到调用点。