Численні та рекурсійні з використанням прибутку


307

У мене є IEnumerable<T>метод, який я використовую для пошуку елементів керування на сторінці WebForms.

Метод є рекурсивним, і у мене виникають проблеми із поверненням типу, який я хочу, коли 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)
            {
                yield return c.GetDeepControlsByType<T>();
            }
        }
    }

Наразі це призводить до помилки "Неможливо перетворити тип виразу". Якщо цей метод повертає тип IEnumerable<Object>, код будується, але неправильний тип повертається у висновку.

Чи існує спосіб використання, yield returnа також використання рекурсії?


1
stackoverflow.com/questions/1815497/… : посилання на відповідь "mrydengrens" у потоці "Перерахування колекцій, які за своєю суттю не мають чисельності?" Його зразок коду заснований на статті блогу Еріка Ліпперта, яка показує вам, як використовувати стеки в рекурсивному перерахуванні з Linq, таким чином уникаючи можливого дорогого використання пам'яті ітераторами. imho дуже корисно!
BillW

До речі. if(c.Controls.Count > 0)-> if(c.Controls.Any()), особливо якщо ви також поступаєтесь :)
tymtam

Я не думаю, що ця справа має переваги. Для повноти я забезпечив реалізацію без yield. Будь ласка, дивіться нижче :) І це
однолінійний

Ви повинні бути обережними, щоб уникнути yield returnрекурсивних функцій, коли використання пам'яті масштабує вибухонебезпечно. Дивіться stackoverflow.com/a/30300257/284795
Полковник Паніка

Відповіді:


485

Всередині методу, який повертається IEnumerable<T>, yield returnповинен повернутися T, а не an 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;
            }
        }
    }
}

Зауважте, що повторюваність таким чином коштує - ви в кінцевому підсумку створите безліч ітераторів, які можуть створити проблему продуктивності, якщо у вас справді глибоке дерево управління. Якщо ви хочете цього уникнути, вам в основному потрібно зробити рекурсію в рамках методу, щоб переконатися, що створений лише один ітератор (стан машини). Дивіться це запитання для отримання більш детальної інформації та реалізації вибірки - але це, очевидно, також додає певної складності.


2
Мені здається дивним, що в темі про поступливість Джон не згадав c.Controls.Count > 0проти .Any():)
tymtam

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

На відміну від рішення Еріка Ліпперта , RecursiveSelect працює безпосередньо з перечислювачами, так що йому не потрібно викликати Зворотний (який буферизує всю послідовність у пам'яті).

Використовуючи 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 Forms, ControlCollection не перелічується: return control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Where (c => c 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? ;)
Філіпп М

Це гладко. Мене завжди турбував додатковий foreachцикл. Тепер я можу це зробити за допомогою чисто функціонального програмування!
jsuddsjr

1
Мені подобається це рішення з точки зору читабельності, але воно стикається з тими ж проблемами продуктивності, що і ітератори, як використання урожайності. @PhilippM: Підтверджено , що LINQ використовує вихід referencesource.microsoft.com/System.Core/R / ...
Herman

Великий палець вгору для прекрасного рішення.
Tomer W

12

Потрібно повернути елементи у перелічувача, а не самого перелічувача, у другу секунду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

Синтаксис Серединського правильний, але ви повинні бути обережними, щоб уникнути yield returnрекурсивних функцій, оскільки це катастрофа для використання пам'яті. Дивіться https://stackoverflow.com/a/3970171/284795, вона масштабує вибухонебезпечно з глибиною (подібна функція використовувала 10% пам'яті в моєму додатку).

Просте рішення - скористатися одним списком і передати його за допомогою рекурсії 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);
    }
}

Крім того, ви можете використовувати цикл стека та часу, щоб усунути рекурсивні дзвінки https://codereview.stackexchange.com/a/5661/754


0

Хоча там є багато хороших відповідей, я все-таки додаю, що можна використовувати методи LINQ, щоб досягти того ж самого,.

Наприклад, оригінальний код ОП може бути переписаний як:

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

Рішення, що використовує той самий підхід, було опубліковане три роки тому .
Сервіс

@Servy Хоча вона схожа (яку BTW я пропустила між усіма відповідями ... під час написання цієї відповіді), вона все одно відрізняється, оскільки використовує .OfType <> для фільтрації, і .Union ()
yoel halb

2
OfTypeЦе на самому ділі не meainingful різні. Принаймні незначна стилістична зміна. Елемент керування не може бути дочіркою декількох елементів керування, тому пройдене дерево вже не має жодного значення. Використання Unionзамість того Concat, що непотрібно перевіряти унікальність послідовності, яка вже гарантовано є унікальною, і тому є об'єктивним пониженням.
Сервіс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.