Двоичная сериализация в C# (действительно, WYSIWYG-сериализация)

(для WYSIWYG я имею в виду, что я решаю, ЧТО написать и КАК это написать, а не кто-то в Microsoft или Google) (ОК.. Технически я ничего не решаю... Кто-то, кто программировал несколько лет назад, решил, и я могу только спросить, насколько высоко я должен прыгнуть)

Сегодня я чувствую себя немного глупо, но я уже потерял два часа в поисках решения :-(.

В настоящее время...

У меня бинарный протокол. Он основан на C, так что это похоже на структуру C, в которой определено порядок следования байтов машины (и, к счастью, это то же самое, что и «локальный» порядок следования байтов), определены размеры различных типов, определено выравнивание структуры данных, макет структуры определен, строки представляют собой фиксированные массивы символов в известной кодировке... Все определено! И все очень похоже на C# unsafe struct, когда вы используете [(LayoutKind.Explicit)] и не очень придирчивы к использованию модификатора fixed для массивов. Теперь мне нужно сериализовать/десериализовать его на С#... Я огляделся, но ничего не смог найти... Что я пропустил? Прежде чем вы спросите, я знаю о BinaryFormatter, но мне недостаточно WYSIWYG... BinaryFormatter реализует свой язык форматирования. Да, я знаю о BitConverter (и о том, что он не реализует преобразователи для прямого порядка байтов), но это не "полное" решение. Это только "базовый" инструмент. И я знаю о BinaryWriter/BinaryReader, но они, похоже, не поддерживают массивы, отличные от byte[] или char[], и они, похоже, не могут «дополнять» массив при записи (у вас есть массив из 5 элементов byte[] и вам нужно записать его как массив из 10 элементов byte[], потому что формат, который вы используете, требует этого... Для этого вам нужно написать строки кода)

План B (но, возможно, даже план Z) состоит в том, чтобы создать тень unsafe struct для каждого класса, интерфейс IWysiwygSerializable с двумя методами (Read и Write) и реализовать интерфейс в каждом классе (запись заполнит unsafe struct и запишет его в выходных данных). поток, чтение сделало бы наоборот) (или я мог бы даже напрямую сделать несколько десятков BitConverter в Read и Write без использования struct, но для массивов это немного сложнее)

Спасибо!


person xanatos    schedule 18.06.2011    source источник


Ответы (3)


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

binaryserializer.com

person Jeff    schedule 08.03.2013

Не используйте BinaryFormatter. Вместо этого используйте BinaryWriter и BinaryReader для записи на диск точных байтов, которые вы хотите записать на диск, в точном порядке. Если массивы обрабатываются не так, как вам нравится, вам просто нужно самостоятельно пройтись по массиву. Чтобы это выглядело чище, вы, возможно, могли бы написать метод расширения для выполнения цикла.

person Fantius    schedule 18.06.2011
comment
В конце концов я написал набор методов расширения, которые мне помогут :-) - person xanatos; 10.07.2012

(обратите внимание, что это, возможно, можно было бы считать рекламой, но «продукт» имеет лицензию MIT с открытым исходным кодом, и даже другой упомянутый «продукт» имеет лицензию MIT с открытым исходным кодом) (обратите внимание, что я являюсь автором рекламируемого «продукта» и другой упомянутый «продукт»)

Не было никакого «хорошего» решения, поэтому я сделал свое :-) Мне пришлось создать библиотеку только для того, чтобы создать библиотеку: FluentSerializer. Библиотеку можно использовать для создания «описания» того, как вы хотите, чтобы ваши двоичные данные были сериализованы. Это описание написано беглой нотацией. Вы можете (через другую библиотеку, которую я написал, FluentStatement) включить в описание FluentStatement все обычные операторы, такие как while, if, for... (явно используя даже там беглую запись). Затем ваше описание компилируется в виде дерева выражений, а затем в виде группы динамических методов (Serialize, Deserialize и Size (сериализованных данных)).

Небольшой образец сериализатора для тестового класса

