admin管理员组

文章数量:1356441

I'm starting with serialization in C# and am a bit confused about how ISerializable works. I have something like

[Serializable]
public class MyClass : ISerializable
{
    public enum MyEnum {FIRST, SECOND}
    
    private Dictionary<MyEnum,Vector2[]> dict;
    
    ...
}

I want to manually/low-level (de)serialize it, i.e. with GetObjectData and the constructor. Now in Java I would just write down something like

os.write(dict.size());
for (var pair : dict) {
    os.write(pair.getKey().ordinal());
    os.write(pair.getValue().length());
    for (Vector2 vec : pair.getValue()) {
        os.write(vec.getX());
        os.write(vec.getY());
    }
}

Since I know the order in which the stuff was written, I can deserialize it as well.

However, in C# the SerializationInfo info has a method AddValue which wants a string key from me. Vector2 isn't implementing ISerializable. I thought of writing a utility method, but then again I need the keys to be different every time? Can't just use info.AddValue("x", v.X) because there might be more vectors. Can't use info.AddValue("x_"+i, v[i].X) because there might be multiple arrays along the line.

What to do? I'm missing something like a compound element.

I'm starting with serialization in C# and am a bit confused about how ISerializable works. I have something like

[Serializable]
public class MyClass : ISerializable
{
    public enum MyEnum {FIRST, SECOND}
    
    private Dictionary<MyEnum,Vector2[]> dict;
    
    ...
}

I want to manually/low-level (de)serialize it, i.e. with GetObjectData and the constructor. Now in Java I would just write down something like

os.write(dict.size());
for (var pair : dict) {
    os.write(pair.getKey().ordinal());
    os.write(pair.getValue().length());
    for (Vector2 vec : pair.getValue()) {
        os.write(vec.getX());
        os.write(vec.getY());
    }
}

Since I know the order in which the stuff was written, I can deserialize it as well.

However, in C# the SerializationInfo info has a method AddValue which wants a string key from me. Vector2 isn't implementing ISerializable. I thought of writing a utility method, but then again I need the keys to be different every time? Can't just use info.AddValue("x", v.X) because there might be more vectors. Can't use info.AddValue("x_"+i, v[i].X) because there might be multiple arrays along the line.

What to do? I'm missing something like a compound element.

Share Improve this question edited Mar 28 at 12:51 Uwe Keim 40.8k61 gold badges190 silver badges304 bronze badges asked Mar 27 at 23:21 SK19SK19 1831 silver badge8 bronze badges 12
  • As of .NET 9, there is no builtin binary serialization technology in .NET. See BinaryFormatter migration guide and BinaryFormatter removed from .NET 9. It's deprecated in .NET 8. So, if you are working in .NET Core, how ISerializable works is pretty much moot. It's still there (with all its insecurities) in .NET Framework, but I wouldn't recommend using it even there. – dbc Commented Mar 27 at 23:39
  • Ah. But surely there are frameworks for that? Searching for serialization only gives me Json and Xml stuff which I don't want to use, – SK19 Commented Mar 27 at 23:41
  • There is no builtin binary formatter as of .NET 9. See Is there a high performance way to replace the BinaryFormatter in .NET5?. You'll need to chose a serializer package and install it yourself, then annotate your types with the necessary attributes. For some options from MSFT, see Choose a serializer. – dbc Commented Mar 27 at 23:46
  • 1 And as far as I know (but I could be wrong), none of the binary formatters that MSFT suggests support ISerializable. See also SYSLIB0051: Legacy serialization support APIs are obsolete - you'll get a compiler warning if you implement a GetObjectData() method as of .NET 8. – dbc Commented Mar 27 at 23:46
  • 1 Does Grpc work for your purposes? – mjwills Commented Mar 28 at 1:58
 |  Show 7 more comments

5 Answers 5

Reset to default 1

If you just wanted to convert an array of vectors to bytes you could possibly just cast it directly using MemoryMarshal.Cast<Vector2, byte>. This might not be safe for storage since .Net could change how vector2 is represented in memory, especially considering big/little endianess.

Since you have a more complex object graph my recommendation would be to use a serialization library. My preference is protobuf , but there are several others. Since Protobuf cannot handle Vector2 by default we need to do some configuring:

using ProtoBuf.Meta;
...

var typeModel = RuntimeTypeModel.Create();
typeModel.Add(typeof(Vector2), false).Add(nameof(Vector2.X), nameof(Vector2.Y));

