2009-12-16 6 views
9

Me gustaría crear una copia de IEnumerator<T> para que pueda reiniciar el proceso de enumeración desde una ubicación particular en la colección. Claramente, no hay beneficio en hacerlo para colecciones que implementan IList, ya que podemos recordar el índice de interés.¿Es posible clonar una instancia de IEnumerable <T>, guardando una copia del estado de iteración?

¿Existe alguna forma inteligente de realizar esta tarea utilizando una combinación de instrucciones yield y funciones de Linq? No pude encontrar un método Clone() adecuado para copiar el enumerador, y me gustaría evitar el uso de Enumerable.Skip() para reposicionar un nuevo enumerador en el punto de reanudación deseado.

Además, me gustaría mantener las soluciones lo más genéricas posible, y no tener que depender del estado de ninguna colección concreta.

+0

¿Puede dar más detalles sobre lo que quiere lograr con ¿la implementación? – GrayWizardx

+0

Esta es solo una idea que encontré el otro día. Si esto fuera posible, permitiría que un enumerador se utilizara como un puntero (que solo se puede incrementar), y podría permitir implementaciones inteligentes de búsqueda binaria en colecciones no indexables. –

+0

Hace tiempo que lo pienso y me doy cuenta de que, sin acceso a los elementos de la colección, o una forma de indexarlos, no hay forma de hacerlo, a menos que sea aceptable volver a explorar la colección. Usar la serialización binaria para clonar el iterador es una opción, pero eso abre otra lata de gusanos todos juntos. –

Respuesta

4

Lo mejor que puede hacer es escribir algo que guarde un búfer (quizás un Queue<T>) de los datos consumidos de uno y no del otro (lo cual sería complicado/costoso si avanzara un iterador por posiciones de 1M, pero dejó el otro solo). I realmente cree que sería mejor que reconsidere el diseño, y simplemente usando GetEnumerator() (es decir, otro foreach) para comenzar de nuevo - o guarde los datos (si es corto) en una lista/matriz/lo que sea.

Nada elegante construido en


Actualización: tal vez un diseño alternativo interesante aquí es "PushLINQ";. en lugar de clonar el iterador, permite múltiples "cosas" para consumir el mismo feed de datos al mismo tiempo.

En este ejemplo (levantado de la página de Jon) calculamos múltiples agregados en paralelo:

// Create the data source to watch 
DataProducer<Voter> voters = new DataProducer<Voter>(); 

// Add the aggregators 
IFuture<int> total = voters.Count(); 
IFuture<int> adults = voters.Count(voter => voter.Age >= 18); 
IFuture<int> children = voters.Where(voter => voter.Age < 18).Count(); 
IFuture<int> youngest = voters.Min(voter => voter.Age); 
IFuture<int> oldest = voters.Select(voter => voter.Age).Max(); 

// Push all the data through 
voters.ProduceAndEnd(Voter.AllVoters()); 

// Write out the results 
Console.WriteLine("Total voters: {0}", total.Value); 
Console.WriteLine("Adult voters: {0}", adults.Value); 
Console.WriteLine("Child voters: {0}", children.Value); 
Console.WriteLine("Youngest vote age: {0}", youngest.Value); 
Console.WriteLine("Oldest voter age: {0}", oldest.Value); 
+0

@ Marc ... ¿alguna vez duermes? –

+2

No, él cubre la cuenta de Jon Skeet cuando está dormido. Jon skeet no duerme, entonces él (Skeet) cubre la cuenta de Marc cuando se supone que está dormido. ;) – RCIX

+6

pah! ¡Dormir es para los débiles! ¿Seguirá sugiriendo un descanso para el almuerzo? ¡Locura, te digo, locura! –

0

Así que lo que realmente desea es ser capaz de reanudar una iteración más tarde, estoy en lo correcto? Y clonar el enumerador o la colección es ¿cómo crees que harías tal cosa?

Puede crear una clase que envuelva un IEnumerable y exponer un enumerador personalizado que, internamente, clone el IEnumerable interno y luego lo enumere. Entonces, usar GetEnumerator() le daría un enumerador que podría pasarse.

Esto crearía una copia extra de IEnumerable para cada Enumerator "en vuelo", pero creo que satisfaría sus necesidades.

+0

