立即退还所有可枚举的收益;没有循环


164

我具有以下功能来获取卡的验证错误。我的问题与处理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而不必通过错误进行枚举?

考虑一下这可能是一个愚蠢的问题,但是我想确保自己没有错。


我很高兴(也很好奇!)看到更多的收益率问题出现-我自己不太了解。这不是一个愚蠢的问题!
2009年

什么GetCardProductionValidationErrorsFor
安德鲁·黑尔

4
什么是错与回报GetMoreErrors(卡);
Sam Saffron

10
@Sam:“进一步的收益回报带来更多的验证错误”
Jon Skeet

1
从无歧义的语言的角度来看,一个问题是该方法无法知道是否存在同时实现T和IEnumerable <T>的任何东西。因此,您需要在产量上使用其他结构。就是说,一定有一种方法可以做到这一点。Yield return yield foo,也许,foo在哪里实现IEnumerable <T>?
William Jockusch '16

Answers:


140

这绝对不是一个愚蠢的问题,它是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有一篇有趣的文章提到了这种模式。blogs.msdn.com/wesdyer/archive/2007/03/23/...
JohannesH

1
对路人的较小修正,即-System.Linq.Enumeration.Concat <>(第一,第二)。不是IEnumeration.Concat()。
redcalx

@ -locster:我不确定你的意思。绝对是可枚举的,而不是枚举的。您能否澄清您的评论?
乔恩·斯基特

@Jon Skeet-您将立即调用方法的确切含义是什么?我运行了一个测试,看起来它完全推迟了方法调用,直到实际进行迭代为止。此处的代码:pastebin.com/0kj5QtfD
史蒂芬·奥克斯利

5
@Steven:不。它在调用方法-但在您的情况下GetOtherErrors()(etc)推迟了它们的结果(因为它们是使用迭代器块实现的)。尝试更改它们以返回一个新数组或类似的东西,您将明白我的意思。
乔恩·斯基特

26

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


16

我想出了一个简短的yield_摘要:

yield_剪辑用法动画

这是片段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>

2
这如何回答这个问题?
伊恩·肯普

@Ian,这是您必须在C#中执行嵌套收益率的方式。没有yield!像F#中那样。
John Gietzen

这不是问题的答案
divyang4481

8

我看不到您的功能有什么问题,我想说它在做您想要的事情。

可以将Yield看作是每次调用最终Enumeration时都返回一个元素,因此,当您将它放在这样的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

令我惊讶的是,没有人想到推荐一种简单的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呼叫站点。


2
可能没有人考虑过扩展方法,因为DoGetErrors(card).SelectMany(x => x)这样做并保留了延迟的行为。亚当的回答正是如此。
huysentruitw

3

是的,可以一次返回所有错误。只需返回一个List<T>ReadOnlyCollection<T>

通过返回 IEnumerable<T>您将返回一个序列的东西。从表面上看,这与返回收藏集相同,但是有许多区别,您应该牢记。

馆藏

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

顺序

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

如果客户端真正需要的全部是通过枚举枚举,则返回集合可能导致不合理的开销,因为您事先为所有元素分配了数据结构。另外,如果您委托给另一个返回序列的方法,那么将其捕获为一个集合将涉及额外的复制,并且您不知道这可能涉及多少个项目(以及多少开销)。因此,最好在集合已经存在并且可以直接返回而不进行复制(或包装为只读)的情况下返回它。在所有其他情况下,顺序是更好的选择
Pavel Minaev 09年

我同意,如果您有印象,我说退货总是一个好主意,那您就错了我的意思。我试图强调一个事实,即返回集合和返回序列之间存在差异。我将尝试使其更加清晰。
布赖恩·拉斯穆森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.