/// <summary>
/// 
/// </summary>
public class Serializer : ISerializer<MyClass, EmptyParameters>
{
    #region ISerializer<MyClass,EmptyParameters> Members

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    public Expression<Serializer<MyClass, EmptyParameters>> GetSerializer()
    {
        return (op, obj, par) => Statement.Start(fl => fl
            .Serialize(obj.Version)

            // Static objects can be serialized/deserialized.
            .Serialize(MyClass.StaticReadonlyInts1, typeof(FixedLength<>))

            // So can readonly collections.
            .Serialize(obj.ReadonlyInts1, typeof(FixedLength<>))

            // Both array and List<> (and Dictionary<,>, and SortedDictionary<,>, and
            // many other types of collections)
            ////.Serialize(obj.ReadonlyList1)
            .Serialize(obj.ReadonlyList1, typeof(VariableLengthByte<>))

            ////// Readonly fields can be serialized/deserialized.
            ////// Sadly you can't Dump() serializers that replace read only fields
            ////// (replace is the keyword here, readonly int X is a no-no, 
            ////// readonly List<int> X is a yes, readonly int[] X is a yes if it's 
            ////// FixedLength<>.
            ////.Serialize(obj.ReadonlyInt1)

            .Serialize(obj.Bool1)
            .Serialize(obj.Int2)

            // This will be serialized/deserialized only if obj.Version != 0
            // It's only an example of what you can do. You can use the full power of
            // FluentStatement, and remember that if instead of EmptyParameters you
            // had used another class as the parameters, you could have manipulated it
            // through the par object, so par.Version for example.
            .If(obj.Version != 0, fl.Serialize(obj.Int3))

            // This if(s) depend on the operation that is being done
            // (serialization/deserialization/size)
            .If(op == Operation.Serialize, fl.Serialize(obj.Int2))
            .If(op == Operation.Deserialize, fl.Serialize(obj.Int3))

            .Serialize(obj.Short1)

            // Tuples are supported.
            .Serialize(obj.Tuple1)

            // Arrays need to have the length prepended. There are helpers for this.
            // The default helper can be specified in the Serializer<T> constructor and
            // will be used if the field serializer isn't specified.
            ////.Serialize(obj.Ints1)

            // Or you can specify it:
            .Serialize(obj.Ints2, typeof(VariableLengthByte<>))
            .Serialize(obj.Ints3, typeof(VariableLengthByte<int[]>))

            // Nullable types are supported
            .Serialize(obj.NullableInt1, typeof(Nullable<int>))
            ////.Serialize(obj.NullableInt2)

            // But note that you could even use the Optional<> with value types,
            // usefull for example if you have to use a modifier that is a class
            // (like VariableLengthInt32 for example)
            .Serialize(obj.NullableInt1, typeof(Optional<int>))
            .Serialize(obj.NullableInt2, typeof(Optional<>))

            // So are "optional" objects (fields that can be null)
            // (Note that here if we wanted to specify the helper, we would have
            // to use typeof(Optional<VariableLengthByte<int>>)
            .Serialize(obj.OptionalInts1, typeof(Optional<VariableLengthInt32<int[]>>))
            .Serialize(obj.OptionalInts2, typeof(Optional<>))
            .Serialize(obj.OptionalList1, typeof(Optional<VariableLengthInt32<List<int>>>))
            .Serialize(obj.OptionalList2, typeof(Optional<>))

            // You can serialize a DateTime as the full .NET value
            .Serialize(obj.DateTime1)

            // Or, for example, as an Unix datetime (32 or 64 bits)
            .Serialize(obj.DateTime2, typeof(UnixDateTime<int>))

            .Serialize(obj.Float1)
            .Serialize(obj.Double1)
            .Serialize(obj.Decimal1)
            .Serialize(obj.TimeSpan1)

            // For strings it's a little more complex. There are too many combinations 
            // of possible formats (encoding x string length * (use char or byte length))
            // At this time there isn't any helper for C strings (null terminated strings).
            // You have to "manually" register you string formats.
            .Serialize(obj.String1, typeof(Program.MyUtf8VariableLengthInt32String))
            .Serialize(obj.String2, typeof(Program.MyAsciiVariableLengthInt32String))
            .Serialize(obj.String3, typeof(Program.MyUnicodeVariableLengthInt32String))

            // Chain serializing the base class can be done in this way
            .Serialize(obj, typeof(MySimpleClass))

            // This is only to make it easy to add new serialization fields. The last ) is
            // "attached" to the .Empty and doesn't need to be moved.
            .Empty());
    }

    #endregion
}

Ясно, что эта библиотека хороша только в том случае, если вам нужно сериализовать/десериализовать много данных. Если у вас есть только один объект для сериализации/десериализации, вам, вероятно, достаточно BinaryReader/BinaryWriter (как было предложено мной в исходном вопросе и Фантиусом в его ответе).

person xanatos    schedule 01.09.2012