MemoryPack:C# Unity极致性能二进制序列化


MemoryPack

适用于 C# 和 Unity 的零编码极致性能二进制序列化器。

图像
与System.Text.Json、protobuf-net、MessagePack for C#、Orleans.Serialization进行了比较。由 .NET 7 / Ryzen 9 5950X 机器测量。这些序列化器具有IBufferWriter方法、序列化使用ArrayBufferWriter和重用,以避免测量缓冲区复制。

对于标准对象,MemoryPack 比其他二进制序列化器快 10 倍,快 2 至 5 倍。对于结构数组,MemoryPack 的功能更强大,速度比其他序列化器快 50 至 200 倍。

MemoryPack 是我的第四个序列化器,之前我创建过一些著名的序列化器,零格式化程序,通用格式, MessagePack for C#。 MemoryPack 速度快的原因在于它特定于 C#、针对 C# 优化的二进制格式以及根据我过去的经验精心调整的实现。它也是一个全新的设计,利用了 .NET 7 和 C# 11 以及增量源生成器(.NET Standard 2.1(.NET 5、6)并且还支持 Unity)。

其他序列化器执行许多编码操作,例如 VarInt 编码、标签、字符串等。MemoryPack 格式使用零编码设计,尽可能多地复制 C# 内存。零编码类似于 FlatBuffers,但它不需要特殊类型,MemoryPack 的序列化目标是 POCO。

除了性能之外,MemoryPack 还具有以下功能。

  1. 支持现代 I/O API(IBufferWriter、ReadOnlySpan、ReadOnlySequence)
  2. 基于本机 AOT 友好的源生成器的代码生成,无需动态代码生成 (IL.Emit)
  3. 无反射的非泛型 API
  4. 反序列化到现有实例
  5. 多态(联合)序列化
  6. 有限版本容忍(快速/默认)和完整版本容忍支持
  7. 循环引用序列化
  8. 基于 PipeWriter/Reader 的流序列化
  9. TypeScript 代码生成和 ASP.NET Core Formatter
    Unity (2021.3) 通过 .NET 源生成器支持 IL2CPP

安装

此库通过 NuGet 分发。为获得最佳性能,建议使用.NET 7。最低要求是.NET Standard 2.1。

PM> 安装包MemoryPack

另外,代码编辑器需要 Roslyn 4.3.1 支持,例如 Visual Studio 2022 版本 17.3、.NET SDK 6.0.401。有关详细信息,请参阅Roslyn 版本支持文档。

对于 Unity,要求和安装过程完全不同。有关详细信息,请参阅Unity部分。

快速开始

定义要序列化的结构或类,并用[MemoryPackable]属性和partial关键字对其进行注释。

using MemoryPack;

[MemoryPackable]
public partial class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
}

序列化代码由实现接口的 C# 源生成器功能生成。在 Visual Studio 中,您可以使用类名的IMemoryPackable快捷方式并选择 来检查生成的代码。Ctrl+K, R*.MemoryPackFormatter.g.cs

调用MemoryPackSerializer.Serialize/Deserialize来序列化/反序列化对象实例。

var v = new Person { Age = 40, Name = "John" };

var bin = MemoryPackSerializer.Serialize(v);
var val = MemoryPackSerializer.Deserialize<Person>(bin);

Serialize方法支持返回类型,byte[]并且可以序列化为IBufferWriter或Stream。Deserialize方法支持ReadOnlySpan、ReadOnlySequence和Stream。此外还有非泛型版本。

内置支持类型

这些类型可以默认序列化:

  1. .NET原语(byte、、、、、等)intboolchardouble
  2. 非托管类型(任何,任何不包含引用类型的enum用户定义)struct
  3. string,,,,,,,,decimal​Half​Int128​UInt128​Guid​Rune​BigInteger
  4. TimeSpan,,,,,, DateTime​DateTimeOffset​TimeOnly​DateOnly TimeZoneInfo
  5. Complex,,,,,,,Plane​Quaternion Matrix3x2​Matrix4x4​Vector2​Vector3​Vector4
  6. Uri,,,,,,Version​StringBuilder​Type​BitArray​CultureInfo
  7. T[],,,,,,,,T[,]​T[,]​T[,]​Memory<>​ReadOnlyMemory<>​ArraySegment<>​ReadOnlySequence<>
  8. Nullable<>,,,,,Lazy<>​KeyValuePair<,>​Tuple<,…>​ValueTuple<,…>
  9. List<>,,,,,,,LinkedList<>​Queue<>​Stack<>​HashSet<>​SortedSet<>​PriorityQueue<,>
  10. Dictionary<,>,,,,SortedList<,>​SortedDictionary<,>​ ReadOnlyDictionary<,>
  11. Collection<>,,,,ReadOnlyCollection<>​ObservableCollection<>​ReadOnlyObservableCollection<>
  12. IEnumerable<>,,,,,,ICollection<>​IList<>​IReadOnlyCollection<>​IReadOnlyList<>​ISet<>
  13. IDictionary<,>,,,,,IReadOnlyDictionary<,>​ILookup<,>​IGrouping<,>​
  14. ConcurrentBag<>,,,,,ConcurrentQueue<>​ConcurrentStack<>​ConcurrentDictionary<,>​BlockingCollection<>
  15. 不可变集合(ImmutableList<>等)和接口(IImmutableList<>等)

定义[MemoryPackable] class/ struct/ record/record struct

