ISerializationSurrogate
Тяжёлая жизнь довела меня до того, что мне потребовалось сериализовать итераторы C#. Проблема в том, что компилятор C# не считает должным вешать атрибут [Serializable]
на генерируемые для итераторов классы (а так же на классы-замыкания для лямбда-выражений и анонимных методов). В F# подобной проблемы не существует:
let xs = seq { yield 1 } in xs.GetType().IsSerializable // true
id.GetType().IsSerializable // true
Один из вариантов решения проблемы - использовать объект-суррогат, подменяющий несериализуемый объект и описывающий корректный процесс сериализации / десериализации объекта. Самая простая реализация - проход рефлексией по полям объекта и сохранение их в объект SerializationInfo
(вообщем-то нам ничего другого и не остаётся сделать, так как реальный тип итератора недоступен). Пример реализации:
using System;
using System.Reflection;
using System.Runtime.Serialization;
sealed class AnySurrogate : ISerializationSurrogate
{
const BindingFlags AllFields =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) {
Type objType = obj.GetType();
foreach (var field in objType.GetFields(AllFields)) {
info.AddValue(
field.Name,
field.GetValue(obj),
field.FieldType);
}
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) {
Type objType = obj.GetType();
foreach (var serializedValue in info) {
var field = objType.GetField(serializedValue.Name, AllFields);
if (field == null) {
throw new SerializationException(string.Format(
"Field '{0}' is not founded in target type.", serializedValue.Name));
}
if (field.FieldType != serializedValue.ObjectType) {
throw new SerializationException(string.Format(
"Field '{0}' with type '{1}' is not matched with the target field type of '{2}'.",
serializedValue.Name, serializedValue.ObjectType, field.FieldType));
}
field.SetValue(obj, serializedValue.Value);
}
return obj;
}
}
Пример использования:
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
class Foo {
static IEnumerator Bar() {
var now = DateTime.Now;
yield return now.Ticks;
yield return now.Ticks;
}
static void Main() {
var e1 = Bar();
// создаём экземпляр итератора
var selector = new SurrogateSelector();
selector.AddSurrogate(
type: e1.GetType(), // реальный тип итератора
context: new StreamingContext(
StreamingContextStates.All), // это важно!
surrogate: new AnySurrogate() // экземпляр суррогата
);
using (var mem = new MemoryStream()) {
var binary = new BinaryFormatter {
SurrogateSelector = selector
};
e1.MoveNext(); // первый yield return
Console.WriteLine(e1.Current);
// сериализуем экземпляр итератора
binary.Serialize(mem, e1);
mem.Position = 0;
// десериализуем экземпляр итератора
var e2 = (IEnumerator)binary.Deserialize(mem);
e2.MoveNext(); // второй yield return
Console.WriteLine(e2.Current);
Console.WriteLine(e2.MoveNext()); // false
}
}
}
Похоже на то, что реализовав ISurrogateSelector
, можно избавиться от необходимости в задании типа объекта при добавлении объекта-суррогата и сериализовать что угодно, не отмеченное [Serializable]
, но это уже другая история…