var data = new Dictionary<MyEnum, Vector2[]>(){
     { MyEnum.FIRST, [Vector2.UnitX, Vector2.UnitY] }, 
     { MyEnum.SECOND, [Vector2.One,] } };
var ms = new MemoryStream();

typeModel.Serialize(ms, data);
ms.Position = 0;
var result = typeModel.Deserialize<Dictionary<MyEnum, Vector2[]>>(ms);

You may want to consider some kind of interface to make it easier to switch between serialization methods.

An alternative would be to use BinaryReader/Writer directly. A common way to deal with sequences of something is to prefix the length, i.e. length | v[0].X | v[0].Y | v[1].X.... But in my experience serializing manually require significantly more code, and tend to be more fragile if you ever want to add more data in the future.

Whatever you do you need to follow a protocol for how objects and primitives are converted to bytes. BinaryFormatter was obsoleted for several very good reasons. There are well known third party protocol like protobuf, or you can invent something yourself. But chances are that the third party protocols will be better designed and easier to use than anything you would come up with yourself.

If you want this to be highly performant and space-efficient, you could consider using MemoryPack (for which you would need to install the MemoryPack NuGet packager).

For your example it would look like this:

using System.Numerics;
using System.Text;
using MemoryPack;

namespace ConsoleApp1;

public static class Program
{
    static void Main()
    {
        var unserialised = new MyClass();
        unserialised.Add(MyClass.MyEnum.FIRST,  [new Vector2(1, 2), new Vector2(3,  4), new Vector2( 5,  6)]);
        unserialised.Add(MyClass.MyEnum.SECOND, [new Vector2(7, 8), new Vector2(9, 10), new Vector2(11, 12)]);
        Console.WriteLine(unserialised);

        byte[] data = MemoryPackSerializer.Serialize(unserialised);

        Console.WriteLine("Data length (bytes) = " + data.Length + "\n");

        var deserialised = MemoryPackSerializer.Deserialize<MyClass>(data);

        Console.WriteLine(deserialised);
    }
}

[MemoryPackable]
public partial class MyClass
{
    public enum MyEnum { FIRST, SECOND }

    [MemoryPackInclude]
    private Dictionary<MyEnum, Vector2[]> dict = new();

    public void Add(MyEnum key, Vector2[] value)
    {
        dict.Add(key, value);
    }

    public override string ToString()
    {
        var result = new StringBuilder();

        foreach (var (key, value) in dict)
        {
            result.Append(key + ": ");
            result.AppendLine(string.Join(", ", value));
        }

        return result.ToString();
    }
}

This serialises the data into 69 bytes, which is an overhead of 21 bytes over the space needed for 12 raw floats. This is a fixed overhead for the Dictionary - if you increase the size of the vectors, the serialised data will only increase by the binary size of the additional floats (4 bytes each).

MemoryPack is an evolution of the older (and slightly less performant) MessagePack.

TL;DR:

  • If you are not interested in alternative ways and want to use only BinaryFormatter, see the solution above the last code block.
  • If you don't mind using a 3rd party library and the main goal is a compact payload, then just see the first example below. You can use the same approach as with BinaryFormatter, still, the result result is much more compact.

