【C#】ref与out使用区别探究

       在C#语言中,ref与out都是引用传递关键字,旨在将一个对象传递进一个方法后,返回此方法“加工”后的对象,还可用于实际需求需要函数返回多个返回值。那么有没有什么情况下,只能用其一?有的。一般性的面向过程开发的代码编写,两者我认为是可以换用没问题,但在面向对象中,有时只能用其一。下面来看看此情况:

       假设我们现在需要一个通信层发送命令,我们将所有命令都缓存到一个队列里面,交由专门的线程发送。首先定义一个基本命令实体,包含公共属性如版本号、会话ID、字节长度等
 

    public class CmdModel
    {
        //版本
        public byte Version { get; set; }
        //会话ID
        public uint SessionId { get; set; }
        //命令类型
        public byte CmdType { get; set; }
        //整条命令byte数组长度
        public int CmdLen { get; set; }
        //整条命令字节数据
        public string CmdData { get; set; }

        //将所有的子类型的公共属性值,赋值给为CmdModel父类型的外部变量,因为发送命令只需要CmdModel父类型包含的字段就可以了
        public virtual void ConvertToCmd(ref CmdModel pCmd)
        {
            pCmd.Version = Version;
            pCmd.SessionId = SessionId;
            pCmd.CmdType = CmdType;
            pCmd.CmdLen = CmdLen;
            pCmd.CmdData = CmdData;
        }
    }

 

       由以上ConvertToCmdData方法加了ref我们也知道,外部调用需要的还是我们传递的参数pCmd。接着我们就定义上面提到的发送命令泛型方法:

 

 

    public void Send_CmdList(IEnumerable pCmdList) where T : CmdModel
    {
        try
        {
            foreach (var cmd in pCmdList)   //逐个将具体的子命令插入到发送队列
            {
                CmdModel Cmd = new CmdModel();
                cmd.SessionId = GetSessionID(); //赋值SessionID
                cmd.ConvertToCmd(ref Cmd);
                mSendQueue.Enqueue(Cmd);
            }
        }
        catch (Exception ex)
        {
            //错误处理
        }
    }

       具体子类的发送命令,继承自基本命令实体CmdModel:

    /// 
    /// 变化声道
    /// 
    public class ChangeVoid : CmdModel
    {
        //机器编号(4字节)
        public ulong MachineID { get; set; }
        //音量等级(4字节)
        public int VoiceRank { get; set; }
        //播放类型(4字节)
        public int VoiceTye { get; set; }

        //转换为CASCmd
        public override void ConvertToCmd(ref CmdModel pCmd)
        {
            try
            {
                StringBuilder BodyData = new StringBuilder();
                BodyData.Append(MachineID.ToString("X8"));
                BodyData.Append(VoiceRank.ToString("X8"));
                BodyData.Append(VoiceTye.ToString("X8"));
                pCmd.CmdLen = (BodyData.Length / 2);//字符串长度除2就是数组长度
                pCmd.CmdData = BodyData.ToString();
                base.ConvertToCmd(ref pCmd);
            }
            catch (Exception ex)
            {
                return;
            }
        }
    }

       好了,简单的通信机制层大概好了。下面讲重点,如果将Send_CmdList方法内的ConvertToCmd(ref pCmd)调用,ref改为out。由于带有out参数的函数,必须在函数内部赋值(ref的话可不赋值),那么子类和基类的同名函数ConvertToCmd也要修改,以便满足pCmd在函数内部必须赋值才能传递出去,所以多了下面一行标为红色的代码,出现以下形式:

    public virtual void ConvertToCmd(out CmdModel pCmd)
    {
        pCmd = new CmdModel();   //基类添加了此行
        pCmd.Version = Version;
        pCmd.SessionId = SessionId;
        pCmd.CmdType = CmdType;
        pCmd.CmdLen = CmdLen;
        pCmd.CmdData = CmdData;
    }
    public override void ConvertToCmd(out CmdModel pCmd)
    {
        try
        {
            pCmd = new CmdModel();   //子类添加了此行
            StringBuilder BodyData = new StringBuilder();
            BodyData.Append(MachineID.ToString("X8"));
            BodyData.Append(VoiceRank.ToString("X8"));
            BodyData.Append(VoiceTye.ToString("X8"));
            pCmd.CmdLen = (BodyData.Length / 2);//字符串长度除2就是数组长度
            pCmd.CmdData = BodyData.ToString();
            base.ConvertToCmd(out pCmd);    //这里也要改
        }
        catch (Exception ex)
        {
            return;
        }
    }

       发现问题了吧,就出在这里,子类调用父类的同名方法,里面又重新构造了一个pCmd,这个时候pCmd已经重新赋值为一个新对象了,具体子类中ConvertToCmd方法的“加工”操作,实际上是没有成功赋值到外部要调用的pCmd对象。
当然,这里我们把base.ConvertToCmd(out pCmd)方法的实现,交给具体子类来实现,是不会有此问题的,只构造一次始终是那一个对象。只是这样就会出现大量重复代码,也不符合面向对象设计思想。另外将ConvertToCmd的所有实现交给基类也不会有问题,即子类中该函数只有一行代码:

 

    public override void ConvertToCmd(out CmdModel pCmd)
    {
        base.ConvertToCmd(out pCmd);   
    }

       这相当于子类通过base.ConvertToCmd(out pCmd)方法再进行了一次传递而已,只有在这种情况下,不需要在函数实现中为out指代的参数赋值。其实这种直接进行方法传递的情况,意思上可以理解为还是只传递了一次,因为ChangeVoid.ConvertToCmd(out CmdModel pCmd)就是base.ConvertToCmd(out pCmd); 

       究其原因,还是out指代的参数必须在函数内部实现中赋值导致,而ref只起一个加工传递作用,在函数内部可以赋值也可以不赋值,最后加工传递完成回来的还是那个对象,而out指代的参数,在函数内部必须赋值,除非直接进行方法传递。这里由于是对象就必须重新构造赋值,经历两层以上加工传递(构造),出来后已不是之前那个对象了。

       或者换一个经典说法:ref是有进有出,而out是只出不进!

你可能感兴趣的:(C#,C#,ref与out,使用区别,探究,只用其一)