ChatGPT解决这个技术问题 Extra ChatGPT

一次返回所有具有收益返回的枚举;不循环

我有以下功能来获取卡的验证错误。我的问题与处理 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,我相信。
对于参考问题 378,实际上是出于性能考虑(关于递归调用?idk。)的更复杂的边缘情况。在那次谈话的背后,我提出了一个专门的讨论,只是为了将更改作为纯粹的语法糖来宣传:github.com/dotnet/csharplang/discussions/5303

v
vinzee

这是 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() 中的所有内容后,才会询问下一个错误。

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


Wes Dyer 有一篇有趣的文章提到了这种模式。 blogs.msdn.com/wesdyer/archive/2007/03/23/…
对路人的小修正 - 它是 System.Linq.Enumeration.Concat<>(first,second)。不是 IEnumeration.Concat()。
@Jon Skeet-它会立即调用方法到底是什么意思?我运行了一个测试,看起来它完全推迟了方法调用,直到实际迭代某些东西。此处代码:pastebin.com/0kj5QtfD
@史蒂文:不。它正在 调用 方法 - 但在您的情况下 GetOtherErrors() (等)正在推迟它们的 results (因为它们是使用迭代器块实现的)。尝试更改它们以返回一个新数组或类似的东西,你会明白我的意思。
@Jon 好的,我明白了。我想我忽略了一个事实,即数组也实现了 IEnumerable,并且调用方法和获取方法的结果是两件不同的事情(我对迭代器块的想法很陌生)。感谢您的澄清。
A
Adam Boddington

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


r
riQQ

我想出了一个快速的 yield_ 片段:

https://i.stack.imgur.com/orXEZ.gif

这是片段 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# 中一样。
这不是问题的答案
我当然发现使用片段比争论片段是否有用更有用。
T
Tim Jarvis

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

将 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;
    }
  }
}

h
huysentruitw

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


可能没有人考虑过扩展方法,因为 DoGetErrors(card).SelectMany(x => x) 做了同样的事情并保留了延迟行为。这正是亚当在 his answer 中所建议的。
B
Brian Rasmussen

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

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

收藏品

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

大多数集合可以在返回时进行修改。

集合的大小是有限的。

序列

可以列举——这几乎就是我们可以肯定地说的所有内容。

返回的序列本身不能被修改。

每个元素都可以作为运行序列的一部分创建(即返回 IEnumerable 允许延迟评估,返回 List 不允许)。

一个序列可能是无限的,因此由调用者决定应该返回多少元素。


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