You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Protobuf-Net中实现自定义条件合并序列化的问题(适配Godot节点场景)

Protobuf-Net中实现自定义条件合并序列化的问题(适配Godot节点场景)

我太懂你这种纠结了——要处理Godot节点的序列化,既要照顾循环引用、对象复用,又不能随便创建新节点(毕竟Godot实例化开销真的大),好不容易找到protobuf-net 2.4.6版本能用AsReference,结果自定义代理的转换又踩了坑。咱们先把问题拆开来捋清楚,再一步步解决。

问题根源分析

你遇到的InvalidCastException本质上是protobuf-net反序列化嵌套对象时,没有正确触发你写的隐式转换运算符。隐式转换是编译时的语法糖,当protobuf-net在内部处理嵌套的IdentifiableSurrogate时,它会直接尝试把代理对象强制转换成目标类型(比如TestIdentifiable),而不会主动调用你定义的隐式转换方法——这也是为什么Merge能正常工作,Deserialize却报错:Merge是基于现有对象的合并逻辑,处理流程和直接反序列化全新对象不一样。

另外你的代理递归逻辑里,suppressSurrogate的控制可能也有漏洞,导致嵌套序列化时生成的代理结构在反序列化时无法被正确解析成目标对象。

修复方案:改用显式序列化代理接口

放弃隐式转换,改用protobuf-net提供的ISerializationSurrogate接口来实现自定义序列化/反序列化逻辑,这样能完全掌控对象的转换流程,避免隐式转换的运行时问题。

具体步骤

  1. IdentifiableSurrogate<T>实现ISerializationSurrogate接口,用GetObjectSetObject方法来处理对象和代理的转换,而不是隐式转换。
  2. 完善递归控制的逻辑,确保嵌套序列化时不会无限触发代理,同时在反序列化时优先从实例缓存中获取对象,没有的话再创建(或者按你的逻辑合并)。
  3. 在protobuf的全局配置里注册这个代理,确保所有实现IIdentifiable的类型都能被正确处理。

修改后的代码示例

using System;
using System.Collections.Generic;
using System.IO;
using ProtoBuf;
using ProtoBuf.Meta;

public class Test
{
    public static void Main()
    {
        // 注册代理到protobuf全局配置
        RuntimeTypeModel.Default.Add(typeof(TestIdentifiable), false)
            .SetSurrogate(typeof(IdentifiableSurrogate<TestIdentifiable>));
        
        IdentifiableSurrogate<TestIdentifiable>.printDebugMessages = true;
        Console.WriteLine("---START---");
        TestIdentifiable before = new TestIdentifiable(5);
        Console.WriteLine("---SERIALIZING---");
        using MemoryStream serializeStream = new MemoryStream();
        Serializer.Serialize(serializeStream, before);
        byte[] bytes = serializeStream.ToArray();
        Console.WriteLine("---DESERIALIZING---");
        // 模拟接收端没有该对象的场景
        IIdentifiable.UnregisterInstance(before);
        using MemoryStream deserializeStream = new MemoryStream(bytes);
        TestIdentifiable after = Serializer.Deserialize<TestIdentifiable>(deserializeStream);
        Console.WriteLine("---END---");
        Console.WriteLine("Before: " + before);
        Console.WriteLine("After: " + after);
        Console.WriteLine("Same Object? " + (before == after));
    }
}

public interface IIdentifiable
{
    private static Dictionary<string, IIdentifiable> instances = new Dictionary<string, IIdentifiable>();
    public static IIdentifiable? TryGetInstance(string id) => instances.GetValueOrDefault(id);
    public static void RegisterInstance(IIdentifiable instance)
    {
        if (string.IsNullOrEmpty(instance.Id))
            instance.Id = Guid.NewGuid().ToString();
        if (!instances.TryAdd(instance.Id, instance))
            throw new Exception("Identifiable instance already exists");
    }
    public static void UnregisterInstance(IIdentifiable instance) => instances.Remove(instance.Id);
    public string Id { get; set; }
}

