2010-07-20 16 views
16

¿Es posible serializar un método que contenga yield sentencias (o una clase que contenga dicho método) para que cuando se rehidrate la clase, se conserve el estado interno del iterador generado?Serialización y la instrucción Yield

+1

que desea iniciar la ejecución del método en un solo lugar, y continuar funcionando en un lugar diferente? – arootbeer

+1

@arootbeer: también podría ser una operación de "guardar progreso", no tiene que enviarse necesariamente por el cable. –

+0

@Scott Stafford: Eso tiene más sentido. – arootbeer

Respuesta

8

Sí, puedes hacer esto. Con advertencias.

Un ejemplo de la serialización de un método con un yield, deserializar y continuando se puede encontrar aquí: http://www.agilekiwi.com/dotnet/CountingDemo.cs (Web Archive Link).

En general, intentar serializar sin hacer un trabajo extra fallará. Esto se debe a que las clases generadas por el compilador no están marcadas con el atributo Serializable. Sin embargo, puedes evitar esto.

Me gustaría señalar que la razón por la que no están marcados con serializable es porque son un detalle de implementación y están sujetos a cambios de última hora, por lo que es posible que no pueda deserializarlos en una versión más nueva.

Relacionado con una pregunta que hice en how to serialize anonymous delegates, que también debería funcionar para este caso.

Aquí está el código fuente de la "piratear":

// Copyright © 2007 John M Rusk (http://www.agilekiwi.com) 
// 
// You may use this source code in any manner you wish, subject to 
// the following conditions: 
// 
// (a) The above copyright notice and this permission notice shall be 
//  included in all copies or substantial portions of the Software. 
// 
// (b) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
//  OTHER DEALINGS IN THE SOFTWARE. 

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.IO; 
using System.Reflection; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters.Soap; 

namespace AgileKiwi.PersistentIterator.Demo 
{ 
    /// <summary> 
    /// This is the class we will enumerate over 
    /// </summary> 
    [Serializable] 
    public class SimpleEnumerable 
    { 
     public IEnumerator<string> Foo() 
     { 
      yield return "One"; 
      yield return "Two"; 
      yield return "Three"; 
     } 

     #region Here is a more advanced example 
     // This shows that the solution even works for iterators which call other iterators 
     // See SimpleFoo below for a simpler example 
     public IEnumerator<string> AdvancedFoo() 
     { 
      yield return "One"; 
      foreach (string s in Letters()) 
       yield return "Two " + s; 
      yield return "Three"; 
     } 

     private IEnumerable<string> Letters() 
     { 
      yield return "a"; 
      yield return "b"; 
      yield return "c"; 
     } 
     #endregion 
    } 

    /// <summary> 
    /// This is the command-line program which calls the iterator and serializes the state 
    /// </summary> 
    public class Program 
    { 
     public static void Main() 
     { 
      // Create/restore the iterator 
      IEnumerator<string> e; 
      if (File.Exists(StateFile)) 
       e = LoadIterator(); 
      else 
       e = (new SimpleEnumerable()).Foo(); // start new iterator 

      // Move to next item and display it. 
      // We can't use foreach here, because we only want to get ONE 
      // result at a time. 
      if (e.MoveNext()) 
       Console.WriteLine(e.Current); 
      else 
       Console.WriteLine("Finished. Delete the state.xml file to restart"); 

      // Save the iterator state back to the file 
      SaveIterator(e); 

      // Pause if running from the IDE 
      if (Debugger.IsAttached) 
      { 
       Console.Write("Press any key..."); 
       Console.ReadKey(); 
      } 
     } 

     static string StateFile 
     { 
      get { 
       return Path.Combine(
        Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), 
        "State.xml"); 
      } 
     } 

     static IEnumerator<string> LoadIterator() 
     { 
      using (FileStream stream = new FileStream(StateFile, FileMode.Open)) 
      { 
       ISurrogateSelector selector = new EnumerationSurrogateSelector(); 
       IFormatter f = new SoapFormatter(selector, new StreamingContext()); 
       return (IEnumerator<string>)f.Deserialize(stream); 
      } 
     } 

