C#_结构体与字节流之间的转换

一、使用BinaryWriter 和BinaryReader

    public struct Foo
    {
        public byte A { get; set; }
        public UInt16 B { get; set; }
        public UInt32 C { get; set; }
        public bool D { get; set; }
        public string E { get; set; }
        public UInt32[] F { get; set; }
    }
        static byte[] ConverterFoo2ByteArray(Foo f)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                using (BinaryWriter bw = new BinaryWriter(ms))
                {
                    bw.Write(f.A);
                    bw.Write(f.B);
                    bw.Write(f.C);
                    bw.Write(f.D);

                    //bw.Write(f.E);//使用Write(string)的方法时,字节中会包含一个编码方式信息 
                    byte[] str = new byte[128];
                    Encoding.ASCII.GetBytes(f.E).CopyTo(str, 0);
                    bw.Write(str);

                    for (int i = 0; i < f.F.Length; i++)
                    {
                        bw.Write(f.F[i]);//没有Write(UInt32[])的重载,所以枚举每一个元素单独写
                    }
                }
                return ms.ToArray();
            }
        }

        static Foo ConverterByteArray2Foo(byte[] buffer)
        {
            using (MemoryStream ms = new MemoryStream(buffer))
            {
                using (BinaryReader br = new BinaryReader(ms))
                {
                    Foo foo = new Foo();
                    foo.A = br.ReadByte();
                    foo.B = br.ReadUInt16();
                    foo.C = br.ReadUInt32();
                    foo.D = br.ReadBoolean();
                    foo.E = Encoding.ASCII.GetString(br.ReadBytes(128));

                    foo.F = new UInt32[3];
                    foo.F[0] = br.ReadUInt32();
                    foo.F[1] = br.ReadUInt32();
                    foo.F[2] = br.ReadUInt32();

                    return foo;
                }
            }
        }

显然,上面的例子存在一个很严重的问题,如果Foo结构体中的成员有变动(增删或者顺序调整),那么就不得不修改ConverterFoo2ByteArray()方法。
那么我们可以这样做:

        static byte[] ConverterFoo2ByteArray(Foo f)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                using (BinaryWriter bw = new BinaryWriter(ms))
                {
                    Type type = f.GetType();
                    var properties = type.GetProperties();
                    foreach (var pro in properties)
                    {
                        object o = pro.GetValue(f);

                        if (o.GetType() == typeof(byte))
                            bw.Write((byte)o);
                        else if (o.GetType() == typeof(UInt16))
                            bw.Write((UInt16)o);
                        else if (o.GetType() == typeof(UInt32))
                            bw.Write((UInt32)o);
                        else if (o.GetType() == typeof(bool))
                            bw.Write((bool)o);
                        else if (o.GetType() == typeof(string))
                        {
                            byte[] str = new byte[128];
                            Encoding.ASCII.GetBytes((string)o).CopyTo(str, 0);
                            bw.Write(str);
                        }
                        else if (o.GetType() == typeof(UInt32[]))
                        {
                            for (int i = 0; i < ((UInt32[])o).Length; i++)
                            {
                                bw.Write(((UInt32[])o)[i]);//没有Write(UInt32[])的重载,所以枚举每一个元素单独写
                            }
                        }
                    }
                }
                return ms.ToArray();
            }
        }

利用反射遍历成员中的所有属性,然后判断其Type,写入流中。
但是问题又来了,读怎么办呢?好像还得一个一个读。。。没有办法归一化。。

二、使用StructLayout

        [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
        public struct Foo
        {
            private byte a;
            public byte A { get => a; set => a = value; }
            private UInt16 b;
            public UInt16 B { get => b; set => b = value; }
            private UInt32 c;
            public UInt32 C { get => c; set => c = value; }
            private bool d;
            public bool D { get => d; set => d = value; }

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            private string e;
            public string E { get => e; set => e = value; }

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
            private UInt32[] f;
            public UInt32[] F { get => f; set => f = value; }
        }

先定义好结构体,由于有些特性不能用在属性上,所以把隐藏的字段手动写出来了。

//使用这个方法将你的结构体转化为bytes数组 
        public static byte[] Struct2Bytes(Foo f)
        {
            int size = Marshal.SizeOf(f);
            byte[] bytes = new byte[size];
            try
            {
                //分配结构体大小的内存空间
                IntPtr ptr = Marshal.AllocHGlobal(size);
                //将结构体转换为分配好的内存空间
                Marshal.StructureToPtr(f, ptr, false);
                //将内存空间拷贝到字节数组
                Marshal.Copy(ptr, bytes, 0, size);
                //释放内存空间
                Marshal.FreeHGlobal(ptr);
                return bytes;
            }
            catch (Exception ee)
            {
                Console.WriteLine(ee.Message);
                return bytes;
            }
        }
        //使用这个方法将byte数组转化为结构体 
        public static T BytesToStuct2<T>(byte[] data)
        {
            //得到结构体的大小 
            int size = Marshal.SizeOf(typeof(T));
            //byte数组长度小于结构体的大小 
            if (size > data.Length)
            {
                //返回空 
                return default;
            }
            //分配结构体大小的内存空间 
            IntPtr structPtr = Marshal.AllocHGlobal(size);
            //将byte数组拷到分配好的内存空间 
            Marshal.Copy(data, 0, structPtr, size);
            //将内存空间转换为目标结构体 
            object obj = Marshal.PtrToStructure(structPtr, typeof(T));
            //释放内存空间 
            Marshal.FreeHGlobal(structPtr);
            //返回结构体 
            return (T)obj;
        }
        static void Main(string[] args)
        {
            Foo foo = new Foo
            {
                A = 0x11,
                B = 0x22,
                C = 0x33,
                D = true,
                E = "zhanghanyun",
                F = new UInt32[] { 0x44, 0x55, 0x66 },
            };
            Console.WriteLine(BitConverter.ToString(Encoding.UTF8.GetBytes(foo.E)));

            byte[] buffer = Struct2Bytes(foo);
            Console.WriteLine(BitConverter.ToString(buffer));

            Foo f = BytesToStuct2<Foo>(buffer);
        }

如果对上面代码有不理解的地方,应该是StructLayout特性和Marshal类。这里简单说一下,StructLayout特性就是为了控制结构体的内存分配,不然默认的字节对齐方式可能不是我们期待的,可以参考这里。
Marshal类主要是操作结构体和内存之间的转化,可在后期单独讲解。官方文档:https://docs.microsoft.com/zh-cn/dotnet/standard/native-interop/customize-struct-marshaling

三、总结

还是使用StructLayout特性和Marshal类进行结构体和字节流之间的转换比较合适。最后,将struct换为class也是成立的。另外推荐一个开源库也可以简单实现上述的需求:BinarySerializer

你可能感兴趣的:(BinarySerialize,c#,protocol,serialize)