一次性返回所有使用yield return的可枚举对象,而不需要通过循环来实现。

201

我有以下函数用于获取卡片的验证错误。我的问题与处理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中的所有错误?


4
"*return GetMoreErrors(card);*有什么问题?" - Sam Saffron
10
更多的收益回报会导致更多的验证错误。 - Jon Skeet
1
从一个非歧义语言的角度来看,一个问题是该方法无法知道是否存在同时实现了T和IEnumerable<T>的对象。因此,在yield中需要使用不同的结构。话虽如此,拥有一种方式做到这一点肯定是很好的。也许可以使用“yield return yield foo”,其中foo实现了IEnumerable <T>? - William Jockusch
2
对于那些感兴趣的人,C#语言功能请求在这里:https://github.com/dotnet/csharplang/issues/378。 - StayOnTarget
针对参考问题378,实际上最初是一个更复杂的边缘情况,涉及性能考虑(递归调用之类的东西?我不确定)。在那次对话的基础上,我发起了一个专门的讨论,仅用于提出这种纯语法糖的变更:https://github.com/dotnet/csharplang/discussions/5303 - Brondahl
6个回答

173

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
Wes Dyer在他的一篇文章中提到了这种模式。 http://blogs.msdn.com/wesdyer/archive/2007/03/23/all-about-iterators.aspx - JohannesH
1
给路过的朋友一个小提示 - 正确的写法是 System.Linq.Enumeration.Concat<>(first,second),而不是 IEnumeration.Concat()。 - redcalx
1
@Jon Skeet - 你所说的“它会立即调用方法”是什么意思?我进行了一项测试,看起来它完全推迟了方法的调用,直到实际迭代某些东西。代码在这里:http://pastebin.com/0kj5QtfD - Steven Oxley
7
@Steven:不是在执行方法,而是在调用方法,但在您的情况下,GetOtherErrors()(等等)正在推迟它们的结果(因为它们使用迭代器块实现)。尝试将它们更改为返回一个新数组或类似的东西,您就会明白我的意思。 - Jon Skeet
1
@Jon 好的,我明白了。我想我忽略了一个事实,即数组也实现了IEnumerable,并且调用方法和获取方法结果是两回事(我对迭代器块的概念还很新)。谢谢你的澄清。 - Steven Oxley
显示剩余4条评论

37
您可以像这样设置所有错误源(方法名称借用自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 方法的执行也将被延迟。


22

我想出了一个快速的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
这怎么是对问题的回答? - Ian Kemp
3
@Ian,以下是在C#中嵌套使用yield return的方法。与F#不同,C#没有yield!关键字。 - John Gietzen
这不是对问题的回答 - divyang4481
1
我发现使用代码片段比较讨论代码片段是否有用更有用。 - increddibelly

9

我很惊讶没有人想到推荐一个简单的扩展方法来处理 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 移动到调用方。

6
可能没有人考虑过扩展方法,因为 DoGetErrors(card).SelectMany(x => x) 实现了相同的功能并保留了延迟行为。这正是 Adam 在 他的答案 中建议的。 - huysentruitw

8
我认为你的函数没有问题,它正在按照你想要的方式运行。
把Yield想象成每次调用时返回最终枚举中的一个元素,所以当你在foreach循环中使用它时,每次调用它都会返回1个元素。你可以在foreach中加入条件语句来过滤结果集(只需在不符合排除条件的情况下不yield即可)。
如果你在方法后面添加了后续的yields,它将继续向枚举中添加1个元素,从而使像这样的操作成为可能...
public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists)
{
  foreach (IEnumerable<string> list in lists)
  {
    foreach (string s in list)
    {
      yield return s;
    }
  }
}

3
是的,可以一次性返回所有错误。只需返回一个List<T>ReadOnlyCollection<T>
通过返回IEnumerable<T>,您正在返回一系列内容。表面上看,这似乎与返回集合相同,但是有许多差异需要您牢记。
集合
- 调用者可以确保在返回集合时,集合和所有项都存在。如果必须每次调用创建集合,则返回集合是一个非常糟糕的想法。 - 大多数集合在返回时可以被修改。 - 集合具有有限大小。
序列
- 可以枚举 - 这基本上就是我们能够肯定说的了。 - 返回的序列本身无法修改。 - 每个元素可以作为运行序列的一部分而创建(即返回IEnumerable<T>允许进行惰性评估,而返回List<T>则不允许)。 - 序列可能是无限的,因此由调用者决定应返回多少个元素。

如果客户端只需要枚举集合,那么返回一个集合可能会导致不合理的开销,因为你需要提前为所有元素分配数据结构。此外,如果你委托给另一个返回序列的方法,那么将其捕获为集合会涉及额外的复制,并且你不知道这可能涉及多少项(以及多少开销)。因此,仅当集合已经存在并且可以直接返回而无需复制(或作为只读包装)时,才建议返回集合。在所有其他情况下,序列是更好的选择。 - Pavel Minaev
我同意,如果你认为返回一个集合总是一个好主意,那么你就误解了我的意思。我试图强调返回集合和返回序列之间存在差异的事实。我会尽力让它更清晰明了。 - Brian Rasmussen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接