Me gustaría reanudar la iteración más tarde, después de que el enumerador original haya completado su curso. La clonación de la colección en un 'IList ' facilita la solución, ya que puedo escribir mi propia clase de iterador que rastrea los índices, pero me gustaría evitar esto si es posible. ¿Podría dar un ejemplo corto de código pseudo para su solución? No tengo claro cómo pretendes clonar la instancia interna de 'IEnumerable'. –

+1

Ah, entonces quieres hacer una iteración desde un punto, y luego reanudar esa iteración en algún punto anterior. Entonces crearía una nueva colección, ya que cualquier solución a este problema se reducirá a eso de todos modos. – kyoryu

1

¿Desea poder guardar el estado, continuar la enumeración, luego regresar al estado guardado, o desea simplemente poder enumerar, hacer otras cosas y continuar la enumeración?

Si es esto último, algo como lo siguiente podría funcionar:

public class SaveableEnumerable<T> : IEnumerable<T>, IDisposable 
{ 
    public class SaveableEnumerator : IEnumerator<T> 
    { 
     private IEnumerator<T> enumerator; 

     internal SaveableEnumerator(IEnumerator<T> enumerator) 
     { 
      this.enumerator = enumerator; 
     } 

     public void Dispose() { } 

     internal void ActuallyDispose() 
     { 
      enumerator.Dispose(); 
     } 

     public bool MoveNext() 
     { 
      return enumerator.MoveNext(); 
     } 

     public void Reset() 
     { 
      enumerator.Reset(); 
     } 

     public T Current 
     { 
      get { return enumerator.Current; } 
     } 

     object IEnumerator.Current 
     { 
      get { return enumerator.Current; } 
     } 
    } 

    private SaveableEnumerator enumerator; 

    public SaveableEnumerable(IEnumerable<T> enumerable) 
    { 
     this.enumerator = new SaveableEnumerator(enumerable.GetEnumerator()); 
    } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return enumerator; 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return enumerator; 
    } 

    public void Dispose() 
    { 
     enumerator.ActuallyDispose(); 
    } 
} 

Ahora usted puede hacer:

using (IEnumerable<int> counter = new SaveableEnumerable<int>(CountableEnumerable())) 
{ 
    foreach (int i in counter) 
    { 
     Console.WriteLine(i); 
     if (i > 10) 
     { 
      break; 
     } 
    } 
    DoSomeStuff(); 
    foreach (int i in counter) 
    { 
     Console.WriteLine(i); 
     if (i > 20) 
     { 
      break; 
     } 
    } 
} 
+0

Gracias por la idea, pero estoy buscando una solución para el primero de sus ejemplos. –

2

Esto es completamente no una respuesta, pero el experimento mental que encontró interesante...si tienes un IEnumerable basado en el rendimiento, supongo que sabes que todo es magia generada por el compilador. Si usted tiene una bestia, que podría hacer algo como esto ...;)

class Program 
{ 
    static void Main(string[] args) 
    { 
     var bar = new Program().Foo(); 

     // Get a hook to the underlying compiler generated class 
     var barType = bar.GetType().UnderlyingSystemType; 
     var barCtor = barType.GetConstructor(new Type[] {typeof (Int32)}); 
     var res = barCtor.Invoke(new object[] {-2}) as IEnumerable<int>; 

     // Get our enumerator 
     var resEnum = res.GetEnumerator(); 
     resEnum.MoveNext(); 
     resEnum.MoveNext(); 
     Debug.Assert(resEnum.Current == 1); 

     // Extract and save our state 
     var nonPublicMap = new Dictionary<FieldInfo, object>(); 
     var publicMap = new Dictionary<FieldInfo, object>(); 
     var nonpublicfields = resEnum.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); 
     var publicfields = resEnum.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); 
     foreach(var field in nonpublicfields) 
     { 
      var value = field.GetValue(resEnum); 
      nonPublicMap[field] = value; 
     } 
     foreach (var field in publicfields) 
     { 
      var value = field.GetValue(resEnum); 
      publicMap[field] = value;     
     } 

     // Move about 
     resEnum.MoveNext(); 
     resEnum.MoveNext(); 
     resEnum.MoveNext(); 
     resEnum.MoveNext(); 
     Debug.Assert(resEnum.Current == 5); 

     // Restore state    
     foreach (var kvp in nonPublicMap) 
     { 
      kvp.Key.SetValue(resEnum, kvp.Value); 
     } 
     foreach (var kvp in publicMap) 
     { 
      kvp.Key.SetValue(resEnum, kvp.Value);     
     } 

     // Move about 
     resEnum.MoveNext(); 
     resEnum.MoveNext(); 
     Debug.Assert(resEnum.Current == 3); 
    } 

    public IEnumerable<int> Foo() 
    { 
     for (int i = 0; i < 10; i++) 
     { 
      yield return i; 
     } 
     yield break; 
    } 

} 
+0