[ProtoContract]
public class IdentifiableSurrogate<T> : ISerializationSurrogate where T : class, IIdentifiable, new()
{
    public static bool printDebugMessages = false;
    [ProtoMember(1)] public string? Id;
    [ProtoMember(2)] public byte[]? SerializedData;
    [ProtoMember(3)] public T? DirectData;

    [ThreadStatic] private static bool _isProcessing;

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
    {
        var identifiable = (T)obj;
        if (_isProcessing)
        {
            // 递归序列化时,直接传递实例,避免无限代理
            DirectData = identifiable;
            if (printDebugMessages)
                Console.WriteLine($"递归序列化:直接传递{typeof(T).FullName}实例,ID={identifiable.Id}");
            return;
        }

        try
        {
            _isProcessing = true;
            Id = identifiable.Id;
            // 序列化对象本身,这里会触发代理的递归处理
            using var ms = new MemoryStream();
            Serializer.Serialize(ms, identifiable);
            SerializedData = ms.ToArray();
            if (printDebugMessages)
                Console.WriteLine($"序列化代理:{typeof(T).FullName},ID={Id}");
        }
        finally
        {
            _isProcessing = false;
        }
    }

    public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
    {
        if (DirectData != null)
        {
            if (printDebugMessages)
                Console.WriteLine($"反序列化:直接使用{typeof(T).FullName}实例,ID={DirectData.Id}");
            return DirectData;
        }

        if (string.IsNullOrEmpty(Id) || SerializedData == null)
            throw new ArgumentException("代理必须包含ID和序列化数据,或者直接实例");

        // 优先从缓存获取现有实例
        var existing = IIdentifiable.TryGetInstance(Id) as T;
        if (existing != null)
        {
            if (printDebugMessages)
                Console.WriteLine($"反序列化:合并到现有{typeof(T).FullName}实例,ID={Id}");
            // 合并数据到现有实例
            using var ms = new MemoryStream(SerializedData);
            Serializer.Merge(ms, existing);
            return existing;
        }

        // 缓存中没有,创建新实例并注册
        if (printDebugMessages)
            Console.WriteLine($"反序列化:创建新{typeof(T).FullName}实例,ID={Id}");
        var newInstance = new T();
        newInstance.Id = Id;
        IIdentifiable.RegisterInstance(newInstance);
        using var newMs = new MemoryStream(SerializedData);
        Serializer.Merge(newMs, newInstance);
        return newInstance;
    }
}

[ProtoContract]
public class TestIdentifiable : IIdentifiable
{
    [ProtoMember(1)] public int Value { get; set; }
    public string Id { get; set; } = "";

    public TestIdentifiable() {}
    public TestIdentifiable(int value)
    {
        Value = value;
        IIdentifiable.RegisterInstance(this);
    }

    public override string ToString() => $"TestIdentifiable(ID={Id}, Value={Value})";
}

关键改动说明

  • 改用ISerializationSurrogate接口,显式控制对象和代理的转换逻辑,避免隐式转换的运行时问题。
  • _isProcessing标记控制递归序列化,防止无限触发代理。
  • 反序列化时优先从IIdentifiable的实例缓存中获取对象,存在则合并数据,不存在才创建新实例,完全符合你“不临时创建对象”的需求。
  • 全局注册代理,确保protobuf-net能正确识别目标类型和代理的映射关系。

备选库建议

如果不想在protobuf-net上继续折腾,这里有两个适配你需求的库可以考虑:

  • MessagePack for C#:支持循环引用、自定义对象解析器,能精准控制是否复用现有实例,性能和protobuf-net不相上下,对Godot的兼容性也很好。
  • Newtonsoft.Json:开启PreserveReferencesHandling.Objects支持循环引用,配合自定义JsonConverter可以实现合并逻辑,虽然性能不如前两者,但灵活性极高,适合复杂的自定义场景。

内容来源于stack exchange

火山引擎 最新活动