C#中结构体的逐字节序列化

10

我正在寻找C#中序列化的语言支持。我可以从ISerializable派生并通过将成员值复制到字节缓冲区中来实现序列化。但是,我更喜欢像在C/C++中那样可以更自动化的方式。

考虑以下代码:

using System;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace XBeeHelper
{
    class XBee
    {
        [Serializable()]
        public struct Frame<FrameType> where FrameType : struct
        {
            public Byte StartDelimiter;
            public UInt16 Lenght;
            public Byte APIIdentifier;
            public FrameType FrameData;
            public Byte Checksum;
        }

        [Serializable()]
        public struct ModemStatus
        {
            public Byte Status;
        }

        public Byte[] TestSerialization()
        {
            Frame<ModemStatus> frame = new Frame<ModemStatus>();
            frame.StartDelimiter = 1;
            frame.Lenght = 2;
            frame.APIIdentifier = 3;
            frame.FrameData.Status = 4;
            frame.Checksum = 5;

            BinaryFormatter formatter = new BinaryFormatter();
            MemoryStream stream = new MemoryStream();
            formatter.Serialize(stream, frame);
            Byte[] buffer = stream.ToArray();
            return buffer;
        }
    }
}

我有一个通用的框架结构,作为多种负载类型的包装器,用于串行传输。ModemStatus是这样一种负载的例子。

然而,运行TestSerialization()会返回一个长度为382字节的缓冲区(不含期望内容)!它应该包含6个字节。是否可能在没有手动序列化的情况下正确地序列化这些数据?


一个类似的问题有很多答案:https://dev59.com/OHA75IYBdhLWcg3weY5f - György Kőszeg
4个回答

17

只需使用以下两种方法:

public static class StructTools
{
    /// <summary>
    /// converts byte[] to struct
    /// </summary>
    public static T RawDeserialize<T>(byte[] rawData, int position)
    {
        int rawsize = Marshal.SizeOf(typeof(T));
        if (rawsize > rawData.Length - position)
            throw new ArgumentException("Not enough data to fill struct. Array length from position: "+(rawData.Length-position) + ", Struct length: "+rawsize);
        IntPtr buffer = Marshal.AllocHGlobal(rawsize);
        Marshal.Copy(rawData, position, buffer, rawsize);
        T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T));
        Marshal.FreeHGlobal(buffer);
        return retobj;
    }

    /// <summary>
    /// converts a struct to byte[]
    /// </summary>
    public static byte[] RawSerialize(object anything)
    {
        int rawSize = Marshal.SizeOf(anything);
        IntPtr buffer = Marshal.AllocHGlobal(rawSize);
        Marshal.StructureToPtr(anything, buffer, false);
        byte[] rawDatas = new byte[rawSize];
        Marshal.Copy(buffer, rawDatas, 0, rawSize);
        Marshal.FreeHGlobal(buffer);
        return rawDatas;
    }
}

请按以下方式指定您的结构体(通过一个字节来指定确切大小和包装(对齐)。默认为8):

[StructLayout(LayoutKind.Explicit, Size = 11, Pack = 1)]
private struct MyStructType
{
    [FieldOffset(0)]
    public UInt16 Type;
    [FieldOffset(2)]
    public Byte DeviceNumber;
    [FieldOffset(3)]
    public UInt32 TableVersion;
    [FieldOffset(7)]
    public UInt32 SerialNumber;
}

现在您可以使用以下方法进行反序列化:

StructTools.RawDeserialize<MyStructType>(byteArray, 0); // 0 is offset in byte[]

并使用序列化

StructTools.RawSerialize(myStruct);

1
我现在已经使用了这个答案一个月了,它非常棒。 - rocketsarefast
必须非常棒!看看谁写了第二个答案...但是Jon仍然是StackOverflow的MacGyver。 - JCH2k

10

正如Chris所说的,你可以使用不安全代码——在这种情况下,最好明确指定布局。当然,在这一点上,你正在减少CLR的优化能力——你将会遇到未对齐访问、原子性丢失等问题。这可能与你无关,但值得注意。

个人认为这是一种非常脆弱的序列化/反序列化方式。如果任何事情发生变化,你的数据就无法读取了。如果你尝试在使用不同字节顺序的架构上运行,你会发现所有的值都被搞乱了。此外,使用内存布局将在你需要使用引用类型时失败——这可能会影响你自己的类型设计,鼓励你在本来应该使用类的地方使用结构体。

我更喜欢明确地读取和写入值(例如使用BinaryWriter,或者更好的是一个可以让你设置字节顺序的二进制写入器的版本),或者使用可移植的序列化框架,例如Protocol Buffers


我正在尝试为一个通过UART接受命令的小芯片定义协议结构(用于测试)。对于我来说,协议基本上已经确定了,我永远不会存储数据并稍后读取它。但是对于严肃的序列化/反序列化,我一定会遵循您的建议。谢谢! - joelr

1
看这个链接。它使用Marshal机制来获取你的结构体的实际数据并将其复制到Byte[]中。还有,如何将它们复制回去。这些函数的好处是它们是通用的,所以它们适用于所有的结构体(除非它们包含像字符串这样具有可变大小的数据类型)。

http://dooba.net/2009/07/c-sharp-and-serializing-byte-arrays/


@David:链接已损坏。 - Shantanu Gupta

-1

也许可以使用通用的序列化/反序列化方法:

public static string SerializeObject<T>(T obj)
{
      string xmlString = null;
      using(MemoryStream memoryStream = new MemoryStream())
      {
        using(XmlSerializer xs = new XmlSerializer(typeof(T)))
        {
            XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
            xs.Serialize(xmlTextWriter, obj);
            memoryStream = (MemoryStream)xmlTextWriter.BaseStream;
            xmlString = UTF8ByteArrayToString(memoryStream.ToArray());      
        }
      }
      return xmlString;
}

public static T DeserializeObject<T>(string xml)
{
   XmlSerializer xs = new XmlSerializer(typeof(T));
   MemoryStream memoryStream = new MemoryStream(StringToUTF8ByteArray(xml));
   XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
   return (T)xs.Deserialize(memoryStream);
}

原文在这里找到。


4
抱歉挖掘过去,但这个代码很糟糕。XmlSerializer不是IDisposable,所以不能在using语句中使用。创建并处理了“new MemoryStream()”,但从未使用。memoryStream被分配了两次,这将无法编译,因为它是using语句的一部分。UTF8ByteArrayToString()StringToUTF8ByteArray()在任何地方都没有定义。也许你现在有更多经验,可以抽出时间来修复它? - Buh Buh

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