2011-02-08 14 views
9

Fondamentalmente mi chiedo se sia corretto utilizzare successivamente un'enciclopedia più di una volta nel codice. Sia che si interrompa in anticipo o no, l'enumerazione dovrebbe sempre essere reimpostata in ogni caso foreach di seguito, quindi fornendoci un'enumerazione coerente dall'inizio alla fine della raccolta degli effetti?È corretto riutilizzare le raccolte IEnumerable più di una volta?

var effects = this.EffectsRecursive; 

foreach (Effect effect in effects) 
{ 
... 
} 

foreach (Effect effect in effects) 
{ 
    if(effect.Name = "Pixelate") 
     break; 
} 

foreach (Effect effect in effects) 
{ 
... 
} 

EDIT: L'attuazione del EffectsRecursive è questo:

public IEnumerable<Effect> Effects 
{ 
    get 
    { 
     for (int i = 0; i < this.IEffect.NumberOfChildren; ++i) 
     { 
      IEffect effect = this.IEffect.GetChildEffect (i); 
      if (effect != null) 
       yield return new Effect (effect); 
     } 
    } 
} 

public IEnumerable<Effect> EffectsRecursive 
{ 
    get 
    { 
     foreach (Effect effect in this.Effects) 
     { 
      yield return effect; 
      foreach (Effect seffect in effect.ChildrenRecursive) 
       yield return seffect; 
     } 
    } 
} 
+0

Btw ho aggiunto l'attuazione di EffectsRecursive. –

+1

Attenzione al rendimento ricorsivo. C'è un sacco di magia del compilatore in corso qui per creare macchine di stato. Se la struttura dei dati è ampia, potrebbe essere meno performante/più complessa di quanto ci si aspetti. – spender

+0

@spender: Grazie, c'è un modo alternativo che posso usare? –

risposta

14

Il codice che consuma la sequenza è soddisfacente. Come sottolinea lo spender, il codice che produce l'enumerazione potrebbe avere problemi di prestazioni se l'albero è profondo.

Supponiamo che nel punto più profondo il vostro albero fa quattro profonda; pensa a cosa succede sui nodi che sono quattro in profondità. Per ottenere quel nodo, si esegue l'iterazione della radice, che chiama un iteratore, che chiama un iteratore, che chiama un iteratore, che passa di nuovo il nodo al codice che restituisce il nodo al codice che passa il nodo indietro ... Invece di solo consegnando il nodo al chiamante, hai creato un piccolo gruppo di brigata con quattro ragazzi al suo interno e stanno calpestando i dati da un oggetto all'altro prima che raggiungano il loop che lo desiderava.

Se l'albero ha solo quattro profondità, probabilmente non è un grosso problema. Ma supponiamo che l'albero sia diecimila elementi e che abbia un migliaio di nodi che formano una lista collegata nella parte superiore e i rimanenti novemila nodi sul fondo. Ora, quando si iterano quei novemila nodi, ognuno deve passare attraverso mille iteratori, per un totale di nove milioni di copie per ottenere novemila nodi. (Ovviamente, probabilmente hai ottenuto un errore di overflow dello stack e hai bloccato anche il processo.)

Il modo di affrontare questo problema, se ce l'hai, è gestire lo stack tu stesso piuttosto che spingere nuovi iteratori nello stack .

public IEnumerable<Effect> EffectsNotRecursive() 
{  
    var stack = new Stack<Effect>(); 
    stack.Push(this); 
    while(stack.Count != 0) 
    { 
     var current = stack.Pop(); 
     yield return current; 
     foreach(var child in current.Effects) 
      stack.Push(child); 
    } 
} 

L'implementazione originale ha una complessità temporale O (nd) dove n è il numero di nodi e d è la profondità media dell'albero; poiché d può nel caso peggiore essere O (n), e nel migliore dei casi essere O (lg n), ciò significa che l'algoritmo è compreso tra O (n lg n) e O (n^2) nel tempo. È O (d) nello spazio heap (per tutti gli iteratori) e O (d) nello spazio stack (per tutte le chiamate ricorsive.)