     static void SaveIterator(IEnumerator<string> e) 
     { 
      using (FileStream stream = new FileStream(StateFile, FileMode.Create)) 
      { 
       ISurrogateSelector selector = new EnumerationSurrogateSelector(); 
       IFormatter f = new SoapFormatter(selector, new StreamingContext()); 
       f.Serialize(stream, e); 
      } 
      #region Note: The above code puts the name of the compiler-generated enumerator class... 
      // into the serialized output. Under what circumstances, if any, might a recompile result in 
      // a different class name? I have not yet investigated what the answer might be. 
      // I suspect MS provide no guarantees in that regard. 
      #endregion 
     } 
    } 

    #region Helper classes to serialize iterator state 
    // See http://msdn.microsoft.com/msdnmag/issues/02/09/net/#S3 
    class EnumerationSurrogateSelector : ISurrogateSelector 
    { 
     ISurrogateSelector _next; 

     public void ChainSelector(ISurrogateSelector selector) 
     { 
      _next = selector; 
     } 

     public ISurrogateSelector GetNextSelector() 
     { 
      return _next; 
     } 

     public ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector) 
     { 
      if (typeof(System.Collections.IEnumerator).IsAssignableFrom(type)) 
      { 
       selector = this; 
       return new EnumeratorSerializationSurrogate(); 
      } 
      else 
      { 
       //todo: check this section 
       if (_next == null) 
       { 
        selector = null; 
        return null; 
       } 
       else 
       { 
        return _next.GetSurrogate(type, context, out selector); 
       } 
      } 
     } 
    } 

    // see http://msdn.microsoft.com/msdnmag/issues/02/09/net/#S3 
    class EnumeratorSerializationSurrogate : ISerializationSurrogate 
    { 
     public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
     { 
      foreach(FieldInfo f in obj.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 
       info.AddValue(f.Name, f.GetValue(obj)); 
     } 

     public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, 
            ISurrogateSelector selector) 
     { 
      foreach (FieldInfo f in obj.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 
       f.SetValue(obj, info.GetValue(f.Name, f.FieldType)); 
      return obj; 
     } 
    } 
    #endregion 
} 
+0

+1 muy interesante! – Abel

+0

desafortunadamente, el enlace ahora está roto –

+0

@modosansreves Afortunadamente fue respaldado en el archivo web. –

5

Internamente, la instrucción yield se transforma en máquina de estado implementada como clase que implementa la interfaz IEnumerator. Permite iterar a través de los resultados usando múltiples declaraciones foreach al mismo tiempo. Esa clase no es visible para su código, no está marcada como serializable.

Entonces, la respuesta es no, no es posible. Sin embargo, puede implementar el enumerador deseado por sí mismo, pero requiere más trabajo que yield.

+4

Yo diría que esto no es del todo correcto. Es posible, pero quizás fuertemente no recomendado. –

1

Solo asegúrese de que justo antes de llamar al rendimiento, guarde el estado (es decir, la posición de los iteradores) en un campo serializable (el campo de ubicación, o como se llame). Luego, cuando la clase se deserialice, simplemente continúa donde lo dejaste, usando el campo de ubicación.

Pero, cuando será esto útil? ¿Planeas serializar objetos en el medio de un ciclo foreach? Tal vez lo haga mucho más fácil si le da a su clase un método SetIteratorPosition(), que por defecto es la posición actual. Es más claro que agregar efectos secundarios al comportamiento bien definido existente (rendimiento) y todos entenderán que se puede guardar IteratorPosition.

Nota: los métodos no se pueden serializar. Usted serializa datos, es decir, propiedades y campos.

1

Sí. Cualquier método que devuelva un IEnumerable puede tener su propio código para yield return sea lo que sea que le indique. Si serializa el estado interno de su objeto en cuanto a lo que estaba iterando y qué tan lejos estuvo, entonces puede volver a cargar ese estado en algún momento futuro y continuar la enumeración justo donde lo dejó.