イールドリターンを使用したIEnumerableおよびRecursion


307

を持っています IEnumerable<T>WebFormsページでコントロールを見つけるために使用しメソッドがあります。

メソッドは再帰的でありyield return、再帰呼び出しの値がreturnigであるときに、必要なタイプを返すときに問題が発生します。

私のコードは次のようになります:

    public static IEnumerable<Control> 
                               GetDeepControlsByType<T>(this Control control)
    {
        foreach(Control c in control.Controls)
        {
            if (c is T)
            {
                yield return c;
            }

            if(c.Controls.Count > 0)
            {
                yield return c.GetDeepControlsByType<T>();
            }
        }
    }

これは現在、「式の型を変換できません」エラーをスローします。ただし、このメソッドがtypeを返す場合IEnumerable<Object>、コードはビルドされますが、誤ったタイプが出力に返されます。

yield return再帰を使用しながら使用する方法はありますか?


1
stackoverflow.com/questions/1815497/…:スレッド「mrydengrens」へのリンク「本質的にIEnumerableではないコレクションの列挙?」彼のサンプルコードは、Linqを使用して再帰的な列挙でスタックを使用する方法を示すEric Lippertのブログ記事に基づいており、イテレータによる高額なメモリの使用を回避しています。私見はとても便利です!
BillW 2010年

ところで。if(c.Controls.Count > 0)-> if(c.Controls.Any())、特に降伏している場合:)
tymtam 2013

私は、このケースが譲歩から利益を得るとは思いません。完全を期すために、なしの実装を提供しましたyield。以下を参照してください:)そして、それもワンライナーです:)
tymtam

yield return再帰関数ではメモリ使用量が爆発的に増加するので、注意して回避する必要があります。参照stackoverflow.com/a/30300257/284795を
大佐はパニック

回答:


485

戻るメソッドの内部 IEnumerable<T>、ではなくyield returnを返す必要TがありますIEnumerable<T>

交換する

yield return c.GetDeepControlsByType<T>();

と:

foreach (var x in c.GetDeepControlsByType<T>())
{
  yield return x;
}

98

再帰呼び出しによって生成された各項目を生成する必要があります。

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    foreach(Control c in control.Controls)
    {
        if (c is T)
        {
            yield return c;
        }

        if(c.Controls.Count > 0)
        {
            foreach (Control control in c.GetDeepControlsByType<T>())
            {
                yield return control;
            }
        }
    }
}

この方法で再帰するとコストがかかることに注意してください。非常に深いコントロールツリーがある場合、パフォーマンスの問題を引き起こす可能性のあるイテレータを多数作成することになります。それを避けたい場合は、基本的にメソッド内で再帰を自分で実行して、作成されたイテレータ(ステートマシン)が1つだけであることを確認する必要があります。詳細と実装例については、この質問を参照してください。ただし、これにより明らかに複雑さが増します。


2
ジョンをc.Controls.Count > 0.Any()
譲ること

@Tymekは実際にはリンクされた回答で言及されています。

28

ジョン・スキートとパニック大佐が答えに記しているように、 yield returnいるように、ツリーが非常に深い場合、再帰的メソッドするとパフォーマンスの問題が発生する可能性があります。

以下は、ツリーのシーケンスの深さ優先トラバーサルを実行する一般的な非再帰的拡張メソッドです。

public static IEnumerable<TSource> RecursiveSelect<TSource>(
    this IEnumerable<TSource> source, Func<TSource, IEnumerable<TSource>> childSelector)
{
    var stack = new Stack<IEnumerator<TSource>>();
    var enumerator = source.GetEnumerator();

    try
    {
        while (true)
        {
            if (enumerator.MoveNext())
            {
                TSource element = enumerator.Current;
                yield return element;

                stack.Push(enumerator);
                enumerator = childSelector(element).GetEnumerator();
            }
            else if (stack.Count > 0)
            {
                enumerator.Dispose();
                enumerator = stack.Pop();
            }
            else
            {
                yield break;
            }
        }
    }
    finally
    {
        enumerator.Dispose();

        while (stack.Count > 0) // Clean up in case of an exception.
        {
            enumerator = stack.Pop();
            enumerator.Dispose();
        }
    }
}

