web-dev-qa-db-ja.com

IEnumerableを使用した入れ子になったイールドリターン

カードの検証エラーを取得するには、次の機能があります。私の質問は、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のすべてのエラーを返すことは可能ですか?

それについて考えると、これはおそらく愚かな質問ですが、私は間違っていないことを確認したいと思います。

154
John Oxley

それは間違いなく愚かな質問ではなく、F#がコレクション全体に対して_yield!_と1つのアイテムに対してyieldでサポートしているものです。 (これは、末尾再帰に関して非常に便利です...)

残念ながら、C#ではサポートされていません。

ただし、それぞれ_IEnumerable<ErrorInfo>_を返す複数のメソッドがある場合は、_Enumerable.Concat_を使用してコードを単純化できます。

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

ただし、2つの実装には非常に重要な違いが1つあります。この実装は、返される反復子を1つずつしか使用しない場合でも、すべてのメソッドimmediatelyを呼び出します。既存のコードは、次のエラーについてasksになる前に、GetMoreErrors()のすべてをループするまで待機します。

通常、これは重要ではありませんが、いつ何が起こるかを理解する価値があります。

134
Jon Skeet

このようなすべてのエラーソースをセットアップできます(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のメソッドの実行も遅延します。

21
Adam Boddington

簡単な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>
15
John Gietzen

私はあなたの機能に何の問題も見ていません。あなたが望むことをしていると言います。

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;
    }
  }
}
8
Tim Jarvis

誰もIEnumerable<IEnumerable<T>>このコードに実行の遅延を保持させる。私は多くの理由で遅延実行のファンです。その理由の1つは、膨大な数の列挙可能オブジェクトでもメモリフットプリントが小さいことです。

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を呼び出しサイトに移動するだけです。

3
Frank Bryce

はい、一度にすべてのエラーを返すことができます。 List<T>またはReadOnlyCollection<T>を返すだけです。

IEnumerable<T>を返すことにより、何かのシーケンスを返します。表面的には、コレクションを返すのと同じように見えるかもしれませんが、いくつかの違いがありますので、注意してください。

コレクション

  • 呼び出し元は、コレクションが返されたときにコレクションとすべてのアイテムの両方が存在することを確認できます。呼び出しごとにコレクションを作成する必要がある場合、コレクションを返すことは非常に悪い考えです。
  • ほとんどのコレクションは、返されたときに変更できます。
  • コレクションのサイズは有限です。

シーケンス

  • 列挙することができます-それは私たちが確かに言うことができるほとんどすべてです。
  • 返されるシーケンス自体は変更できません。
  • 各要素は、シーケンスの実行の一部として作成できます(つまり、IEnumerable<T>を返すと遅延評価が可能になり、List<T>を返すことはできません)。
  • シーケンスは無限である可能性があるため、返される要素の数を決定するために呼び出し元に任せます。
3
Brian Rasmussen