[MemoryPackable]可以注释任何class、struct、record和。如果类型是或不包含引用类型(C# 非托管类型record struct) ,则不使用任何其他注释(忽略、包含、构造函数、回调),直接从内存进行序列化/反序列化。interfacestructrecord struct

否则,默认情况下,[MemoryPackable]序列化公共实例属性或字段。您可以使用[MemoryPackIgnore]删除序列化目标,[MemoryPackInclude]将私有成员提升为序列化目标。

[MemoryPackable]
public partial class Sample
{
    // these types are serialized by default
    public int PublicField;
    public readonly int PublicReadOnlyField;
    public int PublicProperty { get; set; }
    public int PrivateSetPublicProperty { get; private set; }
    public int ReadOnlyPublicProperty { get; }
    public int InitProperty { get; init; }
    public required int RequiredInitProperty { get; init; }

    // these types are not serialized by default
    int privateProperty { get; set; }
    int privateField;
    readonly int privateReadOnlyField;

    // use [MemoryPackIgnore] to remove target of a public member
    [MemoryPackIgnore]
    public int PublicProperty2 => PublicProperty + PublicField;

    // use [MemoryPackInclude] to promote a private member to serialization target
    [MemoryPackInclude]
    int privateField2;
    [MemoryPackInclude]
    int privateProperty2 { get; set; }
}

MemoryPack的代码生成器会将有关哪些成员被序列化到该部分的信息添加到其中。可以通过使用 Intellisense 将鼠标悬停在类型上来查看这些信息。
在这里插入图片描述

所有成员都必须是 memorypack-serializable,否则代码生成器将发出错误。

图像

MemoryPack 有 35 条诊断规则(MEMPACK001至MEMPACK035)可以轻松定义。

如果目标类型在外部定义 MemoryPack 序列化并注册,则用于静默诊断。

[MemoryPackAllowSerialize]

[MemoryPackable]
public partial class Sample2
{
    [MemoryPackAllowSerialize]
    public NotSerializableType? NotSerializableProperty { get; set; }
}

成员顺序很重要,MemoryPack 不会序列化成员名称或其他信息,而是按照声明的顺序序列化字段。如果类型是继承的,则按父级 → 子级的顺序进行序列化。成员的顺序在反序列化时不会改变。有关架构演变,请参阅版本容忍部分。

[MemoryPackable(SerializeLayout.Explicit)]默认顺序是连续的,但您可以使用和选择明确的布局[MemoryPackOrder()]。

// serialize Prop0 -> Prop1
[MemoryPackable(SerializeLayout.Explicit)]
public partial class SampleExplicitOrder
{
    [MemoryPackOrder(1)]
    public int Prop1 { get; set; }
    [MemoryPackOrder(0)]
    public int Prop0 { get; set; }
}

构造函数选择

MemoryPack 支持带参数和无参数的构造函数。构造函数的选择遵循以下规则。(适用于类和结构)。

1、如果有[MemoryPackConstructor],请使用它。
2、如果没有显式的构造函数(包括私有的),请使用无参数的构造函数。
3、如果有一个无参数/参数化构造函数(包括私有的),请使用它。
4、如果有多个构造函数,则[MemoryPackConstructor]必须将该属性应用于所需的构造函数(生成器不会自动选择一个),否则生成器将发出错误。
5、如果使用参数化构造函数,所有参数名称必须与相应的成员名称匹配(不区分大小写)。

[MemoryPackable]
public partial class Person
{
    public readonly int Age;
    public readonly string Name;

    // You can use a parameterized constructor - parameter names must match corresponding members name (case-insensitive)
    public Person(int age, string name)
    {
        this.Age = age;
        this.Name = name;
    }
}

// also supports record primary constructor
[MemoryPackable]
public partial record Person2(int Age, string Name);

public partial class Person3
{
    public int Age { get; set; }
    public string Name { get; set; }

    public Person3()
    {
    }

    // If there are multiple constructors, then [MemoryPackConstructor] should be used
    [MemoryPackConstructor]
    public Person3(int age, string name)
    {
        this.Age = age;
        this.Name = name;
    }
}

序列化回调

[MemoryPackOnSerializing]在序列化/反序列化时,MemoryPack 可以使用、[MemoryPackOnSerialized]、[MemoryPackOnDeserializing]、属性调用前/后事件[MemoryPackOnDeserialized]。它可以注释静态和实例(非静态)方法以及公共和私有方法。

[MemoryPackable]
public partial class MethodCallSample
{
    // method call order is static -> instance
    [MemoryPackOnSerializing]
    public static void OnSerializing1()
    {
        Console.WriteLine(nameof(OnSerializing1));
    }

    // also allows private method
    [MemoryPackOnSerializing]
    void OnSerializing2()
    {
        Console.WriteLine(nameof(OnSerializing2));
    }

    // serializing -> /* serialize */ -> serialized
    [MemoryPackOnSerialized]
    static void OnSerialized1()
    {
        Console.WriteLine(nameof(OnSerialized1));
    }

    [MemoryPackOnSerialized]
    public void OnSerialized2()
    {
        Console.WriteLine(nameof(OnSerialized2));
    }

    [MemoryPackOnDeserializing]
    public static void OnDeserializing1()
    {
        Console.WriteLine(nameof(OnDeserializing1));
    }

    // Note: instance method with MemoryPackOnDeserializing, that not called if instance is not passed by `ref`
    [MemoryPackOnDeserializing]
    public void OnDeserializing2()
    {
        Console.WriteLine(nameof(OnDeserializing2));
    }

    [MemoryPackOnDeserialized]
    public static void OnDeserialized1()
    {
        Console.WriteLine(nameof(OnDeserialized1));
    }

    [MemoryPackOnDeserialized]
    public void OnDeserialized2()
    {
        Console.WriteLine(nameof(OnDeserialized2));
    }
}

回调允许无参数方法和ref reader/writer, ref T value方法。例如,ref 回调可以在序列化过程之前写入/读取自定义标头。

[MemoryPackable]
public partial class EmitIdData
{
    public int MyProperty { get; set; }

    [MemoryPackOnSerializing]
    static void WriteId<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, ref EmitIdData? value)
        where TBufferWriter : IBufferWriter<byte> // .NET Standard 2.1, use where TBufferWriter : class, IBufferWriter<byte>
    {
        writer.WriteUnmanaged(Guid.NewGuid()); // emit GUID in header.
    }

    [MemoryPackOnDeserializing]
    static void ReadId(ref MemoryPackReader reader, ref EmitIdData? value)
    {
        // read custom header before deserialize
        var guid = reader.ReadUnmanaged<Guid>();
        Console.WriteLine(guid);
    }
}

如果将值设置为ref value,则可以更改用于序列化/反序列化的值。例如从 ServiceProvider 实例化。

/

/ before using this formatter, set ServiceProvider
// var options = MemoryPackSerializerOptions.Default with { ServiceProvider = provider };
// MemoryPackSerializer.Deserialize(value, options);

[MemoryPackable]
public partial class InstantiateFromServiceProvider
{
    static IServiceProvider serviceProvider = default!;

    public int MyProperty { get; private set; }

    [MemoryPackOnDeserializing]
    static void OnDeserializing(ref MemoryPackReader reader, ref InstantiateFromServiceProvider value)
    {
        if (value != null) return;
        value = reader.Options.ServiceProvider!.GetRequiredService<InstantiateFromServiceProvider>();
    }
}

定义自定义集合

默认情况下,注释[MemoryPackObject]类型会尝试序列化其成员。但是,如果类型是集合(ICollection<>、ISet<>、IDictionary<,>),则使用GenerateType.Collection可以正确序列化它。

[MemoryPackable(GenerateType.Collection)]
public partial class MyList<T> : List<T>
{
}

[MemoryPackable(GenerateType.Collection)]
public partial class MyStringDictionary<TValue> : Dictionary<string, TValue>
{

}

静态构造函数

MemoryPackable 类无法定义静态构造函数,因为生成的部分类会使用它。相反,您可以定义一个static partial void StaticConstructor()来做同样的事情。

[MemoryPackable]
public partial class CctorSample
{
    static partial void StaticConstructor()
    {
    }
}

多态(联合)

MemoryPack 支持序列化接口和抽象类对象以实现多态序列化。在 MemoryPack 中,此功能称为 Union。只有接口和抽象类才允许使用[MemoryPackUnion]属性进行注释。需要唯一的 union 标签。

// Annotate [MemoryPackable] and inheritance types with [MemoryPackUnion]
// Union also supports abstract class
[MemoryPackable]
[MemoryPackUnion(0, typeof(FooClass))]
[MemoryPackUnion(1, typeof(BarClass))]
public partial interface IUnionSample
{
}