Eric Lippertのソリューションとは異なり、RecursiveSelectは列挙子を直接操作するため、Reverse(シーケンス全体をメモリにバッファーする)を呼び出す必要はありません。

RecursiveSelectを使用すると、OPの元のメソッドを次のように簡単に書き換えることができます。

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
}

この(優れた)コードを機能させるには、 'OfTypeを使用してControlCollectionをIEnumerable形式に変換する必要がありました。Windowsフォームでは、ControlCollectionは列挙できません。returncontrol.Controls.OfType <Control>()。RecursiveSelect <Control>(c => c.Controls.OfType <Control>()).Where(c => c is T );
BillW

17

他の人はあなたに正しい答えを提供しましたが、あなたのケースが譲ることから利益を得るとは思いません。

これは、譲らずに同じことを達成するスニペットです。

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
   return control.Controls
                 .Where(c => c is T)
                 .Concat(control.Controls
                                .SelectMany(c =>c.GetDeepControlsByType<T>()));
}

2
LINQも使用しませんyieldか?;)
Philipp M

これは滑らかです。追加のforeachループにいつも悩まされてきました。これで、純粋な関数型プログラミングでこれを行うことができます!
jsuddsjr 2014年

1
私はこのソリューションを読みやすさの点で気に入っていますが、イテレータでの歩留まりの使用と同じパフォーマンスの問題に直面しています。@PhilippM:確認済みLINQの用途が得られることをreferencesource.microsoft.com/System.Core/R/...
ハーマン

優れたソリューションに賛成します。
トーマーW 2018

12

2番目の項目では、列挙子自体ではなく列挙子からアイテムを返す必要があります。yield return

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    foreach (Control c in control.Controls)
    {
        if (c is T)
        {
            yield return c;
        }

        if (c.Controls.Count > 0)
        {
            foreach (Control ctrl in c.GetDeepControlsByType<T>())
            {
                yield return ctrl;
            }
        }
    }
}

9

列挙型の各コントロールを返す必要があると思います。

    public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
    {
        foreach (Control c in control.Controls)
        {
            if (c is T)
            {
                yield return c;
            }

            if (c.Controls.Count > 0)
            {
                foreach (Control childControl in c.GetDeepControlsByType<T>())
                {
                    yield return childControl;
                }
            }
        }
    }

8

Seredynskiの構文は正しいですがyield return、再帰関数ではメモリの使用に障害となるため、注意して回避する必要があります。https://stackoverflow.com/a/3970171/284795を参照してくださいこれは、深度に応じて爆発的にスケーリングし(私のアプリでは、同様の関数がメモリの10%を使用していました)。

簡単な解決策は、1つのリストを使用して、それを再帰で渡すことですhttps://codereview.stackexchange.com/a/5651/754

/// <summary>
/// Append the descendents of tree to the given list.
/// </summary>
private void AppendDescendents(Tree tree, List<Tree> descendents)
{
    foreach (var child in tree.Children)
    {
        descendents.Add(child);
        AppendDescendents(child, descendents);
    }
}

または、スタックとwhileループを使用して再帰呼び出しを排除することもできます https://codereview.stackexchange.com/a/5661/754


0

そこには多くの良い答えがありますが、LINQメソッドを使用して同じことを達成することが可能であることも付け加えておきます。

たとえば、OPの元のコードは次のように書き直すことができます。

public static IEnumerable<Control> 
                           GetDeepControlsByType<T>(this Control control)
{
   return control.Controls.OfType<T>()
          .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
}

同じアプローチを使用したソリューションが3年前に投稿されました
サービー

@Servy似ています(ところで、この回答を書いている間、私がすべての回答の間で見落としました...)。これは、.OfType <>を使用してフィルタリングし、.Union()
yoel halb

2
これOfTypeは本当に意味のある違いではありません。せいぜいマイナーなスタイル変更。コントロールを複数のコントロールの子にすることはできないため、走査されたツリーはすでに unqiueです。のUnion代わりにを使用Concatすると、すでに一意であることが保証されているシーケンスの一意性が不必要に検証されるため、客観的なダウングレードになります。
サービー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.