包序列化模式
定义
定义
一、说明
包序列化模式是为了解决极限序列化的问题。常规序列化的瓶颈,主要是反射、表达式树、创建对象等几个方面,这几个问题在运行时阶段,都没有一个好的解决方案。目前在net6
以后,微软大力支持源生成,这使得这类问题得到了很大程度的解决。所以,这时候包序列化模式就显得非常需要了。
二、特点
2.1 优点
- 简单、可靠、高效
- 可以支持所有类型(需要自己编写代码)
- 数据量最少(从理论来说这是占数据量最轻量的设计)
2.2 缺点
- 不支持跨语言。
- 类型版本兼容性比较差,简单来说就是高版本只能新增属性,不能删除属性,不能修改属性类型(如果类型长度一致,则可以修改类型 ,例如:
int
->float
)。
三、使用
3.1 简单类型
例如:
下列类型MyClass
,有一个int
类属性和一个string
类属性。
public class MyClass
{
public int P1 { get; set; }
public string P2 { get; set; }
}
我们可以使用包序列化模式,将MyClass
序列化成二进制流,或者反序列化成MyClass
。
那么首先需要实现IPackage
接口(或者继承PackageBase
),然后依次将属性写入到ByteBlock
中,或者从ByteBlock
中读取属性。
public class MyClass:PackageBase
{
public int P1 { get; set; }
public string P2 { get; set; }
public override void Package<TByteBlock>(ref TByteBlock byteBlock)
{
//将P1与P2属性按类型依次写入
byteBlock.WriteInt32(this.P1);
byteBlock.WriteString(this.P2);
}
public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
{
//将P1与P2属性按类型依次读取
this.P1 = byteBlock.ReadInt32();
this.P2 = byteBlock.ReadString();
}
}
3.2 数组(列表)类型
对于数组、列表等类型,需要先判断是否为null
,然后再写入有效值。
如果有效值是自定义类型,则也需要实现IPackage
接口,然后依次写入。
public class MyArrayClass : PackageBase
{
public int[] P5 { get; set; }
public override void Package<TByteBlock>(ref TByteBlock byteBlock)
{
//集合类型,可以先判断集合是否为null
byteBlock.WriteIsNull(P5);
if (P5 != null)
{
//如果不为null
//就先写入集合长度
//然后遍历将每 个项写入
byteBlock.WriteInt32(P5.Length);
foreach (var item in P5)
{
byteBlock.WriteInt32(item);
}
}
}
public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
{
var isNull_P5 = byteBlock.ReadIsNull();
if (!isNull_P5)
{
//有值
var count = byteBlock.ReadInt32();
var array = new int[count];
for (int i = 0; i < count; i++)
{
array[i]=byteBlock.ReadInt32();
}
//赋值
this.P5 = array;
}
}
}
3.3 字典类型
字典类型基本上和数组类似,也是先判断是否为null
,然后再写入有效值。
public class MyDictionaryClass : PackageBase
{
public Dictionary<int, MyClassModel> P6 { get; set; }
public override void Package<TByteBlock>(ref TByteBlock byteBlock)
{
//字典类型,可以先判断是否为null
byteBlock.WriteIsNull(P6);
if (P6 != null)
{
//如果不为null
//就先写入字典长度
//然后遍历将每个项,按键、值写入
byteBlock.WriteInt32(P6.Count);
foreach (var item in P6)
{
byteBlock.WriteInt32(item.Key);
byteBlock.WritePackage(item.Value);//因为值MyClassModel实现了IPackage,所以可以直接写入
}
}
}
public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
{
var isNull_6 = byteBlock.ReadIsNull();
if (!isNull_6)
{
int count = byteBlock.ReadInt32();
var dic = new Dictionary<int, MyClassModel>(count);
for (int i = 0; i < count; i++)
{
dic.Add(byteBlock.ReadInt32(), byteBlock.ReadPackage<MyClassModel>());
}
this.P6 = dic;
}
}
}
提示
属性的读取和写入时,没有先后顺序,只要保证读取的顺序与写入的顺序一致即可。
四、打包和解包
4.1 使用内存块
使用内存块,使用ByteBlock
类。
//声明内存大小。
//在打包时,一般会先估算一下包的最大尺寸,避免内存块扩张带来的性能损失。
using (var byteBlock = new ByteBlock(1024 * 64))
{
//初始化对象
var myClass = new MyClass()
{
P1 = 10,
P2 = "RRQM"
};
myClass.Package(byteBlock);
Console.WriteLine($"打包完成,长度={byteBlock.Length}");
//在解包时,需要把游标移动至正确位置,此处为0.
byteBlock.SeekToStart();
//先新建对象
var newMyClass = new MyClass();
newMyClass.Unpackage(byteBlock);
Console.WriteLine($"解包完成,{newMyClass.ToJsonString()}");
}
4.2 使用值类型内存块
使用值类型内存块,使用ValueByteBlock
类。
//声明内存大小。
//在打包时,一般会先估算一下包的最大尺寸,避免内存块扩张带来的性能损失。
var byteBlock = new ValueByteBlock(1024 * 64);
try
{
//初始化对象
var myClass = new MyClass()
{
P1 = 10,
P2 = "RRQM"
};
myClass.Package(ref byteBlock);
Console.WriteLine($"打包完成,长度={byteBlock.Length}");
//在解包时,需要把游标移动至正确位置,此处为0.
byteBlock.SeekToStart();
//先新建对象
var newMyClass = new MyClass();
newMyClass.Unpackage(ref byteBlock);
Console.WriteLine($"解包完成,{newMyClass.ToJsonString()}");
}
finally
{
byteBlock.Dispose();
}