---
id: fastbinaryformatter
title: 高性能二进制序列化
---

### 定义

命名空间：TouchSocket.Core <br/>
程序集：[TouchSocket.Core.dll](https://www.nuget.org/packages/TouchSocket.Core)


## 一、说明

该序列化以二进制方式进行序列化，内存和性能都非常好。并且在序列化和反序列化时支持兼容类型，甚至可以像Json一样不同类型也可以。

目前支持的类型有：
- 基础类型
- 自定义实体类、结构体
- 元组
- 支持类型组成的数组、字典、List等。

:::tip 提示

实际上经过自定义转化器，可以实现对**任意类型**的序列化和反序列化。

:::  

## 二、基本使用

### 2.1 简单使用

一般的，可以非常简单的对支持类型进行序列化和反序列化。

```csharp showLineNumbers
var bytes = FastBinaryFormatter.SerializeToBytes(10);
var newObj = FastBinaryFormatter.Deserialize<int>(bytes);
```

### 2.2 使用内存池块

在使用过程中，如果使用到频繁的序列化、反序列化，可以使用内存块，可以减少内存的申请和释放。

```csharp {6,12} showLineNumbers
//申请内存块，并指定此次序列化可能使用到的最大尺寸。
//合理的尺寸设置可以避免内存块扩张。
using (var block = new ByteBlock(1024*64))
{
    //将数据序列化到内存块
    FastBinaryFormatter.Serialize(block, 10);

    //在反序列化前，将内存块数据游标移动至正确位。
    block.SeekToStart();

    //反序列化
    var newObj = FastBinaryFormatter.Deserialize<int>(block);
}
```

### 2.3 使用值类型内存池块

常规内存块是“类”，所以在使用时，自身对象会产生GC垃圾，如果追求极致序列化，则可以使用值类型内存池块，可以做到**零GC**分配。

```csharp {8,14} showLineNumbers
//申请内存块，并指定此次序列化可能使用到的最大尺寸。
//合理的尺寸设置可以避免内存块扩张。
var block = new ValueByteBlock(1024 * 64);

try
{
    //将数据序列化到内存块
    FastBinaryFormatter.Serialize(ref block, 10);

    //在反序列化前，将内存块数据游标移动至正确位。
    block.SeekToStart();

    //反序列化
    var newObj = FastBinaryFormatter.Deserialize<int>(ref block);
}
finally
{
    //因为使用了ref block，所以无法使用using，只能使用try-finally
    block.Dispose();
}
```

## 三、常规配置

`FastBinaryFormatter`默认情况下，支持的自定义类型必须具有**公共无参构造函数**。对于成员，仅支持**公共的属性和字段**。并且对于属性，要求必须是**可读可写**，对于只读属性，默认也是不做序列化和反序列化的。

例如：下列类型中，只有`P1`和`P3`成员将有效。

```csharp showLineNumbers
public class MyClass1
{
    private int m_p5;

    //公共属性，有效
    public int P1 { get; set; }

    //自动公共属性，即使包含set访问器，但private，无效
    public int P2 { get; private set; }

    //公共字段，有效
    public int P3;

    //私有字段，无效
    private int P4;

    //公共属性，不包含set访问器，无效
    public int P5 => m_p5;

    public void SetP4(int value)
    {
        this.P4 = value;
    }

    public void SetP2(int value)
    {
        this.P2 = value;
    }
    public void SetP5(int value)
    {
        this.m_p5 = value;
    }
}
```

### 3.1 忽略成员

忽略成员，可以通过特性`[FastNonSerialized]`来忽略。

例如：

```csharp {6} showLineNumbers
public class MyClass1
{
    ...

    //公共属性，但忽略，无效
    [FastNonSerialized]
    public int P6 { get; set; }

}
```

### 3.2 强制成员

对于只读成员，有时候也需要序列化时，可以通过特性`[FastSerialized]`来强制。

例如：

```csharp {6} showLineNumbers
public class MyClass1
{
    ...

    //自动公共属性，包含set访问器，即使private，但因为FastSerialized后，有效
    [FastSerialized]
    public int P7 { get;private set; }
}
```

:::caution 注意

强制特性虽然可以将成员添加在操作行列，但是如果成员绝对不可写（或者不可读）时，执行相应操作则会抛出异常。

:::  

## 四、兼容类型

在序列化和反序列化时，并不要求类型一致，只要类型成员名称一致，且对应名称的基础类型一致，即可进行转换。

例如：下列`MyClass2`与`MyClass3`是两个不同类型

```csharp showLineNumbers
public class MyClass2
{
    public int P1 { get; set; }
}

public class MyClass3
{
    public int P1 { get; set; }
    public string P2 { get; set; }
}
```

也可以互相序列化和反序列化。

```csharp showLineNumbers
var myClass2 = new MyClass2()
{
    P1 = 10
};
var bytes = FastBinaryFormatter.SerializeToBytes(myClass2);

var newObj = FastBinaryFormatter.Deserialize<MyClass3>(bytes);
```

反序列化后的`MyClass3`

```csharp showLineNumbers
{"P1":10,"P2":null}
```

但如果是成员名称一致，但基础类型不一致的，则不会成功，且可能会抛出异常。

例如：

```csharp {3,8} showLineNumbers
public class MyClass2
{
    public int P1 { get; set; }
}

public class MyClass3
{
    public string P1 { get; set; }
}
```

:::tip 提示

兼容类型的使用，可以一定程度的解决一些兼容性问题，尤其是增加、或移除成员时都可以兼容。但是当修改成员类型时，可能会导致序列化数据丢失。

:::  

## 五、特性成员

默认情况下，确定成员的方式的是**成员名称**。当成员名称较长时（最大255字节），可能会大大增加序列化后的体积。

那这时候，就可以使用特性来确定成员。它使用的是一个`byte`值，来确定成员。

例如：

对于下列类，如果不使用特性，序列化体积可达**40字节**。

```csharp showLineNumbers
public class MyClass4
{
    public int MyProperty1 { get; set; }
    public int MyProperty2 { get; set; }
}
```

使用特性后，体积可以减少到**18字节**。

```csharp {1,4,7} showLineNumbers
[FastSerialized(EnableIndex =true)]
public class MyClass4
{
    [FastMember(1)]
    public int MyProperty1 { get; set; }

    [FastMember(2)]
    public int MyProperty2 { get; set; }
}
```

:::info 信息

使用特性来确定成员唯一性时，使用的是byte类型的值，所以它只允许最多有*255*个成员（即属性和字段的总数量）。

:::  


## 六、自定义转换器

使用自定义转化器，可以解决**所有类型**的序列化与反序列化，并且可以对特定类型进行优化。

例如：

对于下列类，只有两个int类属性是有效值。

```csharp showLineNumbers
public class MyClass5
{
    public int P1 { get; set; }
    public int P2 { get; set; }
}
```

所以，我们需要自定义一个转换器。来将这2个`int`值，转换成有效数据。

首先，声明一个转换器类，继承`FastBinaryConverter<T>`，或者实现`IFastBinaryConverter`接口。

然后实现`Read`和`Write`方法。实现逻辑如下：

```csharp {3,15} showLineNumbers
public sealed class MyClass5FastBinaryConverter : FastBinaryConverter<MyClass5>
{
    protected override MyClass5 Read<TByteBlock>(ref TByteBlock byteBlock, Type type)
    {
        //此处不用考虑为null的情况
        //我们只需要把有效信息按写入的顺序，读取即可。

        var myClass5 = new MyClass5();
        myClass5.P1 = byteBlock.ReadInt32();
        myClass5.P2 = byteBlock.ReadInt32();

        return myClass5;
    }

    protected override void Write<TByteBlock>(ref TByteBlock byteBlock, in MyClass5 obj)
    {
        //此处不用考虑为null的情况
        //我们只需要把有效信息写入即可。
        //对于MyClass5类，只有两个属性是有效的。

        //所以，依次写入属性值即可
        byteBlock.WriteInt32(obj.P1);
        byteBlock.WriteInt32(obj.P2);

    }
}
```

:::info 信息

在转化器中，我们不需要考虑操作对象为`null`的情况。但是得考虑属性值为`null`的情况。

:::  

最后附加转换器即可

```csharp {1}
[FastConverter(typeof(MyClass5FastBinaryConverter))]
public class MyClass5
{
    public int P1 { get; set; }
    public int P2 { get; set; }
}
```

或者直接往`FastBinaryFormatter`中添加转换器。

```csharp showLineNumbers
FastBinaryFormatter.AddFastBinaryConverter(typeof(MyClass5),new MyClass5FastBinaryConverter());
```

:::caution 注意

使用自定义转化器后，所有的类型兼容问题，都必须自己解决。如示例所示，我们是按顺序写入和读取的，所以一般来说，新增属性是可以的，但是移除属性时，可能得手动解决一些问题。

:::  

## 七、包模式序列化

`FastBinaryFormatter`支持一种特殊的序列化方式，[包序列化模式](./ipackage.mdx) 。可以解决一些特殊场景下的序列化，其性能更高。

并且，默认情况下，已经内置了转换器。

只需要对需要转换的对象实现`IPackage`接口（或继承`PackageBase`）即可。

例如：

下列类，实现了`IPackage`接口。在序列化时，会调用`Package`方法，反序列化时，会调用`Unpackage`方法。

```csharp {6,12} showLineNumbers
public class MyClass6:PackageBase
{
    public int P1 { get; set; }
    public int P2 { get; set; }

    public override void Package<TByteBlock>(ref TByteBlock byteBlock)
    {
        byteBlock.WriteInt32(this.P1);
        byteBlock.WriteInt32(this.P2);
    }

    public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
    {
        this.P1 = byteBlock.ReadInt32();
        this.P2 = byteBlock.ReadInt32();
    }
}
```

当然，在包模式的**源生成**可用时，也可以直接用源生成的方式实现更多细节。

```csharp {1} showLineNumbers
[GeneratorPackage]
public partial class MyClass6:PackageBase
{
    public int P1 { get; set; }
    public int P2 { get; set; }
}
```

<details>
<summary>由源生成的代码</summary>
<div>

```csharp showLineNumbers
/*
此代码由SourceGenerator工具直接生成，非必要请不要修改此处代码
*/
#pragma warning disable
using System;
using System.Diagnostics;
using TouchSocket.Core;
using System.Threading.Tasks;

namespace FastBinaryFormatterConsoleApp
{
    [global::System.CodeDom.Compiler.GeneratedCode("TouchSocket.SourceGenerator", "2.1.0.0")]
    partial class MyClass6
    {
        public override void Package<TByteBlock>(ref TByteBlock byteBlock)
        {
            byteBlock.WriteInt32(P1);
            byteBlock.WriteInt32(P2);
        }

        public override void Unpackage<TByteBlock>(ref TByteBlock byteBlock)
        {
            P1 = byteBlock.ReadInt32();
            P2 = byteBlock.ReadInt32();
        }
    }
}
```

</div>
</details>

:::caution 注意

当使用包模式序列化时，类型兼容性将跟随包类型一致。一般来说，新增属性是允许的，但是修改或移除属性是不允许的。

:::  

## 八、性能测试

### 8.1 简单测试

**待测试类型**

```csharp showLineNumbers
[Serializable]
public class MyPackPerson
{
    public int Age { get; set; }
    public string Name { get; set; }
}
```

**结果**

以下测试是执行10000次序列化和反序列的结果。

![](@site/static/img/docs/fastbinaryformatter-1.png)

### 8.2 复杂类型测试

**待测试类**

```csharp showLineNumbers
 [Serializable]
public class Student
{
    public int P1 { get; set; }
    public string P2 { get; set; }
    public long P3 { get; set; }
    public byte P4 { get; set; }
    public DateTime P5 { get; set; }
    public double P6 { get; set; }
    public byte[] P7 { get; set; }

    public List<int> List1 { get; set; }
    public List<string> List2 { get; set; }
    public List<byte[]> List3 { get; set; }

    public Dictionary<int, int> Dic1 { get; set; }
    public Dictionary<int, string> Dic2 { get; set; }
    public Dictionary<string, string> Dic3 { get; set; }
    public Dictionary<int, Arg> Dic4 { get; set; }
}

[Serializable]
public class Arg
{
    public Arg(int myProperty)
    {
        this.MyProperty = myProperty;
    }

    public Arg()
    {
        Person person = new Person();
        person.Name = "张三";
        person.Age = 18;
    }

    public int MyProperty { get; set; }
}
[Serializable]
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
```

**赋值**

```csharp showLineNumbers
Student student = new Student();
student.P1 = 10;
student.P2 = "若汝棋茗";
student.P3 = 100;
student.P4 = 0;
student.P5 = DateTime.Now;
student.P6 = 10;
student.P7 = new byte[1024 * 64];

Random random = new Random();
random.NextBytes(student.P7);

student.List1 = new List<int>();
student.List1.Add(1);
student.List1.Add(2);
student.List1.Add(3);

student.List2 = new List<string>();
student.List2.Add("1");
student.List2.Add("2");
student.List2.Add("3");

student.List3 = new List<byte[]>();
student.List3.Add(new byte[1024]);
student.List3.Add(new byte[1024]);
student.List3.Add(new byte[1024]);

student.Dic1 = new Dictionary<int, int>();
student.Dic1.Add(1, 1);
student.Dic1.Add(2, 2);
student.Dic1.Add(3, 3);

student.Dic2 = new Dictionary<int, string>();
student.Dic2.Add(1, "1");
student.Dic2.Add(2, "2");
student.Dic2.Add(3, "3");

student.Dic3 = new Dictionary<string, string>();
student.Dic3.Add("1", "1");
student.Dic3.Add("2", "2");
student.Dic3.Add("3", "3");

student.Dic4 = new Dictionary<int, Arg>();
student.Dic4.Add(1, new Arg(1));
student.Dic4.Add(2, new Arg(2));
student.Dic4.Add(3, new Arg(3));
```

**结果**

Fast的效率比System自带的，快了近7倍，比System.Text.Json快了4倍多，比NewtonsoftJson快了近30倍。

![](@site/static/img/docs/fastbinaryformatter-2.png)
