C#中的干式序列化:抽象、委托还是反射?

C#中的干式序列化:抽象、委托还是反射?,c#,serialization,delegates,dry,abstract,C#,Serialization,Delegates,Dry,Abstract,我正在为一个网络多人游戏(反)序列化一些数据结构,对于每个要(反)序列化的数据结构,我只想为可维护性定义一次(反)序列化顺序 我可以使用C#的抽象类方法来实现我的目标,但这样做有几个问题: 性能:我不希望因为对内置类型使用“ref”参数而导致装箱 性能:为每个(反)序列化操作添加额外的抽象方法调用也不理想 额外键入:我必须不必要地初始化所有反序列化的变量,因为C#不允许我交换传递“ref”和“out”参数 如何避免重复数据结构(反)序列化的定义,同时避免上述部分或全部问题 (我玩弄了代表和思考,

我正在为一个网络多人游戏(反)序列化一些数据结构,对于每个要(反)序列化的数据结构,我只想为可维护性定义一次(反)序列化顺序

我可以使用C#的抽象类方法来实现我的目标,但这样做有几个问题:

  • 性能:我不希望因为对内置类型使用“ref”参数而导致装箱

  • 性能:为每个(反)序列化操作添加额外的抽象方法调用也不理想

  • 额外键入:我必须不必要地初始化所有反序列化的变量,因为C#不允许我交换传递“ref”和“out”参数

  • 如何避免重复数据结构(反)序列化的定义,同时避免上述部分或全部问题

    (我玩弄了代表和思考,但我最容易想到这个解决方案):

    public struct ControllerSnapshot
    {
    公共矢量2米尺;
    静态专用无效操作(参考浮动lStickX,参考浮动lStickY,操作操作)
    {
    //在此处定义(反)序列化顺序一次且仅一次
    lStickX=op.Invoke(lStickX);
    lStickY=op.Invoke(lStickY);
    }
    公共控制器快照(uLink.BitStream BitStream)
    {   
    OpRead OpRead=新的OpRead(比特流);
    浮子lStickX,lStickY;
    lStickX=lStickY=0.0f;//3.不能使用“out”;必须匹配Op的“ref”参数
    Op(参考lStickX,参考lStickY,opRead);
    m_lStick=新向量2(lStickX,lStickY);
    }
    公共void序列化(uLink.BitStream BitStream)
    {
    OpWrite OpWrite=新的OpWrite(位流);
    Op(参考m_lStick.x,参考m_lStick.y,opWrite);
    }
    };
    //为了实现上述功能,需要对其进行定义:
    抽象类Op
    {
    公共操作(uLink.BitStream BitStream)
    {
    m_bitStream=位流;
    }
    抽象公共T调用(T arg);
    受保护的uLink.BitStream m_BitStream;
    }
    类别:Op
    {
    公共OpRead(uLink.BitStream BitStream):基(BitStream){}
    重写公共T调用(T参数)
    {
    返回m_bitStream.Read();
    }
    }
    类OpWrite:Op
    {
    公共操作写入(uLink.BitStream BitStream):基(BitStream){}
    重写公共T调用(T参数)
    {
    m_位流写入(arg);
    返回arg;
    }
    }   
    //相比之下,“明显的”代码重复了(反)序列化的顺序,我
    //要避免,尤其是在(反)序列化变得越来越复杂的情况下:
    公共控制器快照(uLink.BitStream BitStream)
    {
    浮子lStickX,lStickY;
    lStickX=bitStream.Read();
    lStickY=bitStream.Read();
    m_lStick=新向量2(lStickX,lStickY);
    }
    公共void序列化(uLink.BitStream BitStream)
    {
    写入(m_lStick.x);
    bitStream.Write(m_lStick.y);
    }
    
    首先,我认为虚拟方法调用在这里不会有任何区别——您正在序列化对象以便通过网络传输。额外的几纳秒是没有意义的

    您提出的解决方案也会让您重复自己的操作—您仍然需要在序列化和反序列化中指定属性

    唯一可以避免这种情况的方法是以某种方式指定哪些字段应该序列化,并使用某种类型的反射,就像.NET的序列化程序十年来一直在做的那样

    更新:


    事实上,为什么不使用谷歌的

    您是否尝试过使用任何标准的序列化方法?为什么它们不适合您的需要?我不确定您所说的“标准序列化方法”是什么意思,所以我不知道它是否适合我的需要。您引用的方法是什么?我的意思是:二进制序列化(请参见
    BinaryFormatter
    class)、XML序列化(请参见
    XmlSerializer
    class)、数据契约序列化(请参见
    DataContractSerializer
    DataContractJsonSerializer
    NetDataContractSerializer
    类)。看起来你是在试图重新发明轮子。我没有任何经验,这些类(直到最近我主要是C++程序员,和C语言等语言的“轻”用户),但我的第一本能是因为这个项目依赖于网络库(ULink),我们没有这个来源,我怀疑这些类与像uLink.BitStream这样的数据结构不兼容。你认为我错了吗?我不知道什么是
    uLink
    ,但是如果它可以传输字节数组,你可以使用任何序列化程序,因为序列化的结果最终是字节数组。顺便说一句,正如@zmbq提到的,Protobufs在这里也是一个很好的选择。您可能是对的,虚拟方法调用不是一个重大的性能问题,但我们以60fps的速度运行,所以我尝试在合理的情况下避免任何不必要的处理。我重复属性规范的另一个好处是——我认为这种重复虽然不理想,但不太可能导致维护错误,而重复(反)序列化的顺序是一种典型的维护错误场景。你会说.NET的序列化反射是一种更好的方法吗?我认为你的第二个版本——简短的版本——是你能得到的最快的版本。除非你使用其他人已经优化过的东西。我记得这样一件事。我同意第二个读/写版本会运行得最快,但我很乐意运行得稍微慢一点——比方说慢0.00016ms,并不是说测量这种“千次切割的死亡”很容易性能成本:P——如果我能在项目过程中避免几小时的维护错误修复;你认为谷歌的Protobufs会与像uLink这样的第三方库兼容吗?我不知道,你必须检查一下。
    public struct ControllerSnapshot
    {
        public Vector2 m_lStick;
    
        static private void Op(ref float lStickX, ref float lStickY, Op<float> op)
        {
            //define (de)serialization order here once and only once
            lStickX = op.Invoke(lStickX);
            lStickY = op.Invoke(lStickY);
        }
        public ControllerSnapshot(uLink.BitStream bitStream)
        {   
            OpRead<float> opRead = new OpRead<float>(bitStream);
            float lStickX,lStickY;
            lStickX = lStickY = 0.0f;//3.can't use "out"; have to match Op's "ref" params
            Op(ref lStickX,ref lStickY,opRead);
    
            m_lStick = new Vector2(lStickX,lStickY);
        }
        public void Serialize(uLink.BitStream bitStream)
        {
            OpWrite<float> opWrite = new OpWrite<float>(bitStream);
            Op(ref m_lStick.x, ref m_lStick.y, opWrite);
        }
    };
    
    //in order to make the above work this needs to be defined:
    abstract class Op<T>
    {
        public Op(uLink.BitStream bitStream)
        {
            m_bitStream = bitStream;
        }
        abstract public T Invoke(T arg);
    
        protected uLink.BitStream m_bitStream;
    }
    class OpRead<T>:Op<T>
    {
        public OpRead(uLink.BitStream bitStream) : base(bitStream) { }
        override public T Invoke(T arg)
        {
            return m_bitStream.Read<T>();
        }
    }
    class OpWrite<T>:Op<T>
    {
        public OpWrite(uLink.BitStream bitStream) : base(bitStream) { }
        override public T Invoke(T arg)
        {
            m_bitStream.Write<T>(arg);
            return arg;
        }
    }   
    
    //by contrast, the "obvious" code duplicates the order of (de)serialization, which I
    //want to avoid, especially as (de)serialization becomes increasingly complex:
    public ControllerSnapshot(uLink.BitStream bitStream)
    {
        float lStickX,lStickY;
        lStickX = bitStream.Read<float>();
        lStickY = bitStream.Read<float>();
    
        m_lStick = new Vector2(lStickX,lStickY);
    }
    public void Serialize(uLink.BitStream bitStream)
    {
        bitStream.Write<float>(m_lStick.x);
        bitStream.Write<float>(m_lStick.y);
    }