IEnumerable e Recursion utilizzando il rendimento


307

Ho un IEnumerable<T>metodo che sto usando per trovare i controlli in una pagina WebForms.

Il metodo è ricorsivo e sto riscontrando dei problemi nel restituire il tipo desiderato quando yield returnrestituisce il valore della chiamata ricorsiva.

Il mio codice ha il seguente aspetto:

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

Questo attualmente genera un errore "Impossibile convertire il tipo di espressione". Se tuttavia questo metodo restituisce il tipo IEnumerable<Object>, il codice viene generato, ma nell'output viene restituito il tipo errato.

C'è un modo di usare yield returnmentre si utilizza anche la ricorsione?


1
stackoverflow.com/questions/1815497/… : collegamento alla risposta "mrydengrens" sul thread "Enumerare raccolte che non sono intrinsecamente IEnumerable?" Il suo codice di esempio si basa su un articolo del blog di Eric Lippert che mostra come utilizzare gli stack nell'enumerazione ricorsiva con Linq, evitando così l'uso di memoria probabilmente costoso da parte degli iteratori. imho molto utile!
Bill,

BTW. if(c.Controls.Count > 0)-> if(c.Controls.Any()), specialmente se anche tu stai cedendo :)
tymtam l'

Non credo che questo caso tragga vantaggio dalla resa. Per completezza ho fornito un'implementazione senza yield. Vedi di seguito :) Ed è anche un one-liner :)
tymtam l'

Si dovrebbe fare attenzione ad evitare yield returnnelle funzioni ricorsive, l'utilizzo della memoria si ridimensiona in modo esplosivo. Vedi stackoverflow.com/a/30300257/284795
Colonnello Panic,

Risposte:


485

All'interno di un metodo che ritorna IEnumerable<T>, yield returndeve tornare T, non un IEnumerable<T>.

Sostituire

yield return c.GetDeepControlsByType<T>();

con:

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

98

È necessario cedere ciascuno degli articoli prodotti dalla chiamata ricorsiva:

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

Tieni presente che il costo in questo modo è ricorrente: finirai per creare molti iteratori, che possono creare un problema di prestazioni se hai un albero di controllo molto profondo. Se vuoi evitarlo, devi fondamentalmente eseguire tu stesso la ricorsione all'interno del metodo, per assicurarti che sia stato creato un solo iteratore (macchina a stati). Vedi questa domanda per maggiori dettagli e un'implementazione di esempio, ma ovviamente ciò aggiunge anche una certa complessità.


2
Trovo sorprendente che in una discussione sulla resa di Jon non abbia menzionato c.Controls.Count > 0vs. .Any():)
tymtam l'

@Tymek in realtà è menzionato nella risposta collegata.

28

Come notano Jon Skeet e il colonnello Panic nelle loro risposte, l'uso yield returndi metodi ricorsivi può causare problemi di prestazioni se l'albero è molto profondo.

Ecco un metodo di estensione generico non ricorsivo che esegue un attraversamento in profondità di una sequenza di alberi:

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

A differenza della soluzione di Eric Lippert , RecursiveSelect funziona direttamente con gli enumeratori in modo da non dover chiamare Reverse (che bufferizza l'intera sequenza in memoria).

Usando RecursiveSelect, il metodo originale dell'OP può essere riscritto semplicemente in questo modo:

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

Per far funzionare questo (eccellente) codice, ho dovuto usare 'OfType per ottenere ControlCollection in formato IEnumerable; in Windows Form, un ControlCollection non è enumerabile: return control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Dove (c => c è T );
BillW,

17

Altri ti hanno fornito la risposta corretta, ma non credo che il tuo caso tragga beneficio dal cedere.

Ecco uno snippet che raggiunge lo stesso senza cedere.

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
Non usa anche LINQ yield? ;)
Philipp M,

Questo è liscio. Sono sempre stato infastidito dal foreachloop aggiuntivo . Ora posso farlo con la pura programmazione funzionale!
jsuddsjr,

1
Mi piace questa soluzione in termini di leggibilità, ma deve affrontare lo stesso problema di prestazioni con gli iteratori dell'utilizzo del rendimento. @PhilippM: verificato che LINQ utilizza i riferimenti di
Herman il

Pollice in su per un'ottima soluzione.
Tomer W,

12

È necessario restituire gli elementi dall'enumeratore, non dall'enumeratore stesso, nel secondoyield 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

Penso che devi cedere ogni singolo controllo negli enumerabili.

    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

La sintassi di Seredynski è corretta, ma dovresti stare attento a evitare yield returnle funzioni ricorsive perché è un disastro per l'utilizzo della memoria. Vedi https://stackoverflow.com/a/3970171/284795 ridimensiona in modo esplosivo con la profondità (una funzione simile utilizzava il 10% della memoria nella mia app).

Una soluzione semplice è utilizzare un elenco e passarlo con la ricorsione 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);
    }
}

In alternativa, è possibile utilizzare uno stack e un ciclo while per eliminare le chiamate ricorsive https://codereview.stackexchange.com/a/5661/754


0

Mentre ci sono molte buone risposte là fuori, aggiungerei ancora che è possibile utilizzare i metodi LINQ per ottenere lo stesso risultato.

Ad esempio, il codice originale dell'OP potrebbe essere riscritto come:

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

Una soluzione che utilizzava lo stesso approccio è stata pubblicata tre anni fa .
Servito il

@Servy Anche se è simile (cosa che ho perso tra tutte le risposte ... mentre scrivevo questa risposta), è ancora diverso, poiché utilizza .OfType <> per filtrare e .Union ()
yoel halb

2
Il OfTypenon è davvero un altro minaccioso. Al massimo un piccolo cambiamento styalistic. Un controllo non può essere figlio di più controlli, quindi l'albero attraversato non è ancora valido. Usare Unioninvece di Concatverificare inutilmente l'unicità di una sequenza già garantita come unica, ed è quindi un downgrade oggettivo.
Servito il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.