Esto es bastante inteligente; No consideré usar la reflexión para clonar el objeto. Sin embargo, la clave para este funcionamiento parece ser garantizar que el código de reflexión para "guardar" el estado, haga una copia de los valores de campo, en lugar de mantener una referencia. –

+0

Sí, las "variables de estado" dentro de la clase de enumeración generada son las cosas que uno necesitaría marcar para hacer un movimiento de continuación como este; por supuesto, dado que todos los bits y bobs de estado y valor tienen un nombre desquiciado, la única manera "segura" (y uso el término vagamente) es copiar todos los campos de estado internos. Para ser precisos, sin embargo, no estaría copiando toda la enumeración aquí, probablemente 3-4 campos en promedio. – JerKimball

+0

(por supuesto, uno podría tratar de implementar una mónada de continuación adecuada en C#, pero eso es todo un animal); – JerKimball

1

JerKimball tenía un enfoque interesante. Intento llevarlo al siguiente nivel. Esto usa la reflexión para crear una nueva instancia y luego establece los valores en la nueva instancia. También encontré que este capítulo de C# en profundidad es muy útil. Iterator block implementation details: auto-generated state machines

static void Main() 
{ 
    var counter = new CountingClass(); 
    var firstIterator = counter.CountingEnumerator(); 
    Console.WriteLine("First list"); 
    firstIterator.MoveNext(); 
    Console.WriteLine(firstIterator.Current); 

    Console.WriteLine("First list cloned"); 
    var secondIterator = EnumeratorCloner.Clone(firstIterator); 

    Console.WriteLine("Second list"); 
    secondIterator.MoveNext(); 
    Console.WriteLine(secondIterator.Current); 
    secondIterator.MoveNext(); 
    Console.WriteLine(secondIterator.Current); 
    secondIterator.MoveNext(); 
    Console.WriteLine(secondIterator.Current); 

    Console.WriteLine("First list"); 
    firstIterator.MoveNext(); 
    Console.WriteLine(firstIterator.Current); 
    firstIterator.MoveNext(); 
    Console.WriteLine(firstIterator.Current); 
} 

public class CountingClass 
{ 
    public IEnumerator<int> CountingEnumerator() 
    { 
     int i = 1; 
     while (true) 
     { 
      yield return i; 
      i++; 
     } 
    } 
} 

public static class EnumeratorCloner 
{ 
    public static T Clone<T>(T source) where T : class, IEnumerator 
    { 
     var sourceType = source.GetType().UnderlyingSystemType; 
     var sourceTypeConstructor = sourceType.GetConstructor(new Type[] { typeof(Int32) }); 
     var newInstance = sourceTypeConstructor.Invoke(new object[] { -2 }) as T; 

     var nonPublicFields = source.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); 
     var publicFields = source.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); 
     foreach (var field in nonPublicFields) 
     { 
      var value = field.GetValue(source); 
      field.SetValue(newInstance, value); 
     } 
     foreach (var field in publicFields) 
     { 
      var value = field.GetValue(source); 
      field.SetValue(newInstance, value); 
     } 
     return newInstance; 
    } 
} 
3

No hay manera general, para hacer esto, ya que un IEnumerable puede depender de aspectos arbitrarios de estado del sistema que no pueden ser detectados a través de la reflexión o cualquier otro medio. Por ejemplo, una clase PaperTapeReader podría implementar un enumerador que lea caracteres de la cinta hasta que el sensor indique que no hay más cinta en la máquina. El estado de dicho enumerador sería la ubicación física de la cinta, que podría ser imposible restaurar mediante programación.

Dado un iEnumerable, sería posible producir dos o más iEnumerables, cada uno de los cuales actuaría como el original o el clon del mismo. Las solicitudes de MoveNext para la que estaba 'más avanzada' leerían datos nuevos del iEnumerable original y lo almacenarían en búfer para los demás. Sin embargo, a menos que el iEnumerable original sea compatible con esa funcionalidad de 'gancho', no creo que haya ninguna manera de engancharnos a sus datos.

+0

Veo lo que hiciste allí – pomeroy

Cuestiones relacionadas