[MemoryPackable]
public partial class FooClass : IUnionSample
{
    public int XYZ { get; set; }
}

[MemoryPackable]
public partial class BarClass : IUnionSample
{
    public string? OPQ { get; set; }
}
// ---

IUnionSample data = new FooClass() { XYZ = 999 };

// Serialize as interface type.
var bin = MemoryPackSerializer.Serialize(data);

// Deserialize as interface type.
var reData = MemoryPackSerializer.Deserialize<IUnionSample>(bin);

switch (reData)
{
    case FooClass x:
        Console.WriteLine(x.XYZ);
        break;
    case BarClass x:
        Console.WriteLine(x.OPQ);
        break;
    default:
        break;
}

tag允许0~ 65535,对于小于 的情况尤其有效250。

如果接口和派生类型位于不同的程序集中,则可以改用。格式化程序的生成方式是通过在 C# 9.0 及更高版本中MemoryPackUnionFormatterAttribute自动注册的方式。ModuleInitializer

请注意,ModuleInitializerUnity 不支持,因此必须手动注册格式化程序。要注册联合格式化程序,请{name of your union formatter}Initializer.RegisterFormatter()在启动时手动调用。例如UnionSampleFormatterInitializer.RegisterFormatter()。

// AssemblyA
[MemoryPackable(GenerateType.NoGenerate)]
public partial interface IUnionSample
{
}

// AssemblyB define definition outside of target type
[MemoryPackUnionFormatter(typeof(IUnionSample))]
[MemoryPackUnion(0, typeof(FooClass))]
[MemoryPackUnion(1, typeof(BarClass))]
public partial class UnionSampleFormatter
{
}

可以通过 在代码中组装 Union DynamicUnionFormatter。

var formatter = new DynamicUnionFormatter<IFooBarBaz>(new[]
{
    (0, typeof(Foo)),
    (1, typeof(Bar)),
    (2, typeof(Baz))
});

MemoryPackFormatterProvider.Register(formatter);

序列化 API

Serialize有三个重载。

// Non generic API also available, these version is first argument is Type and value is object?
byte[] Serialize<T>(in T? value, MemoryPackSerializerOptions? options = default)
void Serialize<T, TBufferWriter>(in TBufferWriter bufferWriter, in T? value, MemoryPackSerializerOptions? options = default)
async ValueTask SerializeAsync<T>(Stream stream, T? value, MemoryPackSerializerOptions? options = default, CancellationToken cancellationToken = default)

为了提高性能,建议使用 API BufferWriter。这会直接序列化到缓冲区中。它可以应用于ASP .NET Core 等PipeWriter中System.IO.Pipelines。BodyWriter

如果byte[]需要(例如RedisValue在StackExchange.Redis中),返回byte[]API 很简单并且几乎一样快。

请注意,SerializeAsyncforStream仅对 Flush 是异步的;它将所有内容序列化一次放入 MemoryPack 的内部池缓冲区中,然后使用 进行写入WriteAsync。因此,BufferWriter分离和控制缓冲区和刷新的重载更好。

如果要执行完整的流式写入,请参阅流序列化部分。

MemoryPackSerializer选项

MemoryPackSerializerOptions配置字符串是否序列化为 UTF16 或 UTF8。可以通过传递MemoryPackSerializerOptions.Utf8UTF8 编码、MemoryPackSerializerOptions.Utf16UTF16 编码或MemoryPackSerializerOptions.Default默认为 UTF8 来配置。传递 null 或使用默认参数会导致 UTF8 编码。

由于 C# 的内部字符串表示形式是 UTF16,因此 UTF16 的性能更佳。但是,有效负载往往更大;在 UTF8 中,ASCII 字符串为一个字节,而在 UTF16 中则为两个字节。由于此有效负载的大小差异很大,因此默认设置为 UTF8。

如果数据是非 ASCII 的(例如日语,可能超过 3 个字节,而 UTF8 更大),或者您必须单独压缩它,则 UTF16 可能会提供更好的结果。

虽然序列化时可以选择UTF8或UTF16,但反序列化时无需指定。它将被自动检测并正常反序列化。

另外,您可以从选项中获取/设置。从序列化过程中IServiceProvider? ServiceProvider { get; init; }获取 DI 对象(例如)很有用(具有 .Options 属性)。ILoggerMemoryPackReader/MemoryPackWriter

反序列化 API

Deserialize有ReadOnlySpan和ReadOnlySequence,Stream超载和ref支持。

T? Deserialize<T>(ReadOnlySpan<byte> buffer)
int Deserialize<T>(ReadOnlySpan<byte> buffer, ref T? value)
T? Deserialize<T>(in ReadOnlySequence<byte> buffer)
int Deserialize<T>(in ReadOnlySequence<byte> buffer, ref T? value)
async ValueTask<T?> DeserializeAsync<T>(Stream stream)

ref重载会覆盖现有实例,有关详细信息,请参阅覆盖部分。

DeserializeAsync(Stream)不是一个完整的流式读取操作,它首先读入 MemoryPack 的内部池直到流的末尾,然后反序列化。

如果要执行完整的流式读取操作,请参阅流序列化部分。

覆盖

为了减少分配,MemoryPack 支持反序列化为现有实例,并覆盖它。这可以与Deserialize(ref T? value)重载一起使用。

var person = new Person();
var bin = MemoryPackSerializer.Serialize(person);

// overwrite data to existing instance.
MemoryPackSerializer.Deserialize(bin, ref person);

MemoryPack 会尝试尽可能地覆盖,但如果以下条件不匹配,它将创建一个新实例(如正常的反序列化一样)。

1、ref 值(包括对象图中的成员)为空,设置新实例
2、只允许无参数构造函数,如果使用带参数构造函数,则创建新实例
3、如果值为T[],则仅当长度相同时才重用,否则创建新实例
4、如果值是具有.Clear()方法(List<>,,,,,,,,,,,,,,,,)的集合Stack<>,Queue<>则调用Clear() 并重用它,LinkedList<>否则创建新实例HashSet<>PriorityQueue<,>ObservableCollectionCollectionConcurrentQueue<>ConcurrentStack<>ConcurrentBag<>Dictionary<,>SortedDictionary<,>SortedList<,>ConcurrentDictionary<,>

版本兼容

在默认情况下(GenerateType.Object),MemoryPack 支持有限的模式演变。

1、非托管结构无法再更改
2、可以添加成员,但不能删除
3、可以更改会员名称
4、无法更改会员顺序
5、无法更改会员类型

[MemoryPackable]
public partial class VersionCheck
{
    public int Prop1 { get; set; }
    public long Prop2 { get; set; }
}

// Add is OK.
[MemoryPackable]
public partial class VersionCheck
{
    public int Prop1 { get; set; }
    public long Prop2 { get; set; }
    public int? AddedProp { get; set; }
}

// Remove is NG.
[MemoryPackable]
public partial class VersionCheck
{
    // public int Prop1 { get; set; }
    public long Prop2 { get; set; }
}

// Change order is NG.
[MemoryPackable]
public partial class VersionCheck
{
    public long Prop2 { get; set; }
    public int Prop1 { get; set; }
}

