イールドリターンを含むすべての列挙型を一度に返します。ループせずに


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を列挙せずにすべてのエラーを返すことは可能ですか?

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


イールドリターンの質問が増えるのを見て嬉しく思います(そして興味があります!)。ばかげた質問ではありません!
JoshJordan 2009

なにGetCardProductionValidationErrorsFor
Andrew Hare

4
戻り値GetMoreErrors(card);の何が問題か ?
サムサフラン

10
@Sam:「より多くの検証エラーでさらに収益が上がる」
Jon Skeet、

1
非あいまいでない言語の観点から見ると、1つの問題は、TとIEnumerable <T>の両方を実装するものがあるかどうかをメソッドが認識できないことです。したがって、yieldには別の構成要素が必要です。そうは言っても、これを行う方法があれば確かに良いでしょう。おそらく、fooがIEnumerable <T>を実装している場合、yieldはyield fooを返しますか?
William Jockusch

回答:


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());
}

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

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


3
Wes Dyerがこのパターンについて言及している興味深い記事があります。blogs.msdn.com/wesdyer/archive/2007/03/23/…–
JohannesH

1
通行人のマイナー修正-これはSystem.Linq.Enumeration.Concat <>(first、second)です。IEnumeration.Concat()ではありません。
redcalx 2010

@ the-locster:どういう意味かわかりません。列挙ではなく、間違いなく列挙可能です。コメントを明確にしていただけませんか?
Jon Skeet、2010

@ジョンスキート-メソッドをすぐに呼び出すとはどういう意味ですか?私はテストを実行しましたが、何かが実際に反復されるまで、メソッド呼び出しを完全に延期しているようです。ここにコード:pastebin.com/0kj5QtfD
Steven Oxley、

5
@スティーブン:いいえ。それはメソッドを呼び出しています-しかし、あなたの場合GetOtherErrors()(など)は結果を延期しています(イテレータブロックを使用して実装されているため)。それらを変更して新しい配列またはそのようなものを返すようにしてください。
Jon Skeet、2010

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 Kemp、

@イアン、これはC#でネストされた利回りを返す方法です。yield!F#のようなはありません。
John Gietzen

これは質問に対する回答ではありません
divyang4481

8

私はあなたの機能に何の問題もないと思います、私はそれがあなたが望むことをしていると言います。

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

4

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
}

同様に、ラッパー関数を使用DoGetErrorsUnWrapずに、呼び出しサイトに移動できます。


2
DoGetErrors(card).SelectMany(x => x)同じことをし、据え置きの振る舞いを維持するので、だれも拡張メソッドについて考えなかったでしょう。これはまさにアダムが彼の答えで示唆していることです。
huysentruitw

3

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

を返すことによって IEnumerable<T>何かのシーケンスが返されます。一見、コレクションを返すのと同じに見えるかもしれませんが、いくつかの違いがありますので、覚えておく必要があります。

コレクション

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

シーケンス

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

すべての要素のデータ構造を事前に割り当てているため、クライアントが本当にそれを列挙する必要がある場合、コレクションを返すと不当なオーバーヘッドが発生する可能性があります。また、シーケンスを返す別のメソッドに委任した場合、それをコレクションとしてキャプチャすると、追加のコピーが必要になり、アイテムの数(およびオーバーヘッド)がわからない可能性があります。したがって、コレクションが既に存在し、コピーせずに直接返すことができる(または読み取り専用としてラップされる)場合にのみ、コレクションを返すことをお勧めします。他のすべてのケースでは、シーケンスがより良い選択です
Pavel Minaev '08 / 08/13

私も同意します。コレクションを返すのは良い考えだと私が言ったという印象を受けた場合は、私の見落としを逃してください。コレクションを返すこととシーケンスを返すことには違いがあるという事実を強調しようとしていました。私はそれをより明確にするよう努めます。
ブライアンラスムッセン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.