2011-11-08 21 views
6

Ho una classe chiamata GenericPermutations che è enumerabile e un enumeratore. Il suo compito è quello di prendere una lista ordinata di oggetti e iterare attraverso ogni permutazione di loro in ordine.La classe C# è IEnumerable AND an IEnumerator allo stesso tempo. Quali sono i problemi con questo?

esempio, un implemenation intero di questa classe potrebbe scorrere la seguente:

GenericPermutations<int> p = new GenericPermutations<int>({ 1, 2, 3 }); 
p.nextPermutation(); // 123 
p.nextPermutation(); // 132 
p.nextPermutation(); // 213 
// etc. 

Così la sua enumerabile, nel senso che esso contiene una 'lista' di cose che si possono enumerare sopra. È anche un enumeratore, perché il suo compito consiste nel trovare la prossima permutazione.

il problema: Attualmente sto cercando di integrare IEnumerator e IEnumerable con questa classe, e mi sembra come dovrebbe essere sia (piuttosto che utilizzare una classe secondaria come IEnumerable). Finora ho evitato il problema con il tentativo di ottenere due enumeratori da esso passando un nuovo oggetto GenericPermutation nel metodo GetEnumerator.

Questa è una cattiva idea? Qualcos'altro che dovrei considerare?

+0

Se l'istanza enumerabile è anche l'unica istanza per tutti gli enumeratori richiesti, come si manterrà lo stato tra di essi? –

+0

La mia idea era di creare una nuova copia del mio oggetto ogni volta che viene richiesto un enumeratore e di passarla come enumeratore. Quindi, se viene richiesto un nuovo enumeratore, viene passata una copia dell'enumeratore corrente nel suo stato originale. – jtfairbank

risposta

8

Riduci la confusione (?) Utilizzando le versioni generiche di IEnumerable e IEnumerator.

Una permutazione enumerabile è IEnumerable<IEnumerable<T>>. Così si potrebbe avere qualcosa di simile

IEnumerable<IEnumerable<T>> GetPermutations(IEnumerable<T> sequence) 
{ 
    return new Permuter<T>(sequence); 
} 

e

public class Permuter<T> : IEnumerable<IEnumerable<T>> { ... } 

Inoltre, ho visto più di un caso in cui un singolo tipo implementato sia IEnumerable<T> e IEnumerator<T>; il suo metodo GetEnumerator era semplicemente return this;.

Penso che un tale tipo dovrebbe essere una struttura, però, perché se fosse una classe avresti tutti i tipi di problemi se hai chiamato GetEnumerator() una seconda volta prima che la prima enumerazione fosse completata.

EDIT: consumo del permutatore

var permuter = GetPermutations(sequence); 
foreach (var permutation in permuter) 
{ 
    foreach (var item in permutation) 
     Console.Write(item + "; "); 
    Console.WriteLine(); 
} 

Supponendo che la sequenza di ingresso è {1, 2, 3}, l'uscita è

1; 2; 3; 
1; 3; 2; 
2; 1; 3; 
2; 3; 1; 
3; 1; 2; 
3; 2; 1; 

EDIT:

Ecco un super-inefficiente implementazione per illustrare il suggerimento:

public class Permuter<T> : IEnumerable<IEnumerable<T>> 
{ 
    private readonly IEnumerable<T> _sequence; 

    public Permuter(IEnumerable<T> sequence) 
    { 
     _sequence = sequence; 
    } 

    public IEnumerator<IEnumerable<T>> GetEnumerator() 
    { 
     foreach(var item in _sequence) 
     { 
      var remaining = _sequence.Except(Enumerable.Repeat(item, 1)); 
      foreach (var permutation in new Permuter<T>(remaining)) 
       yield return Enumerable.Repeat(item, 1).Concat(permutation); 
     } 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 
} 
+0

Non mi piace solo passare 'this' nel metodo GetEnumerator perché possono esserci problemi in nested for-each loop e altre situazioni. Il tuo primo suggerimento sembra interessante, potresti espanderlo un po 'di più? Sono confuso dal 'IEnumerable >'. – jtfairbank

+1

Di cosa sei confuso? Le permutazioni della sequenza {1, 2, 3} comprendono sei sequenze ({1, 2, 3}, {1, 3, 2}, ecc.); questo può essere pensato come una sequenza di sequenze. Aggiungerò un esempio di codice. – phoog

+0

Ah capisco cosa intendi. Tuttavia non dovrei mai enumerare oltre le sequenze stesse, solo le permutazioni che le generano. – jtfairbank

0

È possibile che un oggetto si comporti sia come IEnumerator<T> sia come IEnumerable<T>, ma è generalmente difficile per un oggetto farlo in modo tale da evitare una semantica stravagante; a meno che lo IEnumerator<T> non sia stateless (ad esempio un enumeratore vuoto, dove MoveNext() restituisce sempre false o un enumeratore a ripetizione infinita, dove MoveNext() non restituisce sempre lo stesso valore e Current restituisce sempre lo stesso valore), ogni chiamata a GetEnumerator() deve restituisce un'istanza di oggetto distinta e è probabile che ci sia poco valore nell'implementare l'istanza IEnumerable<T>.

Avere un tipo di valore implementare IEnumerable<T> e IEnumerator<T>, e avente il suo metodo GetEnumerator() ritorno this, soddisfi il requisito che ogni chiamata di ritorno GetEnumerator un'istanza oggetto distinto, ma tipi di valore aventi implementano interfacce mutabili è generalmente pericoloso. Se un valore è inserito in IEnuerator<T> e mai escluso, si comporterà come un oggetto di tipo classe, ma non c'è una vera ragione per cui non dovrebbe essere stato semplicemente un oggetto di tipo classe.

Gli iteratori in C# sono implementati come oggetti di classe che implementano sia IEnumerable<T> e IEnumerator<T>, ma includono un bel po 'di logica di fantasia per garantire la correttezza semantica. L'effetto netto è che avere un oggetto che implementa entrambe le interfacce offre un leggero miglioramento delle prestazioni, in cambio di un bel po 'di complessità nel codice generato, e qualche stranezza semantica nel loro comportamento IDisposable. Non consiglierei questo approccio in alcun codice che debba essere leggibile; poiché gli aspetti IEnumerator<T> e IEnumerable<T> della classe utilizzano principalmente campi diversi e poiché una classe combinata deve avere un campo "thread-id" che non sarebbe necessario se si utilizzano classi separate, il miglioramento delle prestazioni si può ottenere utilizzando lo stesso l'oggetto da implementare per entrambe le interfacce è limitato. Vale la pena, forse, se aggiungere la complessità al compilatore fornirà quel leggero miglioramento delle prestazioni a milioni di routine di iteratore, ma non vale la pena di fare per migliorare le prestazioni di una routine.