在用例中,存储旧数据(到文件、到 redis 等…)并读取新模式始终是可以的。在 RPC 场景中,模式同时存在于客户端和服务器端,客户端必须在服务器之前更新。更新后的客户端可以毫无问题地连接到旧服务器,但旧客户端无法连接到新服务器。

默认情况下,当将旧数据读取到新架构时,任何不在数据端的成员都将使用文字初始化default。如果您想避免这种情况并使用字段/属性的初始值,则可以使用[SuppressDefaultInitialization]。

[MemoryPackable]
public partial class DefaultValue
{
    public string Prop1 { get; set; }

    [SuppressDefaultInitialization]
    public int Prop2 { get; set; } = 111; // < if old data is missing, set `111`.
    
    public int Prop3 { get; set; } = 222; // < if old data is missing, set `default`.
}

[SuppressDefaultInitialization]有以下限制:

1、不能与 readonly、init-only 和 required 修饰符一起使用。
下一个序列化信息部分显示如何检查模式更改(例如通过 CI),以防止事故。

当使用时GenerateType.VersionTolerant,它支持完整的版本容忍。

1、非托管结构无法再改变
2、所有成员必须[MemoryPackOrder]明确添加(注释除外SerializeLayout.Sequential)
3、会员可以添加,可以删除但不能重复使用订单(可以使用丢失的订单)
4、可以更改会员名称
5、无法更改会员顺序
6、无法更改会员类型

// Ok to serialize/deserialize both 
// VersionTolerantObject1 -> VersionTolerantObject2 and 
// VersionTolerantObject2 -> VersionTolerantObject1

[MemoryPackable(GenerateType.VersionTolerant)]
public partial class VersionTolerantObject1
{
    [MemoryPackOrder(0)]
    public int MyProperty0 { get; set; } = default;

    [MemoryPackOrder(1)]
    public long MyProperty1 { get; set; } = default;

    [MemoryPackOrder(2)]
    public short MyProperty2 { get; set; } = default;
}

[MemoryPackable(GenerateType.VersionTolerant)]
public partial class VersionTolerantObject2
{
    [MemoryPackOrder(0)]
    public int MyProperty0 { get; set; } = default;

    // deleted
    //[MemoryPackOrder(1)]
    //public long MyProperty1 { get; set; } = default;

    [MemoryPackOrder(2)]
    public short MyProperty2 { get; set; } = default;

    // added
    [MemoryPackOrder(3)]
    public short MyProperty3 { get; set; } = default;
}
// If set SerializeLayout.Sequential explicitly, allows automatically order.
// But it can not remove any member for versoin-tolerant.
[MemoryPackable(GenerateType.VersionTolerant, SerializeLayout.Sequential)]
public partial class VersionTolerantObject3
{
    public int MyProperty0 { get; set; } = default;
    public long MyProperty1 { get; set; } = default;
    public short MyProperty2 { get; set; } = default;
}

GenerateType.VersionTolerant比序列化慢GenerateType.Object。此外,有效载荷大小会略大一些。

序列化信息

您可以在类型中检查 IntelliSense 哪些成员被序列化。有一个选项可以在编译时将该信息写入文件。设置MemoryPackGenerator_SerializationInfoOutputDirectory如下。

<!-- output memorypack serialization info to directory -->
<ItemGroup>
    <CompilerVisibleProperty Include="MemoryPackGenerator_SerializationInfoOutputDirectory" />
</ItemGroup>
<PropertyGroup>
    <MemoryPackGenerator_SerializationInfoOutputDirectory>$(MSBuildProjectDirectory)\MemoryPackLogs</MemoryPackGenerator_SerializationInfoOutputDirectory>
</PropertyGroup>

以下信息写入文件。

图像

如果类型是非托管的,则显示unmanaged在类型名称之前。

unmanaged FooStruct
---
int x
int y

通过检查此文件中的差异,可以防止危险的架构更改。例如,您可能希望使用 CI 检测以下规则

1、修改非托管类型
2、会员订单变更
3、删除成员

循环引用

MemoryPack 还支持循环引用。这允许按原样序列化树对象。

// to enable circular-reference, use GenerateType.CircularReference
[MemoryPackable(GenerateType.CircularReference)]
public partial class Node
{
    [MemoryPackOrder(0)]
    public Node? Parent { get; set; }
    [MemoryPackOrder(1)]
    public Node[]? Children { get; set; }
}

例如,System.Text.Json 的 retain-references代码将变成这样。

// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/preserve-references?pivots=dotnet-7-0
Employee tyler = new()
{
    Name = "Tyler Stein"
};

Employee adrian = new()
{
    Name = "Adrian King"
};

tyler.DirectReports = new List<Employee> { adrian };
adrian.Manager = tyler;

var bin = MemoryPackSerializer.Serialize(tyler);
Employee? tylerDeserialized = MemoryPackSerializer.Deserialize<Employee>(bin);

Console.WriteLine(tylerDeserialized?.DirectReports?[0].Manager == tylerDeserialized); // true

[MemoryPackable(GenerateType.CircularReference)]
public partial class Employee
{
    [MemoryPackOrder(0)]
    public string? Name { get; set; }
    [MemoryPackOrder(1)]
    public Employee? Manager { get; set; }
    [MemoryPackOrder(2)]
    public List<Employee>? DirectReports { get; set; }
}

GenerateType.CircularReference具有与版本容忍相同的特性。但是,作为附加约束,只允许使用无参数构造函数。此外,仅对标有 的对象进行对象引用跟踪。GenerateType.CircularReference如果您想跟踪任何其他对象,请将其包装起来。

自定义格式化程序

如果实现MemoryPackCustomFormatterAttribute或MemoryPackCustomFormatterAttribute<TFormatter, T>(性能更高,但更复杂),您可以配置使用自定义格式化程序来 MemoryPackObject 的成员。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public abstract class MemoryPackCustomFormatterAttribute<T> : Attribute
{
    public abstract IMemoryPackFormatter<T> GetFormatter();
}

MemoryPack提供以下格式属性:Utf8StringFormatterAttribute,,,,,,,,,,。Utf16StringFormatterAttribute
​​​InternStringFormatterAttribute OrdinalIgnoreCaseStringDictionaryFormatterAttribute BitPackFormatterAttribute
BrotliFormatter
BrotliStringFormatterBrotliFormatter
MemoryPoolFormatter
ReadOnlyMemoryPoolFormatter

[MemoryPackable]
public partial class Sample
{
    // serialize this member as UTF16 String, it is performant than UTF8 but in ASCII, size is larger(but non ASCII, sometimes smaller).
    [Utf16StringFormatter]
    public string? Text { get; set; }

    // In deserialize, Dictionary is initialized with StringComparer.OrdinalIgnoreCase.
    [OrdinalIgnoreCaseStringDictionaryFormatter<int>]
    public Dictionary<string, int>? Ids { get; set; }
    
    // In deserialize time, all string is interned(see: String.Intern). If similar values come repeatedly, it saves memory.
    [InternStringFormatter]
    public string? Flag { get; set; }
}

