Сериализация и оператор Yield

Действительно ли возможно сериализировать метод, содержащий yield операторы (или класс, который содержит такой метод) таким образом, что при перегидратировании класса внутреннее состояние сгенерированного итератора сохраняется?

16
задан svick 13 April 2014 в 21:01
поделиться

4 ответа

Да, вы можете это сделать. С оговорками.

Пример сериализации метода с yield, десериализации и продолжения можно найти здесь: http://www.agilekiwi.com/dotnet/CountingDemo.cs (Web Archive Link).

В общем случае, попытка сериализации без выполнения некоторой дополнительной работы будет неудачной. Это происходит потому, что сгенерированные компилятором классы не помечены атрибутом Serializable. Однако это можно обойти.

Я бы отметил, что причина, по которой они не помечены атрибутом serializable, заключается в том, что они являются деталями реализации и могут быть изменены в будущих версиях, поэтому вы не сможете десериализовать их в новой версии.

Связано с вопросом, который я задал на как сериализовать анонимные делегаты, который должен работать и в этом случае.

Вот исходный код "хака":

// 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
}
8
ответ дан 30 November 2019 в 22:49
поделиться

Внутри оператор yield преобразуется в государственную машину, реализованную в виде класса, реализующего интерфейс IEnumerator. Он позволяет выполнять итерации по набору результатов, используя несколько операторов foreach одновременно. Этот класс не виден вашему коду, он не помечен как сериализуемый.

Итак, ответ - нет, это невозможно. Но вы можете реализовать желаемый перечислитель самостоятельно, но это требует больше труда, чем yield.

5
ответ дан 30 November 2019 в 22:49
поделиться

Да. Любой метод, возвращающий IEnumerable, может иметь собственный код для yield return независимо от того, что вы ему скажете. Если вы сериализуете внутреннее состояние вашего объекта относительно того, что он повторял и как далеко он зашел, вы можете перезагрузить это состояние в будущем и продолжить перечисление с того места, где вы остановились.

1
ответ дан 30 November 2019 в 22:49
поделиться

Просто убедитесь, что непосредственно перед вызовом yield вы сохранили состояние (т.е. позицию итератора) в сериализуемом поле (поле местоположения или как вы его называете). Затем, когда класс десериализован, просто продолжите с того места, где вы остановились, используя поле местоположения.

Но когда это будет полезно? Планируете ли вы сериализовать объекты в середине цикла foreach? Может быть, вы сделаете это намного проще, если дадите классу метод SetIteratorPosition () , который по умолчанию соответствует текущей позиции. Это понятнее, чем добавление побочных эффектов к существующему четко определенному поведению (yield), и каждый поймет, что IteratorPosition можно сохранить.

Примечание: методы не могут быть сериализованы. Вы сериализуете данные, то есть свойства и поля.

1
ответ дан 30 November 2019 в 22:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: