2009-11-29 6 views
21

Quando si desidera elencare ricorsivamente un oggetto gerarchico, selezionando alcuni elementi sulla base di alcuni criteri, ci sono numerosi esempi di tecniche come "appiattimento" e quindi il filtraggio utilizzando Linq: come quelli trovati qui:Enumerazione di raccolte che non sono intrinsecamente IEnumerable?

link text

Tuttavia, quando si enumera qualcosa come la raccolta Controls di un Form o la raccolta Nodes di un TreeView, non sono stato in grado di utilizzare questi tipi di tecniche perché sembrano richiedere un argomento (al metodo di estensione) che è un IEnumerable collezione: il passaggio in SomeForm.Controls non viene compilato.

La cosa più utile che ho trovato è stato questo:

link text

che ti dà un metodo di estensione per Control.ControlCollection con un risultato IEnumerable è possibile utilizzare con LINQ.

Ho modificato l'esempio precedente per analizzare i nodi di un TreeView senza problemi.

public static IEnumerable<TreeNode> GetNodesRecursively(this TreeNodeCollection nodeCollection) 
{ 
    foreach (TreeNode theNode in nodeCollection) 
    { 
     yield return theNode; 

     if (theNode.Nodes.Count > 0) 
     { 
      foreach (TreeNode subNode in theNode.Nodes.GetNodesRecursively()) 
      { 
       yield return subNode; 
      } 
     } 
    } 
} 

Questo è il tipo di codice che sto scrivendo ora utilizzando il metodo di estensione:

var theNodes = treeView1.Nodes.GetNodesRecursively(); 

    var filteredNodes = 
    (
     from n in theNodes 
      where n.Text.Contains("1") 
       select n 
    ).ToList(); 

e penso che ci possa essere un più elegante modo per fare questo in cui il vincolo (s) sono passato.

Che cosa voglio sapere se è possibile definire genericamente tali procedure, in modo che: in fase di esecuzione posso passare il tipo di raccolta, così come la raccolta effettiva, ad un parametro generico, quindi il codice è indipendente dal fatto che si tratti di TreeNodeCollection o Controls.Collection .

Mi piacerebbe anche sapere se c'è altro modo (più economico? Fastser?) Di quello mostrato nel secondo link (sopra) per ottenere un TreeNodeCollection o Control.ControlCollection in una forma utilizzabile da Linq.

Un commento di Leppie su "SelectMany nel post SO collegato al primo (sopra) sembra un indizio.

I miei esperimenti con SelectMany sono stati: beh, chiamali "disastri". :)

Apprezzare qualsiasi suggerimento. Ho passato molte ore a leggere ogni post SO che ho potuto trovare toccato in queste aree, e vagare per la mia strada in una tale esotismo come "y-combinator". Un'esperienza "umiliante", potrei aggiungere :)

risposta

30

Questo codice dovrebbe fare il trucco

public static class Extensions 
{ 
    public static IEnumerable<T> GetRecursively<T>(this IEnumerable collection, 
     Func<T, IEnumerable> selector) 
    { 
     foreach (var item in collection.OfType<T>()) 
     { 
      yield return item; 

      IEnumerable<T> children = selector(item).GetRecursively(selector); 
      foreach (var child in children) 
      { 
       yield return child; 
      } 
     } 
    } 
} 

Ecco un esempio di come usarlo Aggiornamento

TreeView view = new TreeView(); 

// ... 

IEnumerable<TreeNode> nodes = view.Nodes. 
    .GetRecursively<TreeNode>(item => item.Nodes); 

: In risposta al post di Eric Lippert.

Ecco una versione molto migliorata utilizzando la tecnica discussa in All About Iterators.

public static class Extensions 
{ 
    public static IEnumerable<T> GetItems<T>(this IEnumerable collection, 
     Func<T, IEnumerable> selector) 
    { 
     Stack<IEnumerable<T>> stack = new Stack<IEnumerable<T>>(); 
     stack.Push(collection.OfType<T>()); 

     while (stack.Count > 0) 
     { 
      IEnumerable<T> items = stack.Pop(); 
      foreach (var item in items) 
      { 
       yield return item; 

       IEnumerable<T> children = selector(item).OfType<T>(); 
       stack.Push(children); 
      } 
     } 
    } 
} 

ho fatto un semplice test delle prestazioni utilizzando la seguente benchmarking technique. I risultati parlano da soli. La profondità dell'albero ha un impatto solo marginale sulle prestazioni della seconda soluzione; mentre le prestazioni diminuiscono rapidamente per la prima soluzione, portando infine a un StackOverflowException quando la profondità dell'albero diventa troppo grande.

benchmarking

+1

Nel "selettore (elemento) .OfType ()" l'OfType non è obbligatorio, come dovrebbe già restituire T. –

+0

No è ​​obbligatorio perché il selettore restituisce un IEnumerable e non e IEnumerable . – mrydengren

+1

Oh, vedo il problema ora. Ho aggiornato il codice per riflettere le modifiche. Buona pesca. – mrydengren

2

io non sono sicuro di TreeNodes, ma si può fare Controls di una forma IEnumerable utilizzando System.Linq e, per esempio

var ts = (from t in this.Controls.OfType<TextBox> 
       where t.Name.Contains("fish") 
       select t); 
//Will get all the textboxes whose Names contain "fish" 

Spiace dimmi che non so come renderlo ricorsivo, in cima alla mia testa.

+4

.OfType potrebbe essere più utile di.Trasmetti perché puoi specificare il tipo che vuoi; se la raccolta è eterogenea (come sarebbe una raccolta di controlli) è possibile raccogliere in modo sicuro uno specifico tipo di controllo con .OfType . .Cast potrebbe generare un'eccezione di trasmissione in caso di mancata corrispondenza. Inoltre, se si sa che una collezione è omogenea, allora. Asumerico() farà bene il trucco. –