为了配置集合/字典的相等性比较器,所有内置格式化程序都具有比较器构造函数重载。您可以轻松创建自定义相等性比较器格式化程序。

public sealed class OrdinalIgnoreCaseStringDictionaryFormatter<TValue> : MemoryPackCustomFormatterAttribute<Dictionary<string, TValue?>>
{
    static readonly DictionaryFormatter<string, TValue?> formatter = new DictionaryFormatter<string, TValue?>(StringComparer.OrdinalIgnoreCase);

    public override IMemoryPackFormatter<Dictionary<string, TValue?>> GetFormatter()
    {
        return formatter;
    }
}

BitPackFormatter仅压缩bool[]类型。bool[]通常序列化为每个布尔值 1 个字节,但BitPackFormatter序列化bool[]时会将BitArray每个布尔值存储为 1 位。使用BitPackFormatter,8 个布尔值变为 1 个字节,而它们通常是 8 个字节,因此大小缩小了 8 倍。

[MemoryPackable]
public partial class Sample
{
    public int Id { get; set; }

    [BitPackFormatter]
    public bool[]? Data { get; set; }
}

BrotliFormatter用于byte[],例如,您可以使用 Brotli 压缩大负载。

[MemoryPackable]
public partial class Sample
{
    public int Id { get; set; }

    [BrotliFormatter]
    public byte[]? Payload { get; set; }
}

BrotliStringFormatter用于string,通过 Brotli 序列化压缩字符串(UTF16)。

[MemoryPackable]
public partial class Sample
{
    public int Id { get; set; }

    [BrotliStringFormatter]
    public string? LargeText { get; set; }
}

BrotliFormatter适用于任何类型,由 Brotli 压缩的序列化数据。如果类型为byte[]或string,则应使用BrotliFormatter或BrotliStringFormatter以提高性能。

[MemoryPackable]
public partial class Sample
{
    public int Id { get; set; }

    [BrotliFormatter<ChildType>]
    public ChildType? Child { get; set; }
}

反序列化数组池
为了反序列化大型数组(任何T),MemoryPack 提供了多种有效的池化方法。最有效的方法是使用#Overwrite函数。特别List是总是被重用。

[MemoryPackable]
public partial class ListBytesSample
{
    public int Id { get; set; }
    public List<byte> Payload { get; set; }
}

// ----

// List<byte> is reused, no allocation in deserialize.
MemoryPackSerializer.Deserialize<ListBytesSample>(bin, ref reuseObject);

// for efficient operation, you can get Span<T> by CollectionsMarshal
var span = CollectionsMarshal.AsSpan(value.Payload);

一种方便的方法是在反序列化时反序列化为 ArrayPool。MemoryPack 提供MemoryPoolFormatter和ReadOnlyMemoryPoolFormatter。

[MemoryPackable]
public partial class PoolModelSample : IDisposable
{
    public int Id { get; }

    [MemoryPoolFormatter<byte>]
    public Memory<byte> Payload { get; private set; }

    public PoolModelSample(int id, Memory<byte> payload)
    {
        Id = id;
        Payload = payload;
    }

    // You must write the return code yourself, here is snippet.

    bool usePool;

    [MemoryPackOnDeserialized]
    void OnDeserialized()
    {
        usePool = true;
    }

    public void Dispose()
    {
        if (!usePool) return;

        Return(Payload); Payload = default;
    }

    static void Return<T>(Memory<T> memory) => Return((ReadOnlyMemory<T>)memory);

    static void Return<T>(ReadOnlyMemory<T> memory)
    {
        if (MemoryMarshal.TryGetArray(memory, out var segment) && segment.Array is { Length: > 0 })
        {
            ArrayPool<T>.Shared.Return(segment.Array, clearArray: RuntimeHelpers.IsReferenceOrContainsReferences<T>());
        }
    }
}

// ---

using(var value = MemoryPackSerializer.Deserialize<PoolModelSample>(bin))
{
    // do anything...
}   // return to ArrayPool

表现

请参阅博客文章如何使用 .NET 7 / C# 11 制作最快的 .NET 序列化器(MemoryPack 案例)

有效载荷大小和压缩

有效负载大小取决于目标值;与 JSON 不同,它没有键并且是二进制格式,因此有效负载大小可能比 JSON 小。

对于那些使用 varint 编码的,例如 MessagePack 和 Protobuf,如果大量使用 int,MemoryPack 往往会更大(在 MemoryPack 中,由于固定大小编码,int 始终为 4 个字节,而 MessagePack 为 1~5 个字节)。

在 MemoryPack 中,float 和 double 分别为 4 个字节和 8 个字节,但在 MessagePack 中分别为 5 个字节和 9 个字节。因此,对于 Vector3 (float, float, float) 数组,MemoryPack 较小。

字符串默认是UTF8,和其他序列化器类似,但如果选择UTF16选项,性质就不一样了。

无论如何,如果有效载荷很大,则应考虑压缩。建议使用 LZ4、ZStandard 和 Brotli。

压缩

MemoryPack通过BrotliEncoder和BrotliDecoder为Brotli压缩提供了有效的帮助程序。MemoryPack提供了针对 MemoryPack 内部行为优化的压缩/解压缩。BrotliCompressorBrotliDecompressor

using MemoryPack.Compression;

// Compression(require using)
using var compressor = new BrotliCompressor();
MemoryPackSerializer.Serialize(compressor, value);

// Get compressed byte[]
var compressedBytes = compressor.ToArray();

// Or write to other IBufferWriter<byte>(for example PipeWriter)
compressor.CopyTo(response.BodyWriter);
using MemoryPack.Compression;

// Decompression(require using)
using var decompressor = new BrotliDecompressor();

// Get decompressed ReadOnlySequence<byte> from ReadOnlySpan<byte> or ReadOnlySequence<byte>
var decompressedBuffer = decompressor.Decompress(buffer);

var value = MemoryPackSerializer.Deserialize<T>(decompressedBuffer);

和BrotliCompressor都是BrotliDecompressor结构体,不会在堆上分配内存。两者都将压缩或解压缩的数据存储在内部内存池中,以供序列化/反序列化使用。因此,需要释放内存池,不要忘记使用using。

压缩级别非常重要。默认设置为 quality-1 (CompressionLevel.Fastest),与 .NET 默认值 (CompressionLevel.Optimal, quality-4) 不同。

最快(质量-1)将接近LZ4的速度,但 4 要慢得多。这在序列化器使用场景中被确定为至关重要。使用标准时要小心BrotliStream(质量-4 是默认值)。无论如何,压缩/解压缩速度和大小将导致不同数据的结果大不相同。请准备好应用程序要处理的数据并自行测试。

请注意,MemoryPack 的未压缩版本和 Brotli 的附加压缩版本之间的速度损失有几倍。

Brotli 也支持自定义格式化程序。BrotliFormatter可以压缩特定成员。

[MemoryPackable]
public partial class Sample
{
    public int Id { get; set; }

    [BrotliFormatter]
    public byte[]? Payload { get; set; }
}

