【问题标题】:Return all enumerables with yield return at once; without looping through一次返回所有具有收益返回的枚举;不循环
【发布时间】:2010-11-19 04:38:50
【问题描述】:

我有以下函数来获取卡的验证错误。我的问题与处理 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 中的所有错误都返回而不必枚举它们?

【问题讨论】:

  • 我很高兴(也很好奇!)看到出现更多收益回报问题 - 我自己也不太明白。不是一个愚蠢的问题!
  • return GetMoreErrors(card); 有什么问题?
  • @Sam:“更多的验证错误会带来更多收益”
  • 从非歧义语言的角度来看,一个问题是该方法无法知道是否有任何东西同时实现了 T 和 IEnumerable。所以你需要一个不同的产量结构。也就是说,有办法做到这一点肯定会很好。 Yield return yield foo,也许,foo 在哪里实现 IEnumerable
  • 对于那些感兴趣的人,对此的 C# 语言功能请求位于此处:github.com/dotnet/csharplang/issues/378,我相信。

标签: c# ienumerable yield yield-return


【解决方案1】:

这绝对不是一个愚蠢的问题,它是 F# 支持的东西,yield! 用于整个集合,yield 用于单个项目。 (这在尾递归方面非常有用......)

很遗憾,C# 不支持它。

但是,如果您有多个方法,每个方法都返回一个IEnumerable&lt;ErrorInfo&gt;,您可以使用Enumerable.Concat 来简化您的代码:

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

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

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

【讨论】:

  • Wes Dyer 有一篇有趣的文章提到了这种模式。 blogs.msdn.com/wesdyer/archive/2007/03/23/…
  • 对路人的小修正 - 它是 System.Linq.Enumeration.Concat(first,second)。不是 IEnumeration.Concat()。
  • @the-locster:我不确定你的意思。它绝对是 Enumerable 而不是 Enumeration。你能澄清你的评论吗?
  • @Jon Skeet - 它会立即调用这些方法到底是什么意思?我运行了一个测试,看起来它完全推迟了方法调用,直到实际迭代某些东西。代码在这里:pastebin.com/0kj5QtfD
  • @Steven:不。它是 calling 方法 - 但在您的情况下,GetOtherErrors() (etc) 正在推迟它们的 results (因为它们是使用迭代器块实现的)。尝试更改它们以返回一个新数组或类似的东西,你就会明白我的意思了。
【解决方案2】:

您可以像这样设置所有错误源(方法名称从 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 中方法的执行也会延迟。

【讨论】:

    【解决方案3】:

    我想出了一个快速的yield_sn-p:

    这是 sn-p 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>
    

    【讨论】:

    • 这个问题的答案如何?
    • @Ian,这就是你必须在 C# 中进行嵌套 yield return 的方式。没有yield!,就像在 F# 中一样。
    • 这不是问题的答案
    【解决方案4】:

    我很惊讶没有人想到在IEnumerable&lt;IEnumerable&lt;T&gt;&gt; 上推荐一个简单的扩展方法来使此代码保持其延迟执行。出于多种原因,我喜欢延迟执行,其中一个原因是即使对于庞大的可枚举项,内存占用量也很小。

    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 移动到调用点。

    【讨论】:

    • 可能没有人考虑过扩展方法,因为DoGetErrors(card).SelectMany(x =&gt; x) 做了同样的事情并保留了延迟行为。这正是亚当在his answer 中所建议的。
    【解决方案5】:

    我看不出你的功能有什么问题,我会说它正在做你想做的事。

    将 Yield 视为每次调用它时在最终枚举中返回一个元素,因此当您将它放在这样的 foreach 循环中时,每次调用它时都会返回 1 个元素。您可以在 foreach 中放置条件语句来过滤结果集。 (只需不屈服于您的排除标准)

    如果稍后在方法中添加后续 yield,它将继续向枚举中添加 1 个元素,从而可以执行...

    public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists)
    {
      foreach (IEnumerable<string> list in lists)
      {
        foreach (string s in list)
        {
          yield return s;
        }
      }
    }
    

    【讨论】:

      【解决方案6】:

      是的,可以一次返回所有错误。只需返回 List&lt;T&gt;ReadOnlyCollection&lt;T&gt;

      通过返回IEnumerable&lt;T&gt;,您将返回一系列内容。从表面上看,这似乎与归还收藏品相同,但有许多不同之处,您应该记住。

      收藏

      • 调用者可以确定集合返回时集合和所有项目都将存在。如果每次调用都必须创建集合,那么返回集合是一个非常糟糕的主意。
      • 大多数集合在返回时都可以修改。
      • 集合大小有限。

      序列

      • 可以枚举 - 这几乎是我们可以肯定的全部内容。
      • 无法修改返回的序列本身。
      • 每个元素都可以作为序列运行的一部分创建(即返回 IEnumerable&lt;T&gt; 允许延迟评估,返回 List&lt;T&gt; 不允许)。
      • 一个序列可能是无限的,因此由调用者决定应该返回多少元素。

      【讨论】:

      • 如果客户端真正需要的只是枚举它,那么返回一个集合可能会导致不合理的开销,因为您提前为所有元素分配了数据结构。此外,如果您委托给另一个返回序列的方法,那么将其捕获为一个集合会涉及额外的复制,并且您不知道这可能涉及多少项(以及多少开销)。因此,只有当集合已经存在并且可以直接返回而无需复制(或包装为只读)时才返回它是一个好主意。在所有其他情况下,顺序是更好的选择
      • 我同意,如果你觉得我说归还收藏总是一个好主意,那你就错过了我的意思。我试图强调返回集合和返回序列之间存在差异的事实。我会尽量让它更清楚。
      猜你喜欢
      • 2012-11-04
      • 1970-01-01
      • 2016-10-04
      • 2019-05-16
      • 1970-01-01
      • 2017-06-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多