La nuova implementazione ha una complessità temporale di O (n), ed è O (d) nello spazio heap e O (1) nello spazio stack.

Un lato negativo di questo è che l'ordine è diverso; l'albero è attraversato dall'alto verso il basso e da destra a sinistra nel nuovo algoritmo, anziché dall'alto verso il basso e da sinistra a destra. Se questo ti disturba, puoi semplicemente dire

 foreach(var child in current.Effects.Reverse()) 

invece.

Per ulteriori analisi di questo problema, vedere l'articolo del mio collega Wes Dyer sul tema:

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx

+0

@Eric: Grazie Eric, questa è una risposta sorprendente. Cambierò le mie enumerazioni ricorsive come questa. Ma alla fine nel tuo esempio, il tuo esempio, inizierà con il genitore più in alto dei 9000 elementi, quindi itererà attraverso quei 9000 elementi dall'alto verso il basso, e poi passerà all'altro più in alto i genitori che iniziano dopo il primo più alto genitore abbiamo iterato come il primo elemento, fino a quando finiamo con l'elemento più in alto a sinistra? È questo che intendi con "l'albero è attraversato da cima a fondo e da destra a sinistra"? –

+0

@Eric: Btw la riga in cui hai detto "stack.Push (this);" restituisce anche l'istanza corrente, giusto? Nella mia implementazione, voglio solo restituire i sotto-effetti dell'effetto corrente senza se stesso. Posso semplicemente rimuoverlo? Credo che ciò provocherebbe problemi nella linea stack.Pop(). Cosa ne pensi? –

+1

@Joan: Re la tua prima domanda: supponi che l'albero sia A con il figlio sinistro B e il bambino destro C. Questo algoritmo li restituisce nell'ordine A, C, B. Per prima cosa mettiamo A in pila. Quindi inseriamo A e mettiamo B e C in pila. Poi facciamo il pop C, poi pop B. B e C sono fatti da destra a sinistra invece che da sinistra a destra. Se si desidera A, B, C, invertire la lista dei bambini prima che vengano inseriti. Premi A, apri A, premi C, premi B, apri B, apri C, e ora abbiamo A, B, C. –

0

Tutto dipende dall'implementazione del tipo di EffectsRecursive.

6

legale, sì. Sia che funzionerà come ci si aspetta dipende da:

  • L'attuazione del IEnumerable restituito da EffectsRecursive e se restituisce sempre lo stesso insieme;

  • Se si desidera enumerare lo stesso insieme entrambe le volte

Se restituisce un IEnumerable che richiede un certo lavoro intenso, e non memorizza nella cache i risultati internamente, allora potrebbe essere necessario .ToList() esso te stesso. Se memorizza nella cache i risultati, allora ToList() sarebbe leggermente ridondante ma probabilmente non dannoso.

Inoltre, se GetEnumerator() è implementato in una vera e propria (*) modo tipica /, allora si può tranquillamente elencare qualsiasi numero di volte - ogni foreach sarà una nuova chiamata a GetEnumerator() che restituisce una nuova istanza del IEnumerator. Ma potrebbe essere che in alcune situazioni restituisce lo stesso IEnumerator istanza che è già stata parzialmente o completamente elencate, quindi tutto davvero solo dipende dallo specifico utilizzo previsto di quel particolare IEnumerable.

* Sono quasi sicuro che restituire lo stesso enumeratore più volte è in realtà una violazione del contratto implicito per il modello, ma ho visto alcune implementazioni che lo fanno comunque.

12

Sì questo è legale per farlo. Il modello IEnumerable<T> è pensato per supportare più enumerazioni su una singola fonte. Le raccolte che possono essere enumerate una sola volta devono esporre invece IEnumerator.

1

Molto probabilmente, sì. La maggior parte delle implementazioni di IEnumerable restituiscono un nuovo IEnumerator che inizia all'inizio dell'elenco.

Problemi correlati