序列化外部类型

如果要序列化外部类型,可以创建自定义格式化程序并将其注册到提供程序,详情请参阅Formatter/Provider API。但是,创建自定义格式化程序很困难。因此,我们建议创建包装器类型。例如,如果要序列化名为 的外部类型AnimationCurve。

// Keyframe: (float time, float inTangent, float outTangent, int tangentMode, int weightedMode, float inWeight, float outWeight)
[MemoryPackable]
public readonly partial struct SerializableAnimationCurve
{
    [MemoryPackIgnore]
    public readonly AnimationCurve AnimationCurve;

    [MemoryPackInclude]
    WrapMode preWrapMode => AnimationCurve.preWrapMode;
    [MemoryPackInclude]
    WrapMode postWrapMode => AnimationCurve.postWrapMode;
    [MemoryPackInclude]
    Keyframe[] keys => AnimationCurve.keys;

    [MemoryPackConstructor]
    SerializableAnimationCurve(WrapMode preWrapMode, WrapMode postWrapMode, Keyframe[] keys)
    {
        var curve = new AnimationCurve(keys);
        curve.preWrapMode = preWrapMode;
        curve.postWrapMode = postWrapMode;
        this.AnimationCurve = curve;
    }

    public SerializableAnimationCurve(AnimationCurve animationCurve)
    {
        this.AnimationCurve = animationCurve;
    }
}

要包装的类型是公共的,但从序列化中排除(MemoryPackIgnore)。要序列化的属性是私有的,但包含在内(MemoryPackInclude)。还应准备两种构造函数模式。序列化器使用的构造函数应为私有的。

照这样下去,每次都必须进行包装,很不方便。而且 strcut 包装器不能表示 null。所以让我们创建一个自定义格式化程序。

public class AnimationCurveFormatter : MemoryPackFormatter<AnimationCurve>
{
    // Unity does not support scoped and TBufferWriter so change signature to `Serialize(ref MemoryPackWriter writer, ref AnimationCurve value)`
    public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref AnimationCurve? value)
    {
        if (value == null)
        {
            writer.WriteNullObjectHeader();
            return;
        }

        writer.WritePackable(new SerializableAnimationCurve(value));
    }

    public override void Deserialize(ref MemoryPackReader reader, scoped ref AnimationCurve? value)
    {
        if (reader.PeekIsNull())
        {
            reader.Advance(1); // skip null block
            value = null;
            return;
        }
        
        var wrapped = reader.ReadPackable<SerializableAnimationCurve>();
        value = wrapped.AnimationCurve;
    }
}

最后,在启动时注册格式化程序。

MemoryPackFormatterProvider.Register<AnimationCurve>(new AnimationCurveFormatter());

注意:Unity 的 AnimationCurve 默认可以序列化,因此 AnimationCurve 不需要此自定义格式化程序

Packages

MemoryPack 有这些包。

1、MemoryPack
2、MemoryPack.Core
3、MemoryPack.Generator
4、MemoryPack.Streaming
5、MemoryPack.AspNetCoreMvcFormatter
6、MemoryPack.UnityShims

MemoryPack是主库,它为二进制对象的高性能序列化和反序列化提供全面支持。它依赖于MemoryPack.Core核心基础库和MemoryPack.Generator代码生成。为流序列化MemoryPack.Streaming添加其他扩展。 为 ASP.NET Core 添加输入/输出格式化程序。为 .NET 和 Unity 之间的共享类型添加 Unity 垫片类型和格式化程序。MemoryPack.AspNetCoreMvcFormatterMemoryPack.UnityShims

TypeScript 和 ASP.NET Core 格式化程序

MemoryPack 支持 TypeScript 代码生成。它从 C# 生成类和序列化代码,换句话说,您可以与浏览器共享类型,而无需使用 OpenAPI、proto 等。

代码生成与 Source Generator 集成,以下选项(MemoryPackGenerator_TypeScriptOutputDirectory)设置 TypeScript 代码的输出目录。运行时代码同时输出,因此不需要额外的依赖项。

<!-- output memorypack TypeScript code to directory -->
<ItemGroup>
    <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptOutputDirectory" />
</ItemGroup>
<PropertyGroup>
    <MemoryPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\wwwroot\js\memorypack</MemoryPackGenerator_TypeScriptOutputDirectory>
</PropertyGroup>

C# MemoryPackable 类型必须用 注释[GenerateTypeScript]。

[MemoryPackable]
[GenerateTypeScript]
public partial class Person
{
    public required Guid Id { get; init; }
    public required int Age { get; init; }
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required DateTime DateOfBirth { get; init; }
    public required Gender Gender { get; init; }
    public required string[] Emails { get; init; }
}

public enum Gender
{
    Male, Female, Other
}

运行时代码和 TypeScript 类型将在目标目录中生成。

图像

生成的代码如下,其中包含用于serialize/serializeArray和deserialize/deserializeArray的简单字段和静态方法。

import { MemoryPackWriter } from "./MemoryPackWriter.js";
import { MemoryPackReader } from "./MemoryPackReader.js";
import { Gender } from "./Gender.js"; 

export class Person {
    id: string;
    age: number;
    firstName: string | null;
    lastName: string | null;
    dateOfBirth: Date;
    gender: Gender;
    emails: (string | null)[] | null;

    constructor() {
        // snip...
    }

    static serialize(value: Person | null): Uint8Array {
        // snip...
    }

    static serializeCore(writer: MemoryPackWriter, value: Person | null): void {
        // snip...
    }

    static serializeArray(value: (Person | null)[] | null): Uint8Array {
        // snip...
    }

    static serializeArrayCore(writer: MemoryPackWriter, value: (Person | null)[] | null): void {
        // snip...
    }
    static deserialize(buffer: ArrayBuffer): Person | null {
        // snip...
    }

    static deserializeCore(reader: MemoryPackReader): Person | null {
        // snip...
    }

    static deserializeArray(buffer: ArrayBuffer): (Person | null)[] | null {
        // snip...
    }

    static deserializeArrayCore(reader: MemoryPackReader): (Person | null)[] | null {
        // snip...
    }
}

您可以像下面这样使用此类型。

let person = new Person();
person.id = crypto.randomUUID();
person.age = 30;
person.firstName = "foo";
person.lastName = "bar";
person.dateOfBirth = new Date(1999, 12, 31, 0, 0, 0);
person.gender = Gender.Other;
person.emails = ["foo@bar.com", "zoo@bar.net"];

// serialize to Uint8Array
let bin = Person.serialize(person);

let blob = new Blob([bin.buffer], { type: "application/x-memorypack" })

let response = await fetch("http://localhost:5260/api",
    { method: "POST", body: blob, headers: { "Content-Type": "application/x-memorypack" } });

let buffer = await response.arrayBuffer();

// deserialize from ArrayBuffer 
let person2 = Person.deserialize(buffer);