If you want to use binary serialization that relies on the IFormatter infrastructure ([Serializable] attribute, ISerializable, etc.) in a safe way, feel free to try this serializer from this library (disclaimer: I'm the author).

I want to manually/low-level (de)serialize it, i.e. with GetObjectData and the constructor.

It actually supports the Vector2 type natively (depending on the targeted framework), so you don't even need custom serialization:


public enum MyEnum {FIRST, SECOND}

[Serializable]
public class MyClass // : ISerializable - not needed (but read further)
{
    private Dictionary<MyEnum,Vector2[]> dict;
    
    public MyClass Add(MyEnum key, params Vector2[] values)
    { 
        dict[key] = values;
        return this;
    }
}

And then the usage:

var myClass = new MyClass()
    .Add(MyEnum.FIRST, new (1, 1), new (2, 2))
    .Add(MyEnum.SECOND, new (3, 3), new (4, 4));

using var stream = new MemoryStream();
        
// serialization
var serializer = new BinarySerializationFormatter();
serializer.SerializeToStream(stream, myClass);
            
// deserialization: in safe mode you must enlist the expected custom types
// Note that only MyEnum should be specified because both Dictionary<,> and Vector2 are natively supported
stream.Position = 0;
var clone = serializer.DeserializeFromStream<MyClass>(stream,
    expectedCustomTypes: typeof(MyEnum));

If you still want customization (e.g. to prevent serializing the assembly identity of MyEnum), you can implement the ISerializable interface after all. You can do it like this:

[Serializable]
public class MyClassCustom : MyClass, ISerializable
{
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // a small customization: we use ints instead of enums so safe
        // deserialization does not need to specify the enum as an expected type:
        info.AddValue("keys", dict.Keys.Select(k => (int)k).ToArray()); // int[]

        // Note that this would NOT work by BinaryFormatter because Vector2 is
        // not marked as [Serializable]. But see my notes below the example.
        info.AddValue("values", dict.Values.ToArray()); // Vector2[][]
    }
    
    private MyClassCustom(SerializationInfo info, StreamingContext context)
    {
        var keys = (int[])info.GetValue("keys", typeof(int[]));
        var values = (Vector2[][])info.GetValue("values", typeof(Vector2[][]));
        
        for (int i = 0; i < keys.Length; i++)
            Add((MyEnum)keys[i], values[i]);
    }
    
    public MyClassCustom() { }
}

However, in C# the SerializationInfo info has a method AddValue which wants a string key from me. Vector2 isn't implementing ISerializable. I thought of writing a utility method, but then again I need the keys to be different every time?

Note that the code above is happy with a single key both for the keys and values. But it's just because we use a different formatter that happens to support Vector2 natively. If you have to use the legacy BinaryFormatter for some reason, then you can replace the Vector2 instances by ValueTuple<float, float> for example (similarly to the MyEnum -> int conversion), as tuples are still serializable.

Anyway, the serialization now is very similar. The only practical difference is that the serialization stream does not contain MyEnum now so it's not needed to be specified:

var myClass = new MyClassCustom()
    .Add(MyEnum.FIRST, new (1, 1), new (2, 2))
    .Add(MyEnum.SECOND, new (3, 3), new (4, 4));

using var stream = new MemoryStream();
        
// serialization
var serializer = new BinarySerializationFormatter();
serializer.SerializeToStream(stream, myClass);
            
// deserialization: note that MyEnum is not needed to be specified now
stream.Position = 0;
var clone = serializer.DeserializeFromStream<MyClassCustom>(stream);

Further notes:

  • I created an online demo with your case.
  • To avoid the possible security issues with the classic IFormatter serialization infrastructure make sure you read the security notes at the Remarks section of the BinarySerializationFormatter class. To be completely robust, not only its SafeMode should be used (enabled by default), but if you choose to use custom serialization, then make sure you add some validation in the deserialization constructor as well. See the 2nd example at the linked page for more details.
  • You can use the ExtractExpectedTypes method for the default case. May not be enough in complex cases, or when the extracted types are interfaces or non-sealed classes.
  • To achieve an even more compact result, you can implement the IBinarySerializable interface instead. In fact, if you follow the pattern in the documentation and you can cover the full object graph with it, then you don't even need my serializer.

Part of the point of Vector2 is that it is "raw", i.e. it is a direct representation of the data. That means that you can simply access the underlying bytes directly, if you don't need to worry about things like endianness. The following gets the same data as a ReadOnlySpan<byte> without any arrays or conversions - the span literally points at the original variable (and the contents will change if you change the values in a):

Vector2 a = new(42.5f, 16.4f);
ReadOnlySpan<byte> bytesDirect = MemoryMarshal.Cast<Vector2, byte>(
    MemoryMarshal.CreateReadOnlySpan(ref a, 1));

You should find that bytesDirect has 8 bytes with predictable values. This is like various pointer-based unsafe approaches, but: without the unsafe in your code - i.e. you can think of ReadOnlySpan<byte> as a byte* ptr and int length pair.

A Vector2[] (from the question) would be the same, except simpler because a Vector2[] can already be treated as a Span<Vector2>:

Vector2[] values = ...
ReadOnlySpan<byte> bytesDirect = MemoryMarshal.Cast<Vector2, byte>(values);

This creates a span over the entire array body, i.e. the length will be 8*values.Length.

As of .NET 9, there is no builtin binary serialization technology in .NET.

You can, however, use BinaryReader and BinaryWriter and manually serialize similar as you have demonstrated with Java.

本文标签: cHow do I byteserialize Vector2Stack Overflow