序列化和Yield语句

16

能否对含有 yield 语句的方法(或包含该方法的类)进行序列化,以便在重新还原类时,保留生成的迭代器的内部状态?


1
你想在一个地方开始运行该方法,然后在另一个地方继续运行它吗? - Matt Mills
1
@arootbeer:这也可能是一个“保存进度”的问题,不一定需要通过网络发送。 - Scott Stafford
@Scott Stafford:这样更有意义。 - Matt Mills
4个回答

8
是的,您可以这样做,但有一些注意事项。
一个使用yield序列化方法、反序列化并继续执行的示例可以在这里找到:http://www.agilekiwi.com/dotnet/CountingDemo.csWeb Archive Link)。
通常情况下,尝试进行序列化而不进行额外工作会失败。这是因为编译器生成的类没有标记为可序列化的属性。但是,您可以通过一些方法解决这个问题。
需要注意的是,它们没有被标记为可序列化的原因是它们是实现细节,并且在将来的版本中可能会发生变化,因此您可能无法在新版本中反序列化它。
与我在如何序列化匿名委托上提出的问题相关,这对于此案例也适用。
以下是“hack”的源代码:
// 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
}

很遗憾,该链接现在已经失效。 - bohdan_trotsenko
@modosansreves 幸运的是它已经在网络档案库中备份了。 - Bartek Banachewicz
感谢您提供的存档链接! - deek0146

5

在内部,yield语句被转换为状态机实现的类,该类实现了IEnumerator接口。它允许使用多个foreach语句同时迭代结果集。但是,该类对您的代码不可见,也没有标记为可序列化。

因此,答案是否定的,这是不可能的。但是,您可以单独实现所需的枚举器,但它需要比yield更多的工作。


4
我会说这并不完全正确。虽然有可能,但强烈不建议这样做。 - Joseph Kingry

1

在调用yield之前,确保将状态(即迭代器位置)保存在可序列化字段(位置字段或其他名称)中。然后,在反序列化类时,只需使用位置字段继续之前的操作。

但是,这什么时候有用呢?您是否计划在foreach循环中序列化对象?也许如果您给类一个SetIteratorPosition()方法,默认为当前位置,会更容易理解。这比向现有定义良好的行为(yield)添加副作用更清晰,并且每个人都会理解可以保存IteratorPosition

注意:方法无法序列化。您需要序列化数据,即属性和字段。


1

是的。任何返回IEnumerable的方法都可以有自己的代码来yield return你告诉它的任何东西。如果你将对象的内部状态序列化为它正在迭代的内容以及迭代到哪个位置,那么你可以在将来的某个时间重新加载该状态,并从离开的地方继续枚举。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接