该MemoryPack.AspNetCoreMvcFormatter包MemoryPack为 ASP.NET Core MVC 添加了输入和输出格式化程序。您可以使用以下代码将 添加到 ASP.NET Core

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new MemoryPackInputFormatter());
    // If checkContentType: true then can output multiple format(JSON/MemoryPack, etc...). default is false.
    options.OutputFormatters.Insert(0, new MemoryPackOutputFormatter(checkContentType: false));
});

如果从 HttpClient 调用,则可以设置application/x-memorypack为 content-header。

var content = new ByteArrayContent(bin)
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-memorypack");

TypeScript 类型映射

可以生成的类型有一些限制。在原语中,char和decimal不受支持。此外,不能使用 OpenGenerics 类型。
在这里插入图片描述在这里插入图片描述

[GenerateTypeScript]只能应用于类,目前不支持结构。

配置导入文件扩展名和成员名称大小写
MemoryPack 默认生成文件扩展名,如.js。import { MemoryPackWriter } from “./MemoryPackWriter.js”;如果要更改其他扩展名或为空,请使用MemoryPackGenerator_TypeScriptImportExtension进行配置。此外,成员名称会自动转换为 camelCase。如果要使用原始名称,请MemoryPackGenerator_TypeScriptConvertPropertyName使用false。

<ItemGroup>
    <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptOutputDirectory" />
    <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptImportExtension" />
    <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptConvertPropertyName" />
    <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptEnableNullableTypes" />
</ItemGroup>
<PropertyGroup>
    <MemoryPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\wwwroot\js\memorypack</MemoryPackGenerator_TypeScriptOutputDirectory>
    <!-- allows empty -->
    <MemoryPackGenerator_TypeScriptImportExtension></MemoryPackGenerator_TypeScriptImportExtension>
    <!-- default is true -->
    <MemoryPackGenerator_TypeScriptConvertPropertyName>false</MemoryPackGenerator_TypeScriptConvertPropertyName>
    <!-- default is false -->
    <MemoryPackGenerator_TypeScriptEnableNullableTypes>true</MemoryPackGenerator_TypeScriptEnableNullableTypes>
</PropertyGroup>

MemoryPackGenerator_TypeScriptEnableNullableTypes允许 C# 可空注释反映在 TypeScript 代码中。默认值为 false,使所有内容均可空。

Streaming Serialization

MemoryPack.Streaming提供MemoryPackStreamingSerializer,增加了对使用流序列化和反序列化集合的额外支持。

public static class MemoryPackStreamingSerializer
{
    public static async ValueTask SerializeAsync<T>(PipeWriter pipeWriter, int count, IEnumerable<T> source, int flushRate = 4096, CancellationToken cancellationToken = default)
    public static async ValueTask SerializeAsync<T>(Stream stream, int count, IEnumerable<T> source, int flushRate = 4096, CancellationToken cancellationToken = default)
    public static async IAsyncEnumerable<T?> DeserializeAsync<T>(PipeReader pipeReader, int bufferAtLeast = 4096, int readMinimumSize = 8192, [EnumeratorCancellation] CancellationToken cancellationToken = default)
    public static IAsyncEnumerable<T?> DeserializeAsync<T>(Stream stream, int bufferAtLeast = 4096, int readMinimumSize = 8192, CancellationToken cancellationToken = default)
}

格式化程序/提供程序 API

如果要手动实现格式化程序,请继承MemoryPackFormatter并重写Serialize和Deserialize方法。

public class SkeltonFormatter : MemoryPackFormatter<Skelton>
{
    public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref Skelton? value)
    {
        if (value == null)
        {
            writer.WriteNullObjectHeader();
            return;
        }

        // use writer method.
    }

    public override void Deserialize(ref MemoryPackReader reader, scoped ref Skelton? value)
    {
        if (!reader.TryReadObjectHeader(out var count))
        {
            value = null;
            return;
        }

        // use reader method.
    }
}

创建的格式化程序已在 中注册MemoryPackFormatterProvider。

MemoryPackFormatterProvider.Register(new SkeltonFormatter());

注意:(unmanged struct不包含引用类型)不能使用自定义格式化程序,它总是序列化本机内存布局。

MemoryPackWriter/ReaderOptionalState

初始化MemoryPackWriter/MemoryPackReader需要 OptionalState。它是 的包装器MemoryPackSerializerOptions,可以创建表单MemoryPackWriterOptionalStatePool。

// when disposed, OptionalState will return to pool.
using(var state = MemoryPackWriterOptionalStatePool.Rent(MemoryPackSerializerOptions.Default))
{
    var writer = new MemoryPackWriter<T>(ref t, state);
}

// for Reader
using (var state = MemoryPackReaderOptionalStatePool.Rent(MemoryPackSerializerOptions.Default))
{
    var reader = new MemoryPackReader(buffer, state);
}

目标框架依赖

MemoryPack 提供netstandard2.1和,net7.0但两者不兼容。例如,MemoryPackable 类型在netstandard2.1项目下并从项目中使用它net7.0,会引发如下运行时异常

未处理的异常。System.TypeLoadException:未在程序集“*”中的类型“ ”上实现虚拟静态方法“ ”。

由于 net7.0 使用静态抽象成员(Virtual static method),不支持 netstandard2.1,因此这种行为是一种规范。

.NET 7 项目不应使用 netstandard 2.1 dll。换句话说,如果应用程序是 .NET 7 项目,则所有使用 MemoryPack 的依赖项都必须支持 .NET 7。因此,如果库开发人员对 MemoryPack 有依赖,则需要配置双目标框架。

<TargetFrameworks>netstandard2.1;net7.0</TargetFrameworks>

RPC

Cysharp/MagicOnion是一个代码优先的 grpc-dotnet 框架,使用 MessagePack 而不是 protobuf。MagicOnion 现在通过MagicOnion.Serialization.MemoryPackpackage(preview) 支持 MemoryPack 作为序列化层。查看详细信息:MagicOnion#MemoryPack 支持

Unity

支持的最低 Unity 版本是2022.3.12f1。

核心MemoryPack包由 nuget 提供。Unity 中也提供。如果您想获得 Unity 内置类型支持,我们另外提供了 MemoryPack.Unity 扩展。

1、使用NuGetForUnityMemoryPack从 NuGet安装
从 NuGet 打开窗口 -> 管理 NuGet 包,搜索“MemoryPack”并按安装。
在这里插入图片描述

如果遇到版本冲突错误,请在播放器设置中禁用版本验证Edit -> Project Settings -> Player -> Scroll down and expand “Other Settings” than uncheck “Assembly Version Validation” under the “Configuration” section).

MemoryPack.Unity通过引用 git URL安装软件包
https://github.com/Cysharp/MemoryPack.git?path=src/MemoryPack.Unity/Assets/MemoryPack.Unity
在这里插入图片描述

MemoryPack 使用. .* 发布标签,因此您可以指定版本,如 #1.0.0。例如:https://github.com/Cysharp/MemoryPack.git?path=src/MemoryPack.Unity/Assets/MemoryPack.Unity#1.0.0

与.NET版本一样,代码由代码生成器(MemoryPack.Generator.dll)生成。无反射实现也在IL2CPP中提供最佳性能。