+0

Grazie Cylon Cat. Aggiornerò il codice per riflettere che –

+0

@Matt Grazie per aver risposto! fyi: WinForms (solo VS Studio 2010 beta 2 e FrameWork 4.0 sono stati testati): i soli metodi di estensione esposti da ControlCollection di un modulo che inizia con "Come" sono "AsParallel" e "AsQueryable". TreeNodeCollection di TreeViews standard di WinForms espone anche gli stessi metodi di estensione. Se un altro controllo nativo di WinForms espone una collezione interna con un metodo di estensione Asumerabile non mi è noto. – BillW

1

Credo che si sta chiedendo qualcosa di simile.

public static IEnumerable<T> <T,TCollection> GetNodesRecursively(this TCollection nodeCollection, Func<T, TCollection> getSub) 
where T, TCollection: IEnumerable 
{ 
    foreach (var theNode in) 
    { 
     yield return theNode; 
     foreach (var subNode in GetNodesRecursively(theNode, getSub)) 
     { 
      yield return subNode; 
     } 
    } 
} 

var all_control = GetNodesRecursively(control, c=>c.Controls).ToList(); 
2

Sulla base della soluzione mrydengren:

public static IEnumerable<T> GetRecursively<T>(this IEnumerable collection, 
    Func<T, IEnumerable> selector, 
    Func<T, bool> predicate) 
{ 
    foreach (var item in collection.OfType<T>()) 
    { 
     if(!predicate(item)) continue; 

     yield return item; 

     IEnumerable<T> children = selector(item).GetRecursively(selector, predicate); 
     foreach (var child in children) 
     { 
      yield return child; 
     } 
    } 
} 


var theNodes = treeView1.Nodes.GetRecursively<TreeNode>(
    x => x.Nodes, 
    n => n.Text.Contains("1")).ToList(); 

Edit: per BillW

+0

Grazie per l'opportunità di studiare il tuo codice: per utilizzare il tuo codice ho dovuto modificare il caso d'uso di esempio mostrato in questo modo: var theNodes = winTV.Nodes.GetRecursively (x => x.Nodi, n = > n.Text.Contains ("1")). ToList(); grazie, – BillW

+1

Ho corretto l'errore. Ma consiglio vivamente di considerare l'implementazione basata su stack di mrydengren. Se necessario, posso modificare la mia risposta per includere il parametro del predicato. –

+0

@Yannick Il mio male: ho appena votato la tua risposta sopra, dimenticando che avevo già votato, e ha semplicemente cancellato il mio voto e ora non mi permetterò di resettarlo se non modifichi l'originale. Ho già scritto al team SO su questo aspetto: sembrerebbe banale avere una finestra di dialogo che ti chiede di confermare che stavi cambiando un voto già cast: ma forse è solo la mia incapacità di vedere "orange" :) I am intensamente testando la risposta di Mrydengren, che ho accettato, ed è "gentile" da parte tua parlarne. – BillW

21

ti sembra di essere sulla strada giusta e le risposte di cui sopra hanno alcune buone idee. Ma noto che tutte queste soluzioni ricorsive hanno alcuni difetti profondi.

Supponiamo che l'albero in questione abbia un totale di n nodi con una profondità massima di albero d < = n.

Prima di tutto, consumano spazio di stack di sistema nella profondità dell'albero. Se la struttura ad albero è molto profonda, questo può far saltare la pila e far crashare il programma. La profondità dell'albero d è O (lg n), a seconda del fattore di ramificazione dell'albero. Il caso peggiore non è affatto una ramificazione - solo una lista collegata - nel qual caso un albero con solo poche centinaia di nodi farà esplodere la pila.

In secondo luogo, quello che stai facendo qui è la costruzione di un iteratore che chiama un iteratore che chiama un iteratore ... in modo che ogni MoveNext() sull'iteratore in alto faccia effettivamente una catena di chiamate che è nuovamente O (d) nel costo. Se lo fai su ogni nodo, il costo totale nelle chiamate è O (nd), che è il caso peggiore O (n^2) e il caso migliore O (n lg n). Puoi fare meglio di entrambi; non c'è motivo per cui questo non possa essere lineare nel tempo.

Il trucco è smettere di usare lo stack di sistema piccolo e fragile per tenere traccia di cosa fare dopo e iniziare a utilizzare uno stack allocato all'heap per tenere traccia in modo esplicito.

si dovrebbe aggiungere alla tua lista di lettura l'articolo di Wes Dyer su questo:

https://blogs.msdn.microsoft.com/wesdyer/2007/03/23/all-about-iterators/

egli dà alcune buone tecniche alla fine per la scrittura di iteratori ricorsive.

+1

Grazie per l'ottimo post, ho aggiornato la mia soluzione per riflettere la lezione appresa qui. – mrydengren

+0

Grazie, Eric! Faccio fatica ad ottenere un "modello mentale" di come Linq renda la struttura interna e i suoi "costi": con "ricorsione classica": almeno mi sento "radicato"."Ho andare più e più eccellente libro di Skeet, ho il libro di Albahari (IMHO una serie di note avanzati con piccola spiegazione) quando ho trovato:. IEnumerable nodes1 = treeView1.Nodes.OfType (); e IEnumerable nodes2 = treeView1.Nodes.Cast () .Seleziona (nodo => nodo), entrambi restituiscono i nodi di livello superiore di un TreeView: lo "scienziato" in me vuole sapere cosa c'è "sotto il cofano": e, quale tecnica usare "quando." – BillW