嵌套的yield返回IEnumerable

Joh*_*ley 158 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
}
Run Code Online (Sandbox Code Playgroud)

是否可以返回所有错误GetMoreErrors而无需通过它们进行枚举?

想一想这可能是一个愚蠢的问题,但我想确保我不会出错.

Jon*_*eet 136

这绝对不是一个愚蠢的问题,它是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());
}
Run Code Online (Sandbox Code Playgroud)

但是这两个实现之间有一个非常重要的区别:这个方法会立即调用所有方法,即使它一次只使用一个返回的迭代器.您现有的代码将等到它GetMoreErrors()之前的所有内容循环,甚至会询问下一个错误.

通常这并不重要,但值得了解什么时候会发生什么.

  • @Steven:不.它是*调用*方法 - 但在你的情况下,`GetOtherErrors()`(等)正在推迟它们的*结果*(因为它们是使用迭代器块实现的).尝试更改它们以返回一个新的数组或类似的东西,你会明白我的意思. (5认同)
  • Wes Dyer有一篇有趣的文章提到了这种模式.http://blogs.msdn.com/wesdyer/archive/2007/03/23/all-about-iterators.aspx (3认同)

Ada*_*ton 23

您可以像这样设置所有错误源(从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();
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以同时迭代它们.

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

或者,您可以使用平坦化错误源SelectMany.

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

方法的执行GetErrorSources也会延迟.


Joh*_*zen 15

我想出了一个快速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>
Run Code Online (Sandbox Code Playgroud)

  • 这个问题的答案如何? (3认同)
  • @Ian,这就是您必须在 C# 中执行嵌套收益返回的方式。没有 `yield!`,就像在 F# 中一样。 (2认同)
  • 我当然发现使用该片段比争论该片段是否有用更有用。 (2认同)

Tim*_*vis 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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)


Fra*_*yce 7

我很惊讶没有人想到推荐一种简单的扩展方法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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以像这样在你的情况下使用它

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
}
Run Code Online (Sandbox Code Playgroud)

类似地,您可以取消周围的包装函数,DoGetErrors而只需移至UnWrap调用站点。

  • 可能没有人考虑过扩展方法,因为`DoGetErrors(card).SelectMany(x =&gt; x)` 做了同样的事情并保留了延迟行为。这正是亚当在 [他的回答](http://stackoverflow.com/a/22912410/1300910) 中所建议的。 (4认同)