有关 Unity 和 Source Generator 的更多信息,请参阅Unity 文档。

Source Generator 也是 Unity 官方采用的com.unity.properties和com.unity.entities。换句话说,它是下一代 Unity 代码生成的标准。

您可以序列化所有非托管类型(例如Vector3,Rect等…)和一些类(AnimationCurve,,)。如果您想序列化其他 Unity 特定类型,请参阅序列化外部类型部分。GradientRectOffset

在Unity性能方面,MemoryPack比JsonUtility快3倍~10倍。

图像

如果共享代码具有 Unity 的类型(Vector2,等等),MemoryPack 会MemoryPack.UnityShims在 NuGet 中提供包。

该软件包MemoryPack.UnityShims为Unity的标准结构( Vector2、、、、、、、、、、、、、、、、、、、、、、、、、)和一些类( 、、)提供了垫片。​​​​​Vector2, Vector3, Vector4, Quaternion, Color, Bounds, Rect, Keyframe, WrapMode, Matrix4x4, GradientColorKey, GradientAlphaKey, GradientMode, Color32, LayerMask, Vector2Int, Vector3Int, RangeInt, RectInt, BoundsInt) and some classes(AnimationCurve, Gradient, RectOffset).

警告

目前,在 Unity 中使用存在以下限制

Unity 版本不支持 CustomFormatter。
如果您使用的是.NET7 或更高版本,MemoryPack 二进制格式与 Unity 不完全兼容。
此问题出现在[StructLayout(LayoutKind.Auto)]显式指定的值类型上。(struct 的默认值为LayoutKind.Sequencil。)对于此类类型,在 .NET 中序列化的二进制文件无法在 Untiy 中反序列化。同样,在 Unity 中序列化的二进制文件无法在 .NET 端序列化。
受影响的类型通常包括以下类型。
DateTimeOffset
ValueTuple
目前,简单的解决方案是不使用这些类型。
原生 AOT
Generic virtual method pointer lookup failure不幸的是,由于运行时错误,.NET 7 Native AOT 在使用 MemoryPack 时会导致崩溃( )。它将在 .NET 8 中得到修复。使用“Microsoft.DotNet.ILCompiler”预览版,将在 .NET 7 中修复它。请参阅问题评论以了解如何设置它。

二进制线路格式规范
和T中定义的类型称为 C# 架构。MemoryPack 格式不是自描述格式。反序列化需要相应的 C# 架构。这些类型作为二进制文件的内部表示存在,但如果没有 C# 架构,就无法确定类型。SerializeDeserialize

Endian 必须是Little Endian。但是,参考 C# 实现并不关心字节序,因此不能在大端机器上使用。然而,现代计算机通常是小端的。

共有八种格式。

1、Unmanaged struct
2、Object
3、Version Tolerant Object
4、Circular Reference Object
5、Tuple
6、Collection
7、String
8、Union

Unmanaged struct
Unmanaged struct is C# struct that doesn’t contain reference types, similar constraint of C# Unmanaged types. Serializing struct layout as it is, includes padding.

Object
(byte memberCount, [values…])

Object has 1byte unsigned byte as member count in header. Member count allows 0 to 249, 255 represents object is null. Values store memorypack value for the number of member count.

Version Tolerant Object
(byte memberCount, [varint byte-length-of-values…], [values…])

Version Tolerant Object is similar as Object but has byte length of values in header. varint follows these spec, first sbyte is value or typeCode and next X byte is value. 0 to 127 = unsigned byte value, -1 to -120 = signed byte value, -121 = byte, -122 = sbyte, -123 = ushort, -124 = short, -125 = uint, -126 = int, -127 = ulong, -128 = long.

Circular Reference Object
(byte memberCount, [varint byte-length-of-values…], varint referenceId, [values…])
(250, varint referenceId)

Circular Reference Object is similar as Version Tolerant Object but if memberCount is 250, next varint(unsigned-int32) is referenceId. If not, after byte-length-of-values, varint referenceId is written.

Tuple
(values…)

Tuple is fixed-size, non-nullable value collection. In .NET, KeyValuePair<TKey, TValue> and ValueTuple<T,…> are serialized as Tuple.

Collection
(int length, [values…])

Collection has 4 byte signed integer as data count in header, -1 represents null. Values store memorypack value for the number of length.

String
(int utf16-length, utf16-value)
(int ~utf8-byte-count, int utf16-length, utf8-bytes)

String has two-forms, UTF16 and UTF8. If first 4byte signed integer is -1, represents null. 0, represents empty. UTF16 is same as collection(serialize as ReadOnlySpan, utf16-value’s byte count is utf16-length * 2). If first signed integer <= -2, value is encoded by UTF8. utf8-byte-count is encoded in complement, ~utf8-byte-count to retrieve count of bytes. Next signed integer is utf16-length, it allows -1 that represents unknown length. utf8-bytes store bytes for the number of utf8-byte-count.

Union
(byte tag, value)
(250, ushort tag, value)

First unsigned byte is tag that for discriminated value type or flag, 0 to 249 represents tag, 250 represents next unsigned short is tag, 255 represents union is null.

😍😍 海量H5小游戏、微信小游戏、Web casualgame源码😍😍
😍😍试玩地址: https://www.bojiogame.sg😍😍
😍看上哪一款,需要源码的csdn私信我😍

————————————————

​最后我们放松一下眼睛
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Unity中的protobuf序列化是一种将数据结构转化为字节流的方法,以便在网络传输、存储或在不同平台之间传递数据。它基于Google的protobuf (Protocol Buffers)协议,能够高效地序列化和反序列化复杂的数据结构。 在Unity中使用protobuf序列化需要进行以下几个步骤: 1. 定义消息结构:首先需要在.proto文件中定义要序列化的数据结构。这包括定义消息的字段、枚举、嵌套消息等。可以指定每个字段的类型(整数、浮点数、字符串等)和标签(用于标识字段的唯一性)等信息。 2. 编译.proto文件:使用protobuf编译器将.proto文件编译为相应语言的代码。在Unity中可以使用Protobuf-net等第三方插件来生成C#代码。 3. 序列化数据:在需要序列化数据的地方,将数据按照定义好的消息结构进行赋值,并使用protobuf提供的方法将其序列化为字节流。 4. 反序列化数据:在接收端或需要解析数据的地方,使用protobuf提供的方法将字节流反序列化为消息对象,然后可以通过读取字段的方式获取其中的数据。 使用unity protobuf序列化的好处是: 1. 空间效率高:protobuf采用二进制格式进行序列化,可以将数据压缩为较小的字节流,减少网络传输和存储的空间成本。 2. 速度快:protobuf的序列化和反序列化速度较快,可以更有效地处理大量的数据。 3. 跨平台兼容性好:使用protobuf序列化后的数据可以在不同平台、不同语言之间共享和传输,无需担心兼容性问题。 总之,Unity中的protobuf序列化是一种在网络传输和数据存储中高效、方便的数据序列化方法,可以帮助开发者更好地处理数据结构和跨平台数据传输的